Compare commits
21 Commits
feat/AGP8-
...
feat/cloud
| Author | SHA1 | Date | |
|---|---|---|---|
| 718ed2d209 | |||
| e3f883b784 | |||
| fbb81d7e45 | |||
| 750ec04c9d | |||
| e23a3d3938 | |||
| d75020cdb5 | |||
| c56c71d1f1 | |||
| ccfa50d748 | |||
| 7176a5a4c4 | |||
| d007092193 | |||
| 2552743ff2 | |||
| 4faf15ffe7 | |||
| 33d6ed75ad | |||
| 2eb4878b12 | |||
| 2bea3c72d7 | |||
| fa3db9d521 | |||
| efee9409bf | |||
| 38a7eaf780 | |||
| a1b4233fdb | |||
| 60c24e7457 | |||
| 729685e764 |
@ -5,12 +5,10 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'therouter'
|
||||
apply plugin: 'com.sensorsdata.analytics.android'
|
||||
|
||||
import groovy.xml.XmlUtil
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter'
|
||||
|
||||
String CONFIG_ID = ""
|
||||
String FIRST_LAUNCH = ""
|
||||
@ -23,7 +21,6 @@ android {
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
dataBinding true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@ -186,16 +183,6 @@ android {
|
||||
|
||||
flavorDimensions("env", "region")
|
||||
|
||||
sensorsAnalytics {
|
||||
sdk {
|
||||
disableIMEI = true
|
||||
disableCarrier = true
|
||||
disableMacAddress = true
|
||||
}
|
||||
|
||||
disableModules = ['AUTOTRACK', 'PUSH']
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
||||
debug {
|
||||
@ -551,6 +538,10 @@ dependencies {
|
||||
if(!gradle.ext.excludeOptionalModules || gradle.ext.enableWechatPay){
|
||||
implementation(project(":feature:wechat_pay"))
|
||||
}
|
||||
|
||||
if(!gradle.ext.excludeOptionalModules || gradle.ext.enableCloudGame){
|
||||
implementation(project(":feature:cloud_game"))
|
||||
}
|
||||
}
|
||||
|
||||
File propFile = file('sign.properties')
|
||||
|
||||
@ -7,61 +7,6 @@
|
||||
# Keep Attribute
|
||||
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod,SourceFile,LineNumberTable
|
||||
|
||||
# Remove log related code
|
||||
-assumenosideeffects class android.util.Log {
|
||||
public static *** v(...);
|
||||
public static *** d(...);
|
||||
public static *** i(...);
|
||||
public static *** w(...);
|
||||
public static *** e(...);
|
||||
public static *** println(...);
|
||||
public static *** isLoggable(...);
|
||||
}
|
||||
-assumenosideeffects class java.lang.Throwable {
|
||||
public *** printStackTrace(...);
|
||||
}
|
||||
-assumenosideeffects class java.io.PrintStream {
|
||||
public *** println(...);
|
||||
public *** print(...);
|
||||
}
|
||||
-assumenosideeffects class com.google.devtools.build.android.desugar.runtime.ThrowableExtension {
|
||||
public *** printStackTrace(...);
|
||||
}
|
||||
-assumenosideeffects class com.lightgame.utils.Utils {
|
||||
public static *** log(...);
|
||||
}
|
||||
-assumenosideeffects class com.gh.gamecenter.core.utils.MtaHelper {
|
||||
public static *** onEvent(...);
|
||||
public static *** onEventWithTime(...);
|
||||
public static *** onEventWithBasicDeviceInfo(...);
|
||||
}
|
||||
# Remove all logging calls via JDK Loggers. They are generally from
|
||||
# unused parts of third-party libraries.
|
||||
-assumenosideeffects class java.util.logging.Logger {
|
||||
void finest(...);
|
||||
void finer(...);
|
||||
void fine(...);
|
||||
void info(...);
|
||||
void warning(...);
|
||||
void severe(...);
|
||||
void throwing(...);
|
||||
void log(...);
|
||||
void logp(...);
|
||||
static java.util.logging.Logger getLogger(...) return _NONNULL_;
|
||||
boolean isLoggable(...) return false;
|
||||
}
|
||||
# Remove accesses to Level.<thing> that go unused.
|
||||
-assumenosideeffects class java.util.logging.Level {
|
||||
<fields>;
|
||||
# Flogger uses Level objects, so do not set a return value for intValue().
|
||||
int intValue();
|
||||
}
|
||||
# Remove fields of type Logger.
|
||||
-assumenosideeffects class * {
|
||||
java.util.logging.Logger * return _NONNULL_;
|
||||
}
|
||||
|
||||
|
||||
# OrmLite
|
||||
-keep class com.j256.*
|
||||
-keepclassmembers class com.j256.* { *; }
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.gh.gamecenter">
|
||||
|
||||
<queries>
|
||||
<package android:name="com.gh.gamecenter" />
|
||||
|
||||
@ -18,6 +18,7 @@ import com.gh.gamecenter.common.base.GlobalActivityManager
|
||||
import com.gh.gamecenter.common.provider.IHelpAndFeedbackProvider
|
||||
import com.gh.gamecenter.common.utils.PackageFlavorHelper
|
||||
import com.gh.gamecenter.core.provider.IPushProvider
|
||||
import com.gh.gamecenter.feature.cloudgame.CloudGameHelper
|
||||
import com.gh.gamecenter.login.utils.QuickLoginHelper
|
||||
import com.gh.gamecenter.login.view.LoginActivity
|
||||
import com.gh.gamecenter.va.VCore
|
||||
@ -106,6 +107,8 @@ class GlobalActivityLifecycleObserver : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
if (PackageFlavorHelper.IS_TEST_FLAVOR && activity is AppCompatActivity) {
|
||||
DarkModeSwitchHelper.showDarkModeSwitchFloatingView(activity)
|
||||
|
||||
CloudGameHelper.showCloudGameFloat(activity)
|
||||
}
|
||||
|
||||
if (activity is AppCompatActivity
|
||||
|
||||
@ -114,7 +114,6 @@ class GameDetailActivity : DownloadToolbarActivity() {
|
||||
view,
|
||||
listOf(
|
||||
R.id.menu_download_iv,
|
||||
R.id.gameBigEvent,
|
||||
R.id.cardContainer,
|
||||
R.id.iv_reserve,
|
||||
R.id.iv_concern,
|
||||
|
||||
@ -127,6 +127,8 @@ class GameDetailViewModel(
|
||||
var isSkipOnPageSelected = false // 是否触发论坛/专区Tab跳转
|
||||
|
||||
var defaultCoverEntity: CoverEntity? = null
|
||||
var coverTabSequence = 1 // 用于埋点,详情视频/图集tab 当前选中tab的序号,从1开始
|
||||
var coverTabName = "" // 用于埋点,详情视频/图集tab 详情视频/图集tab 当前选中tab的tab名称
|
||||
|
||||
var isGameInstalled = false
|
||||
private var isGameUpdatable = false
|
||||
|
||||
@ -187,15 +187,19 @@ class GameDetailFragment : LazyFragment(), IScrollable {
|
||||
coverSfv.goneIf(!shouldShowCoverFilter) {
|
||||
val defaultTabPosition =
|
||||
tabNameList.indexOfFirst { tabName -> viewModel.defaultCoverEntity?.tabName == tabName }
|
||||
viewModel.coverTabSequence = defaultTabPosition + 1
|
||||
viewModel.coverTabName = viewModel.defaultCoverEntity?.tabName ?: ""
|
||||
coverSfv.setItemList(tabNameList, if (defaultTabPosition != -1) defaultTabPosition else 0)
|
||||
coverSfv.setOnCheckedCallback { position ->
|
||||
val checkedText = tabNameList.getOrNull(position)
|
||||
val currentCoverEntity = it.getOrNull(coverPosition)
|
||||
viewModel.coverTabSequence = position + 1
|
||||
viewModel.coverTabName = checkedText ?: ""
|
||||
SensorsBridge.trackEvent("GameDetailMediaTabClick", json {
|
||||
"game_id" to gameEntity?.id
|
||||
"game_name" to gameEntity?.name
|
||||
"sequence" to position + 1
|
||||
"tab_name" to checkedText
|
||||
"sequence" to viewModel.coverTabSequence
|
||||
"tab_name" to viewModel.coverTabName
|
||||
})
|
||||
if (currentCoverEntity?.tabName == checkedText) return@setOnCheckedCallback
|
||||
|
||||
|
||||
@ -115,7 +115,6 @@ class GameDetailCoverAdapter(
|
||||
})
|
||||
.build(holder.binding.player)
|
||||
|
||||
holder.binding.player.gameName = viewModel.game?.name ?: ""
|
||||
holder.binding.player.viewModel = viewModel
|
||||
holder.binding.player.showOrHideCoverFilter = showOrHideCoverFilter
|
||||
holder.binding.player.video = topVideo
|
||||
|
||||
@ -143,13 +143,11 @@ class ServersCalendarActivity : ToolBarActivity() {
|
||||
setToolbarMenu(R.menu.menu_server_calendar_more)
|
||||
mBinding.subscribe.visibility = View.GONE
|
||||
mBinding.subscribeHint1.visibility = View.GONE
|
||||
mBinding.subscribeHint2.visibility = View.GONE
|
||||
mBinding.subscribe.setOnClickListener(null)
|
||||
} else {// 用户已登录并处于未订阅状态
|
||||
clearMenu()
|
||||
mBinding.subscribe.visibility = View.VISIBLE
|
||||
mBinding.subscribeHint1.visibility = View.VISIBLE
|
||||
mBinding.subscribeHint2.visibility = View.VISIBLE
|
||||
mBinding.subscribe.setOnClickListener {
|
||||
CheckLoginUtils.checkLogin(this, "游戏详情-开服日历表-开启订阅") {
|
||||
mViewModel.subscribeServer()
|
||||
|
||||
@ -238,7 +238,7 @@ class ServersCalendarDetailNoDataDialog : BottomSheetDialogFragment() {
|
||||
wechatRemindCheckIv.alpha = 1.0F
|
||||
wechatRemind.setOnClickListener {
|
||||
wechatRemindCheckIv.isChecked = !wechatRemindCheckIv.isChecked
|
||||
viewModel.wechatRemind = wechatRemindCheckIv.isChecked
|
||||
// viewModel.wechatRemind = wechatRemindCheckIv.isChecked
|
||||
SPUtils.setBoolean(Constants.SP_SERVERS_CALENDAR_BY_WECHAT, wechatRemindCheckIv.isChecked)
|
||||
}
|
||||
|
||||
|
||||
@ -30,14 +30,14 @@ class ServersCalendarDetailNoDataViewModel(
|
||||
|
||||
var appRemind: Boolean = false
|
||||
|
||||
var wechatRemind: Boolean = false
|
||||
val wechatRemind: Boolean = false
|
||||
|
||||
val timeInSeconds: Int = (serverTimeInMills / 1000).toInt()
|
||||
|
||||
fun initData(notifySetting: ServerCalendarNotifySetting?) {
|
||||
if (isDataInit) return
|
||||
appRemind = notifySetting?.byApp ?: SPUtils.getBoolean(Constants.SP_SERVERS_CALENDAR_BY_APP, true)
|
||||
wechatRemind = notifySetting?.byWechat ?: SPUtils.getBoolean(Constants.SP_SERVERS_CALENDAR_BY_WECHAT, true)
|
||||
// wechatRemind = notifySetting?.byWechat ?: SPUtils.getBoolean(Constants.SP_SERVERS_CALENDAR_BY_WECHAT, true)
|
||||
isDataInit = true
|
||||
}
|
||||
|
||||
|
||||
@ -308,7 +308,7 @@ class ServersCalendarDetailsRemindDialog : BottomSheetDialogFragment() {
|
||||
|
||||
viewBinding.wechatRemind.setOnClickListener {
|
||||
viewBinding.wechatRemindCheckIv.isChecked = !viewBinding.wechatRemindCheckIv.isChecked
|
||||
viewModel.wechatRemind = viewBinding.wechatRemindCheckIv.isChecked
|
||||
// viewModel.wechatRemind = viewBinding.wechatRemindCheckIv.isChecked
|
||||
SPUtils.setBoolean(Constants.SP_SERVERS_CALENDAR_BY_WECHAT, viewBinding.wechatRemindCheckIv.isChecked)
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ class ServersCalendarDetailsRemindViewModel(
|
||||
|
||||
var appRemind: Boolean = false
|
||||
|
||||
var wechatRemind: Boolean = false
|
||||
val wechatRemind: Boolean = false
|
||||
|
||||
val selectedNotifySeconds: Int get() = (selectedNotifyTime / 1000).toInt()
|
||||
|
||||
@ -58,7 +58,7 @@ class ServersCalendarDetailsRemindViewModel(
|
||||
selectedAdvancedTime = ServersCalendarAdvancedTime.valueOf(notifySetting?.secondsAdvance)
|
||||
selectedNotifyTime = (notifySetting?.notifyTime ?: calendarEntity.getTime()) * 1000
|
||||
appRemind = notifySetting?.byApp ?: SPUtils.getBoolean(Constants.SP_SERVERS_CALENDAR_BY_APP, true)
|
||||
wechatRemind = notifySetting?.byWechat ?: SPUtils.getBoolean(Constants.SP_SERVERS_CALENDAR_BY_WECHAT, true)
|
||||
// wechatRemind = notifySetting?.byWechat ?: SPUtils.getBoolean(Constants.SP_SERVERS_CALENDAR_BY_WECHAT, true)
|
||||
isDataInit = true
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment
|
||||
import com.gh.common.util.LogUtils
|
||||
import com.gh.download.cache.ExoCacheManager
|
||||
import com.gh.gamecenter.R
|
||||
import com.gh.gamecenter.common.base.GlobalActivityManager
|
||||
import com.gh.gamecenter.common.observer.MuteCallback
|
||||
import com.gh.gamecenter.common.observer.VolumeObserver
|
||||
import com.gh.gamecenter.common.utils.*
|
||||
@ -40,7 +41,6 @@ class TopVideoView @JvmOverloads constructor(context: Context, attrs: AttributeS
|
||||
private var mMuteCallback: MuteCallback
|
||||
private var mVolumeObserver: VolumeObserver? = null
|
||||
|
||||
var gameName = ""
|
||||
var video: CoverTabEntity.Video? = null
|
||||
var viewModel: GameDetailViewModel? = null
|
||||
var uuid = UUID.randomUUID().toString()
|
||||
@ -87,11 +87,21 @@ class TopVideoView @JvmOverloads constructor(context: Context, attrs: AttributeS
|
||||
}
|
||||
|
||||
setBackFromFullScreenListener {
|
||||
// if (it.id == R.id.fullscreen) {
|
||||
// MtaHelper.onEvent("游戏详情_顶部视频", "${getMtaKeyPrefix()}-退出全屏", combinedTitleAndId)
|
||||
// } else if (it.id == R.id.back) {
|
||||
// MtaHelper.onEvent("游戏详情_顶部视频", "${getMtaKeyPrefix()}-点击返回", combinedTitleAndId)
|
||||
// }
|
||||
if (it.id == R.id.fullscreen || it.id == R.id.back) {
|
||||
SensorsBridge.trackGameDetailVideoClick(
|
||||
gameName = viewModel?.game?.name ?: "",
|
||||
gameId = viewModel?.game?.id ?: "",
|
||||
gameType = viewModel?.game?.categoryChinese ?: "",
|
||||
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
|
||||
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
|
||||
action = if (it.id == R.id.fullscreen) "退出全屏" else "点击返回",
|
||||
playType = if (mIsAutoPlay) "自动播放" else "主动播放",
|
||||
isFullScreen = mIfCurrentIsFullscreen,
|
||||
sequence = viewModel?.coverTabSequence ?: 1,
|
||||
tabName = viewModel?.coverTabName ?: "",
|
||||
playLength = (currentPositionWhenPlaying / 1000).toString()
|
||||
)
|
||||
}
|
||||
clearFullscreenLayout()
|
||||
}
|
||||
|
||||
@ -126,13 +136,22 @@ class TopVideoView @JvmOverloads constructor(context: Context, attrs: AttributeS
|
||||
fun startPlayLogic(isAutoPlay: Boolean) {
|
||||
mIsAutoPlay = isAutoPlay
|
||||
violenceUpdateMuteStatus()
|
||||
// if (isAutoPlay) {
|
||||
// MtaHelper.onEvent("游戏详情_顶部视频", "视频播放方式", "自动播放")
|
||||
// MtaHelper.onEvent("游戏详情_顶部视频", "顶部视频-自动播放", combinedTitleAndId)
|
||||
// } else {
|
||||
// MtaHelper.onEvent("游戏详情_顶部视频", "视频播放方式", "手动播放")
|
||||
// }
|
||||
|
||||
if (isAutoPlay) {
|
||||
SensorsBridge.trackGameDetailVideoClick(
|
||||
gameName = viewModel?.game?.name ?: "",
|
||||
gameId = viewModel?.game?.id ?: "",
|
||||
gameType = viewModel?.game?.categoryChinese ?: "",
|
||||
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
|
||||
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
|
||||
action = "自动播放",
|
||||
playType = if (mIsAutoPlay) "自动播放" else "主动播放",
|
||||
isFullScreen = mIfCurrentIsFullscreen,
|
||||
sequence = viewModel?.coverTabSequence ?: 1,
|
||||
tabName = viewModel?.coverTabName ?: "",
|
||||
playLength = (currentPositionWhenPlaying / 1000).toString()
|
||||
)
|
||||
|
||||
val seekTime = ScrollCalculatorHelper.getPlaySchedule(MD5Utils.getContentMD5(video?.url))
|
||||
seekOnStart = seekTime
|
||||
mTouchingProgressBar = false
|
||||
@ -289,7 +308,7 @@ class TopVideoView @JvmOverloads constructor(context: Context, attrs: AttributeS
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
super.onStopTrackingTouch(seekBar)
|
||||
uploadVideoStreamingPlaying("拖动")
|
||||
uploadVideoStreamingPlaying("拖动进度条", seekBar)
|
||||
}
|
||||
|
||||
override fun isShowNetConfirm(): Boolean {
|
||||
@ -533,7 +552,7 @@ class TopVideoView @JvmOverloads constructor(context: Context, attrs: AttributeS
|
||||
}
|
||||
|
||||
override fun releaseVideos() {
|
||||
uploadVideoStreamingPlaying("结束播放")
|
||||
uploadVideoStreamingPlaying("播放完毕")
|
||||
CustomManager.releaseAllVideos(getKey())
|
||||
}
|
||||
|
||||
@ -556,11 +575,21 @@ class TopVideoView @JvmOverloads constructor(context: Context, attrs: AttributeS
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.start -> {
|
||||
// if (currentState == GSYVideoView.CURRENT_STATE_PLAYING) {
|
||||
// MtaHelper.onEvent("游戏详情_顶部视频", "${getMtaKeyPrefix()}-点击暂停", combinedTitleAndId)
|
||||
// } else {
|
||||
// MtaHelper.onEvent("游戏详情_顶部视频", "${getMtaKeyPrefix()}-点击播放", combinedTitleAndId)
|
||||
// }
|
||||
SensorsBridge.trackGameDetailVideoClick(
|
||||
gameName = viewModel?.game?.name ?: "",
|
||||
gameId = viewModel?.game?.id ?: "",
|
||||
gameType = viewModel?.game?.categoryChinese ?: "",
|
||||
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
|
||||
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
|
||||
action = if (currentState == GSYVideoView.CURRENT_STATE_PLAYING) "点击暂停" else "点击播放",
|
||||
playType = if (mIsAutoPlay) "自动播放" else "主动播放",
|
||||
isFullScreen = mIfCurrentIsFullscreen,
|
||||
sequence = viewModel?.coverTabSequence ?: 1,
|
||||
tabName = viewModel?.coverTabName ?: "",
|
||||
playLength = (currentPositionWhenPlaying / 1000).toString()
|
||||
)
|
||||
// 手动触发暂停/播放,后续行为都算主动播放
|
||||
mIsAutoPlay = false
|
||||
super.onClick(v)
|
||||
}
|
||||
|
||||
@ -568,13 +597,11 @@ class TopVideoView @JvmOverloads constructor(context: Context, attrs: AttributeS
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMtaKeyPrefix() = if (mIfCurrentIsFullscreen) "顶部视频(全屏)" else "顶部视频"
|
||||
|
||||
fun getCurrentPosition(): Long {
|
||||
return mCurrentPosition
|
||||
}
|
||||
|
||||
fun uploadVideoStreamingPlaying(action: String) {
|
||||
fun uploadVideoStreamingPlaying(action: String, seekBar: SeekBar? = null) {
|
||||
if (video == null || video?.url.isNullOrEmpty()) return
|
||||
runOnIoThread(isHeavyWightTask = true) {
|
||||
val isLandscape = mOrientationUtils != null
|
||||
@ -598,6 +625,26 @@ class TopVideoView @JvmOverloads constructor(context: Context, attrs: AttributeS
|
||||
action, video?.videoId, video?.title, viewModel?.game?.id, viewModel?.game?.name,
|
||||
videoPlayModel, videoPlayStatus(), mContentLength, videoTotalTime, videoPlayTs, progress.toInt()
|
||||
)
|
||||
if (action != "开始播放" && action != "暂停播放" && action != "退出全屏") {
|
||||
val playLength = when (action) {
|
||||
"播放完毕" -> (duration / 1000).toString()
|
||||
"拖动进度条" -> ((seekBar?.progress ?: 0) * duration / 100000).toString()
|
||||
else -> (currentPositionWhenPlaying / 1000).toString()
|
||||
}
|
||||
SensorsBridge.trackGameDetailVideoClick(
|
||||
gameName = viewModel?.game?.name ?: "",
|
||||
gameId = viewModel?.game?.id ?: "",
|
||||
gameType = viewModel?.game?.categoryChinese ?: "",
|
||||
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
|
||||
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
|
||||
action = action,
|
||||
playType = if (mIsAutoPlay) "自动播放" else "主动播放",
|
||||
isFullScreen = mIfCurrentIsFullscreen,
|
||||
sequence = viewModel?.coverTabSequence ?: 1,
|
||||
tabName = viewModel?.coverTabName ?: "",
|
||||
playLength = playLength
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ object CustomViewExt {
|
||||
private fun getTestDescription(game: GameEntity): String {
|
||||
val timeText = TimeUtils.formatTestTime(game.test?.start ?: 0L)
|
||||
val eventName = if (game.test?.type == "删档内测") {
|
||||
R.string.first_release.toResString()
|
||||
R.string.delete_test.toResString()
|
||||
} else {
|
||||
R.string.go_live.toResString()
|
||||
}
|
||||
|
||||
@ -37,6 +37,8 @@ class NotificationColumnViewHolder(
|
||||
|
||||
private var isScrolling = false
|
||||
|
||||
private var targetPosition = -1
|
||||
|
||||
private val bannerController = BannerInRecyclerController {
|
||||
nextToPage()
|
||||
}
|
||||
@ -190,6 +192,10 @@ class NotificationColumnViewHolder(
|
||||
override fun onViewAttach(parent: RecyclerView?) {
|
||||
viewModel.shareHiddenNotifications.observe(lifecycleOwner, hiddenNotifiesObserver)
|
||||
bannerController.onViewAttachedToWindow(parent)
|
||||
val selectedPosition = (_item as? CustomCommonContentCollectionItem)?.selectedPosition ?: 0
|
||||
if (targetPosition != -1 && targetPosition != selectedPosition) {
|
||||
binding.rvNotification.scrollToPosition(targetPosition)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewDetach(parent: RecyclerView?) {
|
||||
@ -203,7 +209,10 @@ class NotificationColumnViewHolder(
|
||||
if (layoutManager is LinearLayoutManager) {
|
||||
val firstPosition = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
if (firstPosition != -1) {
|
||||
binding.rvNotification.smoothScrollToPosition(firstPosition + 1)
|
||||
// 请注意,有可能smoothScrollToPosition正在执行滚动动画时,当前ViewHolder会调用onViewDetach,导致RecyclerView无法滚动到目标位置,所以这里需要先记录目标位置
|
||||
// 当ViewHolder Detach之后,再次Attach时,检查targetPosition是否等于selectedPosition,如果不相等,说明发生了以上情况,需要再次调用scrollToPosition将RecyclerView滚动到指定位置
|
||||
targetPosition = firstPosition + 1
|
||||
binding.rvNotification.smoothScrollToPosition(targetPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,13 +16,13 @@ import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.blankj.utilcode.util.ThreadUtils;
|
||||
import com.gh.download.DownloadManager;
|
||||
import com.gh.gamecenter.BuildConfig;
|
||||
import com.gh.gamecenter.SplashScreenActivity;
|
||||
import com.gh.gamecenter.common.base.GlobalActivityManager;
|
||||
import com.gh.gamecenter.core.AppExecutor;
|
||||
import com.gh.ndownload.suspendwindow.NDownloadDrawOverlayPermissionWindowController;
|
||||
import com.gh.ndownload.suspendwindow.NDownloadSuspendWindowController;
|
||||
import com.gh.ndownload.suspendwindow.utils.NDownloadSuspendWindowHelper;
|
||||
import com.lightgame.BuildConfig;
|
||||
import com.lightgame.download.DownloadConfig;
|
||||
import com.lightgame.download.DownloadDao;
|
||||
import com.lightgame.download.DownloadEntity;
|
||||
|
||||
@ -2,23 +2,32 @@ package com.gh.vspace
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.gh.common.exposure.ExposureListener
|
||||
import com.gh.common.util.NewFlatLogUtils
|
||||
import com.gh.download.DownloadManager
|
||||
import com.gh.gamecenter.common.baselist.ListAdapter
|
||||
import com.gh.gamecenter.common.baselist.ListFragment
|
||||
import com.gh.gamecenter.common.baselist.LoadType
|
||||
import com.gh.gamecenter.common.constant.Constants
|
||||
import com.gh.gamecenter.common.constant.EntranceConsts
|
||||
import com.gh.gamecenter.common.utils.*
|
||||
import com.gh.gamecenter.common.utils.goneIf
|
||||
import com.gh.gamecenter.common.utils.setSwitchAnimation
|
||||
import com.gh.gamecenter.common.utils.toColor
|
||||
import com.gh.gamecenter.common.utils.viewModelProvider
|
||||
import com.gh.gamecenter.core.utils.SPUtils
|
||||
import com.gh.gamecenter.core.utils.ToastUtils
|
||||
import com.gh.gamecenter.databinding.FragmentVdownloadManagerBinding
|
||||
import com.gh.gamecenter.feature.entity.GameEntity
|
||||
import com.gh.gamecenter.history.IBatchDelete
|
||||
import com.gh.gamecenter.history.ManageOption
|
||||
import com.lightgame.download.DataWatcher
|
||||
import com.lightgame.download.DownloadEntity
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class VDownloadManagerFragment :
|
||||
ListFragment<GameEntity, VDownloadManagerViewModel>(), IBatchDelete {
|
||||
@ -93,8 +102,16 @@ class VDownloadManagerFragment :
|
||||
mListRv.addOnScrollListener(mExposureListener)
|
||||
}
|
||||
|
||||
VHelper.vGameDao.getAllLiveData().observe(viewLifecycleOwner) {
|
||||
onLoadRefresh()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mViewModel.vGames
|
||||
.catch {
|
||||
ToastUtils.showToast(it.message ?: "")
|
||||
}
|
||||
.collectLatest {
|
||||
onLoadRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -38,6 +38,8 @@ class VDownloadManagerViewModel(application: Application) :
|
||||
load(it)
|
||||
}
|
||||
|
||||
val vGames = VHelper.vGameDao.getAllGames()
|
||||
|
||||
fun refresh() {
|
||||
loadPublishSubject.onNext(LoadType.REFRESH)
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
package com.gh.vspace.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface VGameDao {
|
||||
@ -13,7 +16,7 @@ interface VGameDao {
|
||||
fun getAll(): List<VGameEntity>
|
||||
|
||||
@Query("SELECT * FROM v_game")
|
||||
fun getAllLiveData(): LiveData<List<VGameEntity>>
|
||||
fun getAllGames(): Flow<List<VGameEntity>>
|
||||
|
||||
@Query("DELETE FROM v_game WHERE packageName = :packageName")
|
||||
fun delete(packageName: String)
|
||||
|
||||
@ -187,6 +187,7 @@ class RealNameInfoFragment : ToolbarFragment() {
|
||||
mBinding.nameEt.inputType = InputType.TYPE_NULL
|
||||
mBinding.idCardEt.inputType = InputType.TYPE_NULL
|
||||
mBinding.nameEt.visibility = View.GONE
|
||||
mBinding.textDotIndicator.visibility = View.GONE
|
||||
mBinding.idCardEt.visibility = View.GONE
|
||||
mBinding.nameTv.visibility = View.GONE
|
||||
mBinding.idCardTv.visibility = View.GONE
|
||||
|
||||
@ -153,21 +153,8 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_marginBottom="7dp"
|
||||
android:text="@string/servers_calendar_subscribe_hint_1"
|
||||
android:textColor="@color/text_tertiary"
|
||||
android:textSize="11sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="@id/subscribe"
|
||||
app:layout_constraintBottom_toTopOf="@id/subscribe_hint_2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subscribe_hint_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:text="@string/servers_calendar_subscribe_hint_2"
|
||||
android:text="@string/servers_calendar_subscribe_hint_1"
|
||||
android:textColor="@color/text_tertiary"
|
||||
android:textSize="11sp"
|
||||
android:visibility="gone"
|
||||
|
||||
@ -128,6 +128,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:gravity="center"
|
||||
android:visibility="invisible"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintTop_toTopOf="@id/app_remind"
|
||||
app:layout_constraintStart_toEndOf="@id/app_remind">
|
||||
|
||||
@ -1,310 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/gamedetail_appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/ui_surface"
|
||||
android:fitsSystemWindows="true"
|
||||
android:gravity="center"
|
||||
app:layout_behavior="com.gh.gamecenter.common.view.FixAppBarLayoutBehavior"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/collapsingToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
app:contentScrim="@color/ui_surface"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed"
|
||||
app:scrimAnimationDuration="0"
|
||||
app:scrimVisibleHeightTrigger="105dp"
|
||||
app:titleEnabled="false">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/game_detail_video"
|
||||
layout="@layout/piece_game_detail_video" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/gameIconContainer"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="88dp"
|
||||
android:layout_marginRight="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.gh.gamecenter.feature.view.GameIconView
|
||||
android:id="@+id/gamedetail_iv_thumb"
|
||||
android:layout_width="88dp"
|
||||
android:layout_height="88dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
app:gameIconFadeDuration="0" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/gameDetailRankLl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="25dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="@drawable/bg_game_detail_rank"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:src="@drawable/ic_game_detail_rank_trophy" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gameDetailRankTv"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginLeft="2dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginRight="2dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="9sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="预约榜第1名" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginRight="6dp"
|
||||
android:src="@drawable/ic_game_detail_rank_arrow" />
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/gameTitleContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toStartOf="@+id/rating_score_container"
|
||||
app:layout_constraintStart_toEndOf="@+id/gameIconContainer"
|
||||
app:layout_constraintTop_toTopOf="@+id/gameIconContainer">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gamedetail_tv_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:lineSpacingExtra="2dp"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="地海争霸2" />
|
||||
|
||||
<com.gh.common.view.FlexLinearLayout
|
||||
android:id="@+id/gamedetail_gametag"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/recommendAgeIv"
|
||||
android:layout_width="58dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/gameIconContainer"
|
||||
app:layout_constraintStart_toEndOf="@+id/gameIconContainer"
|
||||
tools:src="@drawable/ic_recommend_age8"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/realnameHintTv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/bg_ebf8ff_radius_2"
|
||||
android:gravity="center"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingTop="0.5dp"
|
||||
android:text="登陆游戏后需进行实名认证"
|
||||
android:textColor="@color/text_theme"
|
||||
android:textSize="@dimen/tag_text_size"
|
||||
android:includeFontPadding="false"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/gameIconContainer"
|
||||
app:layout_constraintStart_toEndOf="@id/recommendAgeIv"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/rating_score_container"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/gameTitleContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/bg_game_detail_rating_score" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rating_score_average"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="8.5" />
|
||||
|
||||
</RelativeLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gameBigEvent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="left"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/bg_game_big_event"
|
||||
android:drawableLeft="@drawable/ic_game_detail_big_event_gray"
|
||||
android:drawableRight="@drawable/ic_game_detail_big_event_arrow_gray"
|
||||
android:drawablePadding="3dp"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:padding="4dp"
|
||||
android:textColor="@color/text_tertiary"
|
||||
android:textSize="10sp"
|
||||
android:visibility="gone"
|
||||
tools:text="游戏大事件游戏大事件"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contentCardContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/toolbarGapView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
android:background="@color/ui_background" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/Base_ToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="44dp"
|
||||
android:background="@android:color/transparent"
|
||||
app:contentInsetStartWithNavigation="0dp"
|
||||
app:layout_collapseMode="pin">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/toolbarContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/transparent"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:translationX="-10dp">
|
||||
|
||||
<com.gh.gamecenter.feature.view.GameIconView
|
||||
android:id="@+id/gamedetail_thumb_small"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleTv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/gamedetail_tabbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.gh.gamecenter.common.view.TabIndicatorView
|
||||
android:id="@+id/tab_indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/default_tab_indicator_height"
|
||||
android:layout_alignParentBottom="true" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_layout_height"
|
||||
app:tabIndicator="@null"
|
||||
app:tabTextAppearance="@style/TabLayoutTextAppearance" />
|
||||
|
||||
</RelativeLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.lightgame.view.NoScrollableViewPager
|
||||
android:id="@+id/gamedetail_vp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:overScrollMode="never"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@ -69,6 +69,7 @@
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintTop_toBottomOf="@id/remind_title"
|
||||
app:layout_constraintStart_toEndOf="@id/app_remind"
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
android:layout_height="112dp"
|
||||
android:background="@color/ui_surface"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:showIn="@layout/activity_main">
|
||||
tools:showIn="@layout/activity_cloudgame">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/video_placeholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.gh.gamecenter.common.view.StatusBarView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/appbar_height" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.gh.gamecenter.gamedetail.video.TopVideoView
|
||||
android:id="@+id/player"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintDimensionRatio="h,180:101"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
@ -480,7 +480,6 @@
|
||||
<string name="archive_limit_description">請選擇替換覆蓋的存檔</string>
|
||||
<string name="servers_calendar_subscribe">加入訂閱</string>
|
||||
<string name="servers_calendar_subscribe_hint_1">訂閱開服表,訂閱後助你獲得一手新服信息</string>
|
||||
<string name="servers_calendar_subscribe_hint_2">通知時間:8:00、12:00、18:00</string>
|
||||
<string name="servers_calendar_more_dialog_title">操作</string>
|
||||
<string name="servers_calendar_dialog_unsubscribe_title">取消訂閱</string>
|
||||
<string name="servers_calendar_dialog_unsubscribe_hint">不再接收開服消息</string>
|
||||
@ -686,9 +685,9 @@
|
||||
<string name="select_the_region">選擇加速區服</string>
|
||||
<string name="tips_for_new_users">新用戶免費3小時</string>
|
||||
<string name="recent_played">最近在玩</string>
|
||||
<string name="first_release">先發</string>
|
||||
<string name="go_live">上線</string>
|
||||
<string name="number_of_reservations">%1$s人預約</string>
|
||||
<string name="wechat_app_not_install_tips">請檢查是否安裝微信客戶端</string>
|
||||
<string name="delete_test">刪測</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -493,13 +493,12 @@
|
||||
<string name="archive_limit_description">请选择替换覆盖的存档</string>
|
||||
<string name="servers_calendar_subscribe">加入订阅</string>
|
||||
<string name="servers_calendar_subscribe_hint_1">订阅开服表,订阅后助你获得一手新服信息</string>
|
||||
<string name="servers_calendar_subscribe_hint_2">通知时间:8:00、12:00、18:00</string>
|
||||
<string name="servers_calendar_more_dialog_title">操作</string>
|
||||
<string name="servers_calendar_dialog_unsubscribe_title">取消订阅</string>
|
||||
<string name="servers_calendar_dialog_unsubscribe_hint">不再接收开服消息</string>
|
||||
<string name="servers_calendar_more_dialog_cancel_title">取消</string>
|
||||
<string name="servers_calendar_subscription_dialog_wechat_content">游戏发布新服信息时,您将在消息中心收到通知。为了避免错过通知,建议您开启微信公众号提醒</string>
|
||||
<string name="servers_calendar_subscription_dialog_content">游戏发布新服信息时,您将在消息中心和微信公众号收到通知,不会错过任何开服的消息</string>
|
||||
<string name="servers_calendar_subscription_dialog_content">游戏发布新服信息时,您将在消息中心收到通知,不会错过任何开服信息</string>
|
||||
<string name="servers_calendar_subscription_dialog_title">游戏订阅成功</string>
|
||||
<string name="servers_calendar_subscription_dialog_confirm">我知道了</string>
|
||||
<string name="servers_calendar_remind_time_setting_dialog_title">设置提醒时间</string>
|
||||
@ -686,8 +685,8 @@
|
||||
<string name="select_the_region">选择加速区服</string>
|
||||
<string name="tips_for_new_users">新用户免费3小时</string>
|
||||
<string name="recent_played">最近在玩</string>
|
||||
<string name="first_release">首发</string>
|
||||
<string name="go_live">上线</string>
|
||||
<string name="number_of_reservations">%1$s人预约</string>
|
||||
<string name="wechat_app_not_install_tips">请检查是否安装微信客户端</string>
|
||||
<string name="delete_test">删测</string>
|
||||
</resources>
|
||||
|
||||
19
build.gradle
19
build.gradle
@ -18,16 +18,16 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.0.2'
|
||||
classpath "com.android.tools.build:gradle:7.1.3"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.sensorsdata.analytics.android:android-gradle-plugin2:4.0.6'
|
||||
classpath 'com.sensorsdata.analytics.android:android-gradle-plugin2:3.5.3'
|
||||
classpath "com.lg.shadow.core:gradle-plugin:$shadow_version"
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'com.google.devtools.ksp' version '1.9.24-1.0.20' apply false
|
||||
id 'cn.therouter.agp8' version '1.2.4' apply false
|
||||
id 'cn.therouter' version '1.2.2' apply false
|
||||
}
|
||||
apply from: 'dependencies.gradle'
|
||||
apply from: 'vspace-bridge/config.gradle'
|
||||
@ -64,19 +64,6 @@ allprojects {
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
subprojects {
|
||||
afterEvaluate { project ->
|
||||
if (project.hasProperty('android')) {
|
||||
project.android {
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
subproject ->
|
||||
afterEvaluate {
|
||||
|
||||
@ -18,7 +18,7 @@ apply plugin: 'kotlin'
|
||||
dependencies {
|
||||
implementation gradleApi()
|
||||
implementation localGroovy()
|
||||
implementation 'com.android.tools.build:gradle:8.0.2'
|
||||
implementation "com.android.tools.build:gradle:7.1.3"
|
||||
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
implementation "commons-io:commons-io:2.4"
|
||||
implementation "org.javassist:javassist:3.25.0-GA"
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import com.android.build.api.instrumentation.AsmClassVisitorFactory
|
||||
import com.android.build.api.instrumentation.ClassContext
|
||||
import com.android.build.api.instrumentation.ClassData
|
||||
import com.android.build.api.instrumentation.InstrumentationParameters
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
|
||||
abstract class ActivityStartActivityForResultVisitorFactory :
|
||||
AsmClassVisitorFactory<InstrumentationParameters.None> {
|
||||
|
||||
override fun createClassVisitor(
|
||||
classContext: ClassContext,
|
||||
nextClassVisitor: ClassVisitor,
|
||||
): ClassVisitor {
|
||||
return ActivityStartActivityForResultVisitor(nextClassVisitor)
|
||||
}
|
||||
|
||||
override fun isInstrumentable(classData: ClassData): Boolean {
|
||||
return classData.className.contains("androidx.activity.ComponentActivity")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ActivityStartActivityForResultVisitor(nextVisitor: ClassVisitor) :
|
||||
ClassVisitor(Opcodes.ASM9, nextVisitor) {
|
||||
|
||||
override fun visitMethod(
|
||||
access: Int,
|
||||
name: String?,
|
||||
descriptor: String?,
|
||||
signature: String?,
|
||||
exceptions: Array<out String>?,
|
||||
): MethodVisitor {
|
||||
val originalMv = super.visitMethod(access, name, descriptor, signature, exceptions)
|
||||
|
||||
// Only modify the run() method
|
||||
return if (name == "startActivityForResult") {
|
||||
println("ActivityStartActivityForResultVisitor found startActivityForResult method")
|
||||
TryCatchMethodVisitor(Opcodes.ASM9, originalMv, access, name, descriptor)
|
||||
} else {
|
||||
originalMv
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import com.android.build.api.instrumentation.AsmClassVisitorFactory
|
||||
import com.android.build.api.instrumentation.ClassContext
|
||||
import com.android.build.api.instrumentation.ClassData
|
||||
import com.android.build.api.instrumentation.InstrumentationParameters
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
|
||||
abstract class AppCompatEditTextVisitorFactory
|
||||
: AsmClassVisitorFactory<InstrumentationParameters.None> {
|
||||
|
||||
override fun createClassVisitor(
|
||||
classContext: ClassContext,
|
||||
nextClassVisitor: ClassVisitor,
|
||||
): ClassVisitor {
|
||||
return AppCompatEditTextVisitor(nextClassVisitor)
|
||||
}
|
||||
|
||||
override fun isInstrumentable(classData: ClassData): Boolean {
|
||||
return classData.className.contains("androidx.appcompat.widget.AppCompatEditText")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AppCompatEditTextVisitor(nextVisitor: ClassVisitor) :
|
||||
ClassVisitor(Opcodes.ASM9, nextVisitor) {
|
||||
|
||||
override fun visitMethod(
|
||||
access: Int,
|
||||
name: String?,
|
||||
descriptor: String?,
|
||||
signature: String?,
|
||||
exceptions: Array<out String>?,
|
||||
): MethodVisitor {
|
||||
val originalMv = super.visitMethod(access, name, descriptor, signature, exceptions)
|
||||
|
||||
return if (name == "onTextContextMenuItem") {
|
||||
TryCatchMethodVisitor(Opcodes.ASM9, originalMv, access, name, descriptor, true)
|
||||
} else {
|
||||
originalMv
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import com.android.build.api.instrumentation.AsmClassVisitorFactory
|
||||
import com.android.build.api.instrumentation.ClassContext
|
||||
import com.android.build.api.instrumentation.ClassData
|
||||
import com.android.build.api.instrumentation.InstrumentationParameters
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
|
||||
abstract class DuplicateClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
|
||||
|
||||
override fun createClassVisitor(
|
||||
classContext: ClassContext,
|
||||
nextClassVisitor: ClassVisitor
|
||||
): ClassVisitor {
|
||||
val className = classContext.currentClassData.className.replace('.', '/')
|
||||
return DuplicateClassVisitor(nextClassVisitor, className)
|
||||
}
|
||||
|
||||
override fun isInstrumentable(classData: ClassData): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class DuplicateClassVisitor(nextVisitor: ClassVisitor, private val className: String)
|
||||
: ClassVisitor(Opcodes.ASM9, nextVisitor) {
|
||||
|
||||
private val duplicatedPackages = listOf(
|
||||
"com/google/android/material/chip",
|
||||
"com/google/android/material/textfield",
|
||||
"com/google/android/material/navigation",
|
||||
"com/google/android/material/datepicker",
|
||||
"com/google/android/material/circularreveal",
|
||||
"com/google/android/material/timepicker",
|
||||
"com/google/android/material/divider",
|
||||
"com/google/android/material/slider",
|
||||
"com/google/android/material/card",
|
||||
"com/google/android/material/transformation",
|
||||
"com/google/android/material/snackbar",
|
||||
"com/google/android/material/switchmaterial",
|
||||
"com/google/android/material/bottomappbar",
|
||||
"com/google/android/material/behavior",
|
||||
"com/google/android/material/floatingactionbutton",
|
||||
)
|
||||
|
||||
override fun visitEnd() {
|
||||
val isDuplicated = duplicatedPackages.any { className.contains(it) }
|
||||
if (isDuplicated) {
|
||||
println("DuplicateClassVisitor found duplicated class $className")
|
||||
} else {
|
||||
super.visitEnd()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,55 +1,20 @@
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import com.android.build.api.instrumentation.FramesComputationMode
|
||||
import com.android.build.api.instrumentation.InstrumentationScope
|
||||
import com.android.build.api.variant.AndroidComponentsExtension
|
||||
import com.android.build.gradle.AppExtension
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
|
||||
class GhPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(project: Project) {
|
||||
val appExtension = project.extensions.getByType(AndroidComponentsExtension::class.java)
|
||||
|
||||
appExtension.onVariants { variant ->
|
||||
// 移除无用的类
|
||||
// variant.instrumentation.transformClassesWith(
|
||||
// DuplicateClassVisitorFactory::class.java,
|
||||
// InstrumentationScope.ALL
|
||||
// ) {}
|
||||
|
||||
// plugin asset 目录处理
|
||||
variant.instrumentation.transformClassesWith(
|
||||
PluginAssetClassVisitorFactory::class.java,
|
||||
InstrumentationScope.ALL
|
||||
) {}
|
||||
|
||||
// Room 闪退 try catch 处理
|
||||
variant.instrumentation.transformClassesWith(
|
||||
RoomClassVisitorFactory::class.java,
|
||||
InstrumentationScope.ALL
|
||||
) {}
|
||||
|
||||
// ComponentActivity startActivityForResult 闪退 try catch 处理
|
||||
variant.instrumentation.transformClassesWith(
|
||||
ActivityStartActivityForResultVisitorFactory::class.java,
|
||||
InstrumentationScope.ALL
|
||||
) {}
|
||||
|
||||
// AppCompatEditText 闪退 try catch 处理 (主要出现在 VIVO 设备上)
|
||||
variant.instrumentation.transformClassesWith(
|
||||
AppCompatEditTextVisitorFactory::class.java,
|
||||
InstrumentationScope.ALL
|
||||
) {}
|
||||
|
||||
// QQ 小游戏获取系统 UA 的处理,避免提前初始化 WebView 导致启动耗时
|
||||
variant.instrumentation.transformClassesWith(
|
||||
MiniGameWebViewVisitorFactory::class.java,
|
||||
InstrumentationScope.ALL
|
||||
) {}
|
||||
|
||||
variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)
|
||||
}
|
||||
val log = project.logger
|
||||
log.error("========================")
|
||||
log.error("光环 transfrom 插件启动")
|
||||
log.error("========================")
|
||||
project.extensions.findByType(AppExtension::class.java)?.registerTransform(GhTransform(project))
|
||||
project.extensions.findByType(AppExtension::class.java)?.registerTransform(PluginAssetTransform())
|
||||
log.error("========================")
|
||||
log.error("光环 transfrom 插件结束")
|
||||
log.error("========================")
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,202 +1,202 @@
|
||||
//package com.gh.gamecenter.plugin
|
||||
//
|
||||
//import com.android.build.api.transform.Format
|
||||
//import com.android.build.api.transform.QualifiedContent
|
||||
//import com.android.build.api.transform.Transform
|
||||
//import com.android.build.api.transform.TransformInvocation
|
||||
//import com.android.build.gradle.AppExtension
|
||||
//import com.android.build.gradle.internal.pipeline.TransformManager
|
||||
//import com.gh.gamecenter.plugin.transform.*
|
||||
//import javassist.ClassPool
|
||||
//import org.apache.commons.io.FileUtils
|
||||
//import org.apache.commons.io.IOUtils
|
||||
//import org.gradle.api.Project
|
||||
//import java.io.BufferedOutputStream
|
||||
//import java.io.File
|
||||
//import java.io.FileInputStream
|
||||
//import java.io.FileOutputStream
|
||||
//import java.util.jar.JarEntry
|
||||
//import java.util.jar.JarFile
|
||||
//import java.util.jar.JarOutputStream
|
||||
//
|
||||
//class GhTransform(var project: Project) : Transform() {
|
||||
//
|
||||
// private val mClassPool = ClassPool.getDefault()
|
||||
// private val mTransformHelper = GhTransformHelper()
|
||||
// private val mExcludePackages = listOf(
|
||||
// "com/google/android/material/chip",
|
||||
// "com/google/android/material/textfield",
|
||||
// "com/google/android/material/navigation",
|
||||
// "com/google/android/material/datepicker",
|
||||
// "com/google/android/material/circularreveal",
|
||||
// "com/google/android/material/timepicker",
|
||||
// "com/google/android/material/divider",
|
||||
// "com/google/android/material/slider",
|
||||
// "com/google/android/material/card",
|
||||
// "com/google/android/material/transformation",
|
||||
// "com/google/android/material/snackbar",
|
||||
// "com/google/android/material/switchmaterial",
|
||||
// "com/google/android/material/bottomappbar",
|
||||
// "com/google/android/material/behavior",
|
||||
// "com/google/android/material/floatingactionbutton",
|
||||
// )
|
||||
//
|
||||
// override fun getName() = "GhTransform"
|
||||
//
|
||||
// init {
|
||||
// mTransformHelper.addTransformer(ExoSourceManagerTransformer())
|
||||
// mTransformHelper.addTransformer(DiskLruCacheTransformer())
|
||||
// mTransformHelper.addTransformer(RoomTransformer())
|
||||
// mTransformHelper.addTransformer(ActivityTransformer())
|
||||
// mTransformHelper.addTransformer(AppCompatEditTextTransformer())
|
||||
// mTransformHelper.addTransformer(MiniGameWebViewTransformer())
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 需要处理的数据类型,目前 ContentType有六种枚举类型,通常我们使用比较频繁的有前两种:
|
||||
// * 1、CONTENT_CLASS:表示需要处理 java 的 class 文件。
|
||||
// * 2、CONTENT_JARS:表示需要处理 java 的 class 与 资源文件。
|
||||
// * 3、CONTENT_RESOURCES:表示需要处理 java 的资源文件。
|
||||
// * 4、CONTENT_NATIVE_LIBS:表示需要处理 native 库的代码。
|
||||
// * 5、CONTENT_DEX:表示需要处理 DEX 文件。
|
||||
// * 6、CONTENT_DEX_WITH_RESOURCES:表示需要处理 DEX 与 java 的资源文件。
|
||||
// */
|
||||
// override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
|
||||
// return TransformManager.CONTENT_CLASS
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 是否增量编译
|
||||
// */
|
||||
// // TODO Why not?
|
||||
// override fun isIncremental() = false
|
||||
//
|
||||
// /**
|
||||
// * Transform 要操作的内容范围,目前 Scope 有五种基本类型:
|
||||
// * 1、PROJECT:只有项目内容
|
||||
// * 2、SUB_PROJECTS:只有子项目
|
||||
// * 3、EXTERNAL_LIBRARIES:只有外部库
|
||||
// * 4、TESTED_CODE:由当前变体(包括依赖项)所测试的代码
|
||||
// * 5、PROVIDED_ONLY:只提供本地或远程依赖项
|
||||
// * SCOPE_FULL_PROJECT 是一个Scope集合,包含Scope.PROJECT,Scope.SUB_PROJECTS,Scope.EXTERNAL_LIBRARIES 这三项,即当前Transform的作用域包括当前项目、子项目以及外部的依赖库
|
||||
// */
|
||||
// override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
|
||||
// // 通常我们使用 SCOPE_FULL_PROJECT
|
||||
// return TransformManager.SCOPE_FULL_PROJECT
|
||||
// }
|
||||
//
|
||||
// override fun transform(transformInvocation: TransformInvocation?) {
|
||||
// super.transform(transformInvocation)
|
||||
//
|
||||
// // 添加 android.jar path
|
||||
// mClassPool?.appendClassPath((project.extensions.findByType(AppExtension::class.java)!!.bootClasspath[0].toString()))
|
||||
//
|
||||
// val outputProvider = transformInvocation?.outputProvider
|
||||
// outputProvider?.deleteAll()
|
||||
//
|
||||
// transformInvocation?.inputs?.forEach { input ->
|
||||
// input.directoryInputs.forEach { dirInput ->
|
||||
// handleDirectory(dirInput.file)
|
||||
//
|
||||
// // 将input的目录复制到output指定目录
|
||||
// val dest = outputProvider?.getContentLocation(
|
||||
// dirInput.name,
|
||||
// dirInput.contentTypes,
|
||||
// dirInput.scopes,
|
||||
// Format.DIRECTORY
|
||||
// )
|
||||
// FileUtils.copyDirectory(dirInput.file, dest)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// var index = 0
|
||||
// transformInvocation?.inputs?.forEach { input ->
|
||||
// input.jarInputs.forEach { jarInput ->
|
||||
// if (jarInput.file.exists()) {
|
||||
// val srcFile = handleJar(jarInput.file)
|
||||
// // 必须给jar重新命名,否则会冲突
|
||||
// var jarName = jarInput.name
|
||||
// if (jarName.endsWith(".jar")) {
|
||||
// jarName = jarName.substring(0, jarName.length - 4)
|
||||
// }
|
||||
// val dest = outputProvider?.getContentLocation(
|
||||
// jarName + "_" + index,
|
||||
// jarInput.contentTypes,
|
||||
// jarInput.scopes,
|
||||
// Format.JAR
|
||||
// )
|
||||
// FileUtils.copyFile(srcFile, dest)
|
||||
// index++
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun handleDirectory(dir: File) {
|
||||
// // 将类路径添加到 classPool 中
|
||||
// mClassPool.insertClassPath(dir.absolutePath)
|
||||
//
|
||||
// if (dir.isDirectory) {
|
||||
// dir.walkTopDown().forEach { file ->
|
||||
// val filePath = file.absolutePath
|
||||
// mClassPool.insertClassPath(filePath)
|
||||
// if (shouldModify(filePath)) {
|
||||
// val inputStream = FileInputStream(file)
|
||||
// mTransformHelper.proceedModifyDir(filePath, inputStream)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 主要步骤:
|
||||
// * 1.遍历所有jar文件
|
||||
// * 2.解压jar然后遍历所有的class
|
||||
// * 3.读取class的输入流并使用javassit修改,然后保存到新的jar文件中
|
||||
// */
|
||||
// fun handleJar(jarFile: File): File {
|
||||
// mClassPool.appendClassPath(jarFile.absolutePath)
|
||||
// val inputJarFile = JarFile(jarFile)
|
||||
// val entries = inputJarFile.entries()
|
||||
// //创建一个新的文件
|
||||
// val outputJarFile = File(jarFile.parentFile, "temp_" + jarFile.name)
|
||||
// if (outputJarFile.exists()) outputJarFile.delete()
|
||||
// val jarOutputStream = JarOutputStream(BufferedOutputStream(FileOutputStream(outputJarFile)))
|
||||
// while (entries.hasMoreElements()) {
|
||||
// val jarInputEntry = entries.nextElement()
|
||||
// val jarInputEntryName = jarInputEntry.name
|
||||
// var isIgnore = false
|
||||
// for (i in mExcludePackages.indices){
|
||||
// if (jarInputEntryName.contains(mExcludePackages[i])) {
|
||||
// isIgnore = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if (isIgnore) continue
|
||||
// val outputJarEntry = JarEntry(jarInputEntryName)
|
||||
// jarOutputStream.putNextEntry(outputJarEntry)
|
||||
//
|
||||
// val inputStream = inputJarFile.getInputStream(jarInputEntry)
|
||||
// if (!shouldModify(jarInputEntryName)) {
|
||||
// jarOutputStream.write(IOUtils.toByteArray(inputStream))
|
||||
// inputStream.close()
|
||||
// continue
|
||||
// }
|
||||
// mTransformHelper.proceedModifyJar(jarInputEntryName, jarOutputStream, inputStream)
|
||||
// }
|
||||
// inputJarFile.close()
|
||||
// jarOutputStream.closeEntry()
|
||||
// jarOutputStream.flush()
|
||||
// jarOutputStream.close()
|
||||
// return outputJarFile
|
||||
// }
|
||||
//
|
||||
// private fun shouldModify(filePath: String): Boolean {
|
||||
// return filePath.endsWith(".class")
|
||||
// && !filePath.contains("R.class")
|
||||
//// && !filePath.contains("$")
|
||||
// && !filePath.contains("R$")
|
||||
// && !filePath.contains("BuildConfig.class")
|
||||
// }
|
||||
//
|
||||
//}
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import com.android.build.api.transform.Format
|
||||
import com.android.build.api.transform.QualifiedContent
|
||||
import com.android.build.api.transform.Transform
|
||||
import com.android.build.api.transform.TransformInvocation
|
||||
import com.android.build.gradle.AppExtension
|
||||
import com.android.build.gradle.internal.pipeline.TransformManager
|
||||
import com.gh.gamecenter.plugin.transform.*
|
||||
import javassist.ClassPool
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.gradle.api.Project
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.JarOutputStream
|
||||
|
||||
class GhTransform(var project: Project) : Transform() {
|
||||
|
||||
private val mClassPool = ClassPool.getDefault()
|
||||
private val mTransformHelper = GhTransformHelper()
|
||||
private val mExcludePackages = listOf(
|
||||
"com/google/android/material/chip",
|
||||
"com/google/android/material/textfield",
|
||||
"com/google/android/material/navigation",
|
||||
"com/google/android/material/datepicker",
|
||||
"com/google/android/material/circularreveal",
|
||||
"com/google/android/material/timepicker",
|
||||
"com/google/android/material/divider",
|
||||
"com/google/android/material/slider",
|
||||
"com/google/android/material/card",
|
||||
"com/google/android/material/transformation",
|
||||
"com/google/android/material/snackbar",
|
||||
"com/google/android/material/switchmaterial",
|
||||
"com/google/android/material/bottomappbar",
|
||||
"com/google/android/material/behavior",
|
||||
"com/google/android/material/floatingactionbutton",
|
||||
)
|
||||
|
||||
override fun getName() = "GhTransform"
|
||||
|
||||
init {
|
||||
mTransformHelper.addTransformer(ExoSourceManagerTransformer())
|
||||
mTransformHelper.addTransformer(DiskLruCacheTransformer())
|
||||
mTransformHelper.addTransformer(RoomTransformer())
|
||||
mTransformHelper.addTransformer(ActivityTransformer())
|
||||
mTransformHelper.addTransformer(AppCompatEditTextTransformer())
|
||||
mTransformHelper.addTransformer(MiniGameWebViewTransformer())
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要处理的数据类型,目前 ContentType有六种枚举类型,通常我们使用比较频繁的有前两种:
|
||||
* 1、CONTENT_CLASS:表示需要处理 java 的 class 文件。
|
||||
* 2、CONTENT_JARS:表示需要处理 java 的 class 与 资源文件。
|
||||
* 3、CONTENT_RESOURCES:表示需要处理 java 的资源文件。
|
||||
* 4、CONTENT_NATIVE_LIBS:表示需要处理 native 库的代码。
|
||||
* 5、CONTENT_DEX:表示需要处理 DEX 文件。
|
||||
* 6、CONTENT_DEX_WITH_RESOURCES:表示需要处理 DEX 与 java 的资源文件。
|
||||
*/
|
||||
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
|
||||
return TransformManager.CONTENT_CLASS
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否增量编译
|
||||
*/
|
||||
// TODO Why not?
|
||||
override fun isIncremental() = false
|
||||
|
||||
/**
|
||||
* Transform 要操作的内容范围,目前 Scope 有五种基本类型:
|
||||
* 1、PROJECT:只有项目内容
|
||||
* 2、SUB_PROJECTS:只有子项目
|
||||
* 3、EXTERNAL_LIBRARIES:只有外部库
|
||||
* 4、TESTED_CODE:由当前变体(包括依赖项)所测试的代码
|
||||
* 5、PROVIDED_ONLY:只提供本地或远程依赖项
|
||||
* SCOPE_FULL_PROJECT 是一个Scope集合,包含Scope.PROJECT,Scope.SUB_PROJECTS,Scope.EXTERNAL_LIBRARIES 这三项,即当前Transform的作用域包括当前项目、子项目以及外部的依赖库
|
||||
*/
|
||||
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
|
||||
// 通常我们使用 SCOPE_FULL_PROJECT
|
||||
return TransformManager.SCOPE_FULL_PROJECT
|
||||
}
|
||||
|
||||
override fun transform(transformInvocation: TransformInvocation?) {
|
||||
super.transform(transformInvocation)
|
||||
|
||||
// 添加 android.jar path
|
||||
mClassPool?.appendClassPath((project.extensions.findByType(AppExtension::class.java)!!.bootClasspath[0].toString()))
|
||||
|
||||
val outputProvider = transformInvocation?.outputProvider
|
||||
outputProvider?.deleteAll()
|
||||
|
||||
transformInvocation?.inputs?.forEach { input ->
|
||||
input.directoryInputs.forEach { dirInput ->
|
||||
handleDirectory(dirInput.file)
|
||||
|
||||
// 将input的目录复制到output指定目录
|
||||
val dest = outputProvider?.getContentLocation(
|
||||
dirInput.name,
|
||||
dirInput.contentTypes,
|
||||
dirInput.scopes,
|
||||
Format.DIRECTORY
|
||||
)
|
||||
FileUtils.copyDirectory(dirInput.file, dest)
|
||||
}
|
||||
}
|
||||
|
||||
var index = 0
|
||||
transformInvocation?.inputs?.forEach { input ->
|
||||
input.jarInputs.forEach { jarInput ->
|
||||
if (jarInput.file.exists()) {
|
||||
val srcFile = handleJar(jarInput.file)
|
||||
// 必须给jar重新命名,否则会冲突
|
||||
var jarName = jarInput.name
|
||||
if (jarName.endsWith(".jar")) {
|
||||
jarName = jarName.substring(0, jarName.length - 4)
|
||||
}
|
||||
val dest = outputProvider?.getContentLocation(
|
||||
jarName + "_" + index,
|
||||
jarInput.contentTypes,
|
||||
jarInput.scopes,
|
||||
Format.JAR
|
||||
)
|
||||
FileUtils.copyFile(srcFile, dest)
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDirectory(dir: File) {
|
||||
// 将类路径添加到 classPool 中
|
||||
mClassPool.insertClassPath(dir.absolutePath)
|
||||
|
||||
if (dir.isDirectory) {
|
||||
dir.walkTopDown().forEach { file ->
|
||||
val filePath = file.absolutePath
|
||||
mClassPool.insertClassPath(filePath)
|
||||
if (shouldModify(filePath)) {
|
||||
val inputStream = FileInputStream(file)
|
||||
mTransformHelper.proceedModifyDir(filePath, inputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主要步骤:
|
||||
* 1.遍历所有jar文件
|
||||
* 2.解压jar然后遍历所有的class
|
||||
* 3.读取class的输入流并使用javassit修改,然后保存到新的jar文件中
|
||||
*/
|
||||
fun handleJar(jarFile: File): File {
|
||||
mClassPool.appendClassPath(jarFile.absolutePath)
|
||||
val inputJarFile = JarFile(jarFile)
|
||||
val entries = inputJarFile.entries()
|
||||
//创建一个新的文件
|
||||
val outputJarFile = File(jarFile.parentFile, "temp_" + jarFile.name)
|
||||
if (outputJarFile.exists()) outputJarFile.delete()
|
||||
val jarOutputStream = JarOutputStream(BufferedOutputStream(FileOutputStream(outputJarFile)))
|
||||
while (entries.hasMoreElements()) {
|
||||
val jarInputEntry = entries.nextElement()
|
||||
val jarInputEntryName = jarInputEntry.name
|
||||
var isIgnore = false
|
||||
for (i in mExcludePackages.indices){
|
||||
if (jarInputEntryName.contains(mExcludePackages[i])) {
|
||||
isIgnore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isIgnore) continue
|
||||
val outputJarEntry = JarEntry(jarInputEntryName)
|
||||
jarOutputStream.putNextEntry(outputJarEntry)
|
||||
|
||||
val inputStream = inputJarFile.getInputStream(jarInputEntry)
|
||||
if (!shouldModify(jarInputEntryName)) {
|
||||
jarOutputStream.write(IOUtils.toByteArray(inputStream))
|
||||
inputStream.close()
|
||||
continue
|
||||
}
|
||||
mTransformHelper.proceedModifyJar(jarInputEntryName, jarOutputStream, inputStream)
|
||||
}
|
||||
inputJarFile.close()
|
||||
jarOutputStream.closeEntry()
|
||||
jarOutputStream.flush()
|
||||
jarOutputStream.close()
|
||||
return outputJarFile
|
||||
}
|
||||
|
||||
private fun shouldModify(filePath: String): Boolean {
|
||||
return filePath.endsWith(".class")
|
||||
&& !filePath.contains("R.class")
|
||||
// && !filePath.contains("$")
|
||||
&& !filePath.contains("R$")
|
||||
&& !filePath.contains("BuildConfig.class")
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import com.gh.gamecenter.plugin.transform.Transformer
|
||||
import org.apache.commons.io.IOUtils
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.util.jar.JarOutputStream
|
||||
|
||||
class GhTransformHelper {
|
||||
|
||||
private val transformers = ArrayList<Transformer>()
|
||||
|
||||
fun addTransformer(transformer: Transformer) {
|
||||
transformers.add(transformer)
|
||||
}
|
||||
|
||||
fun proceedModifyDir(filePath: String, inputStream: InputStream) {
|
||||
transformers.forEach {
|
||||
val ctClass = it.modifyClass(filePath, inputStream)
|
||||
ctClass?.let {
|
||||
ctClass.writeFile()
|
||||
|
||||
// 释放内存
|
||||
ctClass.detach()
|
||||
}
|
||||
}
|
||||
inputStream.close()
|
||||
}
|
||||
|
||||
fun proceedModifyJar(filePath: String, jarOutputStream: JarOutputStream, iStream: InputStream) {
|
||||
var byteCode: ByteArray? = null
|
||||
var inputStream = iStream
|
||||
transformers.forEach {
|
||||
if (byteCode != null) {
|
||||
inputStream = ByteArrayInputStream(byteCode)
|
||||
}
|
||||
|
||||
val ctClass = it.modifyClass(filePath, inputStream)
|
||||
ctClass?.let {
|
||||
byteCode = ctClass.toBytecode()
|
||||
|
||||
// 释放内存
|
||||
ctClass.detach()
|
||||
}
|
||||
}
|
||||
|
||||
if (byteCode == null) {
|
||||
byteCode = IOUtils.toByteArray(inputStream)
|
||||
}
|
||||
|
||||
jarOutputStream.write(byteCode!!)
|
||||
jarOutputStream.flush()
|
||||
inputStream.close()
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,120 +0,0 @@
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import com.android.build.api.instrumentation.AsmClassVisitorFactory
|
||||
import com.android.build.api.instrumentation.ClassContext
|
||||
import com.android.build.api.instrumentation.ClassData
|
||||
import com.android.build.api.instrumentation.InstrumentationParameters
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.commons.AdviceAdapter
|
||||
|
||||
abstract class MiniGameWebViewVisitorFactory :
|
||||
AsmClassVisitorFactory<InstrumentationParameters.None> {
|
||||
|
||||
override fun createClassVisitor(
|
||||
classContext: ClassContext,
|
||||
nextClassVisitor: ClassVisitor,
|
||||
): ClassVisitor {
|
||||
val targetClass = classContext.currentClassData.className.replace('.', '/')
|
||||
return MiniGameWebViewVisitor(nextClassVisitor, targetClass)
|
||||
}
|
||||
|
||||
override fun isInstrumentable(classData: ClassData): Boolean {
|
||||
return classData.className.contains("QUAUtil")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MiniGameWebViewVisitor(nextVisitor: ClassVisitor, private val targetClass: String) :
|
||||
ClassVisitor(Opcodes.ASM9, nextVisitor) {
|
||||
|
||||
override fun visitMethod(
|
||||
access: Int,
|
||||
name: String?,
|
||||
descriptor: String?,
|
||||
signature: String?,
|
||||
exceptions: Array<out String>?,
|
||||
): MethodVisitor {
|
||||
val originalMv = super.visitMethod(access, name, descriptor, signature, exceptions)
|
||||
|
||||
// Only modify the run() method
|
||||
return if (name == "getSystemUA" && descriptor.equals("()Ljava/lang/String;")) {
|
||||
MiniGameWebViewAdviceAdapter(
|
||||
Opcodes.ASM9,
|
||||
originalMv,
|
||||
access,
|
||||
name,
|
||||
descriptor,
|
||||
targetClass
|
||||
)
|
||||
} else {
|
||||
originalMv
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MiniGameWebViewAdviceAdapter(
|
||||
api: Int,
|
||||
mv: MethodVisitor,
|
||||
access: Int,
|
||||
name: String,
|
||||
descriptor: String?,
|
||||
private val targetClass: String,
|
||||
) : AdviceAdapter(api, mv, access, name, descriptor) {
|
||||
|
||||
override fun onMethodEnter() {
|
||||
val l0 = newLabel()
|
||||
val l1 = newLabel()
|
||||
|
||||
// if (systemUA != null) {
|
||||
mv.visitFieldInsn(Opcodes.GETSTATIC, targetClass, "systemUA", "Ljava/lang/String;")
|
||||
mv.visitJumpInsn(Opcodes.IFNULL, l0)
|
||||
|
||||
// return systemUA;
|
||||
mv.visitFieldInsn(Opcodes.GETSTATIC, targetClass, "systemUA", "Ljava/lang/String;")
|
||||
mv.visitInsn(Opcodes.ARETURN)
|
||||
|
||||
mv.visitLabel(l0)
|
||||
|
||||
// String property = System.getProperty("http.agent");
|
||||
mv.visitLdcInsn("http.agent")
|
||||
mv.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
"java/lang/System",
|
||||
"getProperty",
|
||||
"(Ljava/lang/String;)Ljava/lang/String;",
|
||||
false
|
||||
)
|
||||
mv.visitVarInsn(Opcodes.ASTORE, 1) // Store the property in a local variable
|
||||
|
||||
// if (property == null) { return null; }
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 1)
|
||||
mv.visitJumpInsn(Opcodes.IFNULL, l1)
|
||||
|
||||
// systemUA = java.net.URLEncoder.encode(property, "UTF-8");
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 1)
|
||||
mv.visitLdcInsn("UTF-8")
|
||||
mv.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
"java/net/URLEncoder",
|
||||
"encode",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
|
||||
false
|
||||
)
|
||||
mv.visitFieldInsn(Opcodes.PUTSTATIC, targetClass, "systemUA", "Ljava/lang/String;")
|
||||
|
||||
// return systemUA;
|
||||
mv.visitFieldInsn(Opcodes.GETSTATIC, targetClass, "systemUA", "Ljava/lang/String;")
|
||||
mv.visitInsn(Opcodes.ARETURN)
|
||||
|
||||
mv.visitLabel(l1)
|
||||
mv.visitInsn(Opcodes.ACONST_NULL)
|
||||
mv.visitFieldInsn(Opcodes.PUTSTATIC, targetClass, "systemUA", "Ljava/lang/String;")
|
||||
|
||||
mv.visitInsn(Opcodes.ACONST_NULL)
|
||||
mv.visitInsn(Opcodes.ARETURN)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import com.android.build.api.instrumentation.AsmClassVisitorFactory
|
||||
import com.android.build.api.instrumentation.ClassContext
|
||||
import com.android.build.api.instrumentation.ClassData
|
||||
import com.android.build.api.instrumentation.InstrumentationParameters
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
|
||||
abstract class PluginAssetClassVisitorFactory:
|
||||
AsmClassVisitorFactory<InstrumentationParameters.None> {
|
||||
|
||||
override fun createClassVisitor(
|
||||
classContext: ClassContext,
|
||||
nextClassVisitor: ClassVisitor,
|
||||
): ClassVisitor {
|
||||
return PluginAssetClassVisitor(nextClassVisitor)
|
||||
}
|
||||
|
||||
override fun isInstrumentable(classData: ClassData): Boolean {
|
||||
return !classData.className.contains("PluginRedirectHelper")
|
||||
&& !classData.className.contains("BuildConfig")
|
||||
&& !classData.className.endsWith(".R")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PluginAssetClassVisitor(nextVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM9, nextVisitor) {
|
||||
|
||||
override fun visitMethod(
|
||||
access: Int,
|
||||
name: String?,
|
||||
descriptor: String?,
|
||||
signature: String?,
|
||||
exceptions: Array<out String>?,
|
||||
): MethodVisitor {
|
||||
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
|
||||
return AssetOpenMethodVisitor(mv)
|
||||
}
|
||||
|
||||
class AssetOpenMethodVisitor(mv: MethodVisitor) : MethodVisitor(Opcodes.ASM9, mv) {
|
||||
override fun visitMethodInsn(
|
||||
opcode: Int, owner: String, name: String,
|
||||
descriptor: String, isInterface: Boolean,
|
||||
) {
|
||||
|
||||
// Print all method call in AssetManager
|
||||
if ("android/content/res/AssetManager" == owner) {
|
||||
println("owener:$owner method:$name opcode:$opcode desc:$descriptor")
|
||||
}
|
||||
|
||||
// Replace AssetManager.open with com.gh.gamecenter.core.utils.PluginRedirectHelper.openAsset
|
||||
if ("android/content/res/AssetManager" == owner
|
||||
&& "open" == name
|
||||
&& opcode == Opcodes.INVOKEVIRTUAL
|
||||
&& "(Ljava/lang/String;)Ljava/io/InputStream;" == descriptor
|
||||
) {
|
||||
println("owener:$owner method:$name opcode:$opcode desc:$descriptor make it!")
|
||||
|
||||
return super.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
"com/gh/gamecenter/core/utils/PluginRedirectHelper",
|
||||
"openAsset",
|
||||
"(Landroid/content/res/AssetManager;Ljava/lang/String;)Ljava/io/InputStream;",
|
||||
isInterface
|
||||
)
|
||||
}
|
||||
|
||||
// Replace AssetManager.list with com.gh.gamecenter.core.utils.PluginRedirectHelper.listAsset
|
||||
if ("android/content/res/AssetManager" == owner
|
||||
&& "list" == name
|
||||
&& opcode == Opcodes.INVOKEVIRTUAL
|
||||
&& "(Ljava/lang/String;)[Ljava/lang/String;" == descriptor
|
||||
) {
|
||||
|
||||
return super.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
"com/gh/gamecenter/core/utils/PluginRedirectHelper",
|
||||
"listAsset",
|
||||
"(Landroid/content/res/AssetManager;Ljava/lang/String;)[Ljava/lang/String;",
|
||||
isInterface
|
||||
)
|
||||
}
|
||||
return super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,201 @@
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import com.android.build.api.transform.Format
|
||||
import com.android.build.api.transform.Transform
|
||||
import com.android.build.api.transform.TransformInvocation
|
||||
import com.android.build.gradle.internal.pipeline.TransformManager
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
|
||||
class PluginAssetTransform : Transform() {
|
||||
|
||||
override fun getName() = "PluginTransform"
|
||||
|
||||
override fun getInputTypes() = TransformManager.CONTENT_CLASS
|
||||
|
||||
override fun getScopes() = TransformManager.SCOPE_FULL_PROJECT
|
||||
|
||||
override fun isIncremental() = false
|
||||
|
||||
override fun transform(transformInvocation: TransformInvocation) {
|
||||
if (!isIncremental) {
|
||||
transformInvocation.outputProvider.deleteAll()
|
||||
}
|
||||
|
||||
var index = 0
|
||||
|
||||
transformInvocation.inputs.forEach { input ->
|
||||
// 处理 dir 部分
|
||||
input.directoryInputs.forEach { directoryInput ->
|
||||
val outputDir = transformInvocation.outputProvider
|
||||
.getContentLocation(
|
||||
directoryInput.name,
|
||||
directoryInput.contentTypes,
|
||||
directoryInput.scopes,
|
||||
Format.DIRECTORY
|
||||
)
|
||||
|
||||
FileUtils.copyDirectory(directoryInput.file, outputDir)
|
||||
|
||||
directoryInput.file.walkTopDown().forEach { file ->
|
||||
if (file.isFile && file.extension == "class") {
|
||||
val classReader = ClassReader(FileInputStream(file))
|
||||
val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
|
||||
val classVisitor = AssetOpenClassVisitor(classWriter)
|
||||
|
||||
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
|
||||
|
||||
val fos = FileOutputStream(File(outputDir, file.relativeTo(directoryInput.file).path))
|
||||
fos.write(classWriter.toByteArray())
|
||||
fos.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 jar 部分
|
||||
input.jarInputs.forEach { jarInput ->
|
||||
val jarFile = jarInput.file
|
||||
if (jarFile.exists() && jarFile.extension == "jar") {
|
||||
val modifiedJarFile = modifyJar(jarFile)
|
||||
// 必须给 jar 重新命名,否则会冲突
|
||||
var jarName = jarFile.name
|
||||
if (jarName.endsWith(".jar")) {
|
||||
jarName = jarName.substring(0, jarName.length - 4)
|
||||
}
|
||||
val dest = transformInvocation.outputProvider?.getContentLocation(
|
||||
jarName + "_" + index,
|
||||
jarInput.contentTypes,
|
||||
jarInput.scopes,
|
||||
Format.JAR
|
||||
)
|
||||
FileUtils.copyFile(modifiedJarFile, dest)
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主要步骤:
|
||||
* 1.遍历所有jar文件
|
||||
* 2.解压jar然后遍历所有的class
|
||||
* 3.读取class的输入流并使用 ASM 修改,然后保存到新的 jar 文件中
|
||||
*/
|
||||
fun modifyJar(jarFile: File): File {
|
||||
val jarInput = JarFile(jarFile)
|
||||
val jarInputEntries = jarInput.entries()
|
||||
|
||||
// 创建一个新的文件
|
||||
val jarOutput = File(jarFile.parentFile, "temp_" + jarFile.name)
|
||||
if (jarOutput.exists()) jarOutput.delete()
|
||||
|
||||
val jarOutputStream = JarOutputStream(BufferedOutputStream(FileOutputStream(jarOutput)))
|
||||
|
||||
while (jarInputEntries.hasMoreElements()) {
|
||||
val jarInputEntry = jarInputEntries.nextElement()
|
||||
val jarInputEntryName = jarInputEntry.name
|
||||
|
||||
val jarInputStream = jarInput.getInputStream(jarInputEntry)
|
||||
|
||||
val zipEntry = ZipEntry(jarInputEntryName)
|
||||
|
||||
jarOutputStream.putNextEntry(zipEntry)
|
||||
|
||||
if (shouldModify(jarInputEntryName)) {
|
||||
val classReader = ClassReader(IOUtils.toByteArray(jarInputStream))
|
||||
val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
|
||||
val classVisitor = AssetOpenClassVisitor(classWriter)
|
||||
|
||||
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
|
||||
|
||||
jarOutputStream.write(classWriter.toByteArray())
|
||||
} else {
|
||||
jarOutputStream.write(IOUtils.toByteArray(jarInputStream))
|
||||
}
|
||||
|
||||
jarOutputStream.closeEntry()
|
||||
|
||||
jarInputStream.close()
|
||||
}
|
||||
jarInput.close()
|
||||
jarOutputStream.flush()
|
||||
jarOutputStream.close()
|
||||
return jarOutput
|
||||
}
|
||||
|
||||
private fun shouldModify(filePath: String): Boolean {
|
||||
return filePath.endsWith(".class")
|
||||
&& !filePath.contains("PluginRedirectHelper.class")
|
||||
&& !filePath.contains("R.class")
|
||||
&& !filePath.contains("R$")
|
||||
&& !filePath.contains("BuildConfig.class")
|
||||
}
|
||||
|
||||
class AssetOpenClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.ASM9, cv) {
|
||||
override fun visitMethod(
|
||||
access: Int, name: String, descriptor: String,
|
||||
signature: String?, exceptions: Array<String>?
|
||||
): MethodVisitor {
|
||||
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
|
||||
return AssetOpenMethodVisitor(mv)
|
||||
}
|
||||
}
|
||||
|
||||
class AssetOpenMethodVisitor(mv: MethodVisitor) : MethodVisitor(Opcodes.ASM9, mv) {
|
||||
override fun visitMethodInsn(
|
||||
opcode: Int, owner: String, name: String,
|
||||
descriptor: String, isInterface: Boolean
|
||||
) {
|
||||
|
||||
// Print all method call in AssetManager
|
||||
if ("android/content/res/AssetManager" == owner) {
|
||||
println("owener:$owner method:$name opcode:$opcode desc:$descriptor")
|
||||
}
|
||||
|
||||
// Replace AssetManager.open with com.gh.gamecenter.core.utils.PluginRedirectHelper.openAsset
|
||||
if ("android/content/res/AssetManager" == owner
|
||||
&& "open" == name
|
||||
&& opcode == Opcodes.INVOKEVIRTUAL
|
||||
&& "(Ljava/lang/String;)Ljava/io/InputStream;" == descriptor) {
|
||||
|
||||
println("owener:$owner method:$name opcode:$opcode desc:$descriptor make it!")
|
||||
|
||||
return super.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
"com/gh/gamecenter/core/utils/PluginRedirectHelper",
|
||||
"openAsset",
|
||||
"(Landroid/content/res/AssetManager;Ljava/lang/String;)Ljava/io/InputStream;",
|
||||
isInterface
|
||||
)
|
||||
}
|
||||
|
||||
// Replace AssetManager.list with com.gh.gamecenter.core.utils.PluginRedirectHelper.listAsset
|
||||
if ("android/content/res/AssetManager" == owner
|
||||
&& "list" == name
|
||||
&& opcode == Opcodes.INVOKEVIRTUAL
|
||||
&& "(Ljava/lang/String;)[Ljava/lang/String;" == descriptor) {
|
||||
|
||||
return super.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
"com/gh/gamecenter/core/utils/PluginRedirectHelper",
|
||||
"listAsset",
|
||||
"(Landroid/content/res/AssetManager;Ljava/lang/String;)[Ljava/lang/String;",
|
||||
isInterface)
|
||||
}
|
||||
return super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import com.android.build.api.instrumentation.AsmClassVisitorFactory
|
||||
import com.android.build.api.instrumentation.ClassContext
|
||||
import com.android.build.api.instrumentation.ClassData
|
||||
import com.android.build.api.instrumentation.InstrumentationParameters
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.commons.AdviceAdapter
|
||||
|
||||
abstract class RoomClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
|
||||
|
||||
override fun createClassVisitor(
|
||||
classContext: ClassContext,
|
||||
nextClassVisitor: ClassVisitor,
|
||||
): ClassVisitor {
|
||||
return RoomClassVisitor(nextClassVisitor)
|
||||
}
|
||||
|
||||
override fun isInstrumentable(classData: ClassData): Boolean {
|
||||
return classData.className.contains("androidx.room.InvalidationTracker")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RoomClassVisitor(nextVisitor: ClassVisitor) :
|
||||
ClassVisitor(Opcodes.ASM9, nextVisitor) {
|
||||
|
||||
override fun visitMethod(
|
||||
access: Int,
|
||||
name: String?,
|
||||
descriptor: String?,
|
||||
signature: String?,
|
||||
exceptions: Array<out String>?,
|
||||
): MethodVisitor {
|
||||
val originalMv = super.visitMethod(access, name, descriptor, signature, exceptions)
|
||||
|
||||
// Only modify the run() method
|
||||
return if (name == "run" && descriptor == "()V") {
|
||||
println("RoomClassVisitor found run method on $name")
|
||||
TryCatchMethodVisitor(Opcodes.ASM9, originalMv, access, name, descriptor)
|
||||
} else {
|
||||
originalMv
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
package com.gh.gamecenter.plugin
|
||||
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.commons.AdviceAdapter
|
||||
|
||||
/**
|
||||
* A [MethodVisitor] that adds a try-catch block around the original method's code.
|
||||
* @param returnBoolean, if true, the catch block will return a default boolean value instead of void
|
||||
*/
|
||||
class TryCatchMethodVisitor(
|
||||
api: Int,
|
||||
mv: MethodVisitor,
|
||||
access: Int,
|
||||
name: String?,
|
||||
descriptor: String?,
|
||||
private val returnBoolean: Boolean = false,
|
||||
) : AdviceAdapter(api, mv, access, name, descriptor) {
|
||||
|
||||
private val startLabel = org.objectweb.asm.Label()
|
||||
private val endLabel = org.objectweb.asm.Label()
|
||||
private val handlerLabel = org.objectweb.asm.Label()
|
||||
|
||||
override fun onMethodEnter() {
|
||||
// Start of the try block
|
||||
mv.visitLabel(startLabel)
|
||||
}
|
||||
|
||||
override fun onMethodExit(opcode: Int) {
|
||||
// End of try block. Only put it here if the opcode is not ATHROW
|
||||
if (opcode != Opcodes.ATHROW) {
|
||||
mv.visitLabel(endLabel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitMaxs(maxStack: Int, maxLocals: Int) {
|
||||
// Exception handler (placed after the original method's code)
|
||||
mv.visitLabel(handlerLabel)
|
||||
|
||||
// Store the exception in a local variable (slot 1)
|
||||
mv.visitVarInsn(Opcodes.ASTORE, 1)
|
||||
|
||||
// Print the stack trace of the exception
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 1)
|
||||
mv.visitMethodInsn(
|
||||
Opcodes.INVOKEVIRTUAL,
|
||||
"java/lang/Throwable",
|
||||
"printStackTrace",
|
||||
"()V",
|
||||
false
|
||||
)
|
||||
|
||||
if (!returnBoolean) {
|
||||
mv.visitInsn(Opcodes.RETURN)
|
||||
} else {
|
||||
mv.visitInsn(Opcodes.ICONST_0)
|
||||
mv.visitInsn(Opcodes.IRETURN)
|
||||
}
|
||||
|
||||
// Add the try-catch block to the method
|
||||
// Catch all Throwable exceptions
|
||||
mv.visitTryCatchBlock(
|
||||
startLabel,
|
||||
endLabel,
|
||||
handlerLabel,
|
||||
"java/lang/Throwable"
|
||||
)
|
||||
|
||||
// Adjust maxStack and maxLocals. The exact values might need fine-tuning
|
||||
// depending on the original method's complexity. We've added a few
|
||||
// instructions, so we need to increase these values.
|
||||
super.visitMaxs(maxStack + 2, maxLocals + 1)
|
||||
}
|
||||
}
|
||||
@ -118,14 +118,14 @@ ext {
|
||||
sentry = "6.20.0"
|
||||
|
||||
autoServiceVersion = "1.0-rc7"
|
||||
routerVersion = "1.2.4"
|
||||
routerVersion = "1.2.2"
|
||||
|
||||
composeVersion = "1.2.1"
|
||||
activityComposeVersion = "1.6.0"
|
||||
composeCompilerVersion = "1.5.14"
|
||||
constraintlayoutCompose = "1.0.1"
|
||||
|
||||
sensorsDataVersion = "6.8.4"
|
||||
sensorsDataVersion = "6.8.0"
|
||||
|
||||
documentfile = "1.0.1"
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter.accelerator">
|
||||
|
||||
<application>
|
||||
|
||||
|
||||
1
feature/cloud_game/.gitignore
vendored
Normal file
1
feature/cloud_game/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
||||
40
feature/cloud_game/build.gradle
Normal file
40
feature/cloud_game/build.gradle
Normal file
@ -0,0 +1,40 @@
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "kotlin-kapt"
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdk rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode rootProject.ext.versionCode
|
||||
versionName rootProject.ext.versionName
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(path: ":module_common")) {
|
||||
exclude group: 'androidx.swiperefreshlayout'
|
||||
}
|
||||
implementation(project(':module_core_feature')) {
|
||||
exclude group: 'androidx.swiperefreshlayout'
|
||||
}
|
||||
|
||||
implementation "com.tencent.tcr:tcrsdk-full:3.23.0"
|
||||
implementation "com.tencent.tcr:tcr-gamepad:2.2.4"
|
||||
implementation "com.lg:easyfloat:2.0.4-fix_proguard"
|
||||
}
|
||||
21
feature/cloud_game/proguard-rules.pro
vendored
Normal file
21
feature/cloud_game/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
10
feature/cloud_game/src/main/AndroidManifest.xml
Normal file
10
feature/cloud_game/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter.cloudgame">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.feature.cloudgame.CloudGameActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
</application>
|
||||
</manifest>
|
||||
1
feature/cloud_game/src/main/assets/lol_5v5.cfg
Normal file
1
feature/cloud_game/src/main/assets/lol_5v5.cfg
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,500 @@
|
||||
package com.gh.gamecenter.feature.cloudgame
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import com.gh.gamecenter.cloudgame.R
|
||||
import com.gh.gamecenter.cloudgame.databinding.ActivityCloudgameBinding
|
||||
import com.gh.gamecenter.core.AppExecutor
|
||||
import com.gh.gamecenter.core.AppExecutor.ioExecutor
|
||||
import com.gh.gamecenter.feature.cloudgame.OkHelper.obtainServerSession
|
||||
import com.tencent.tcr.sdk.api.AsyncCallback
|
||||
import com.tencent.tcr.sdk.api.CustomDataChannel
|
||||
import com.tencent.tcr.sdk.api.TcrSdk
|
||||
import com.tencent.tcr.sdk.api.TcrSession
|
||||
import com.tencent.tcr.sdk.api.TcrSessionConfig
|
||||
import com.tencent.tcr.sdk.api.data.CursorState
|
||||
import com.tencent.tcr.sdk.api.data.ScreenConfig
|
||||
import com.tencent.tcr.sdk.api.data.StatsInfo
|
||||
import com.tencent.tcr.sdk.api.data.VideoStreamConfig
|
||||
import com.tencent.tcr.sdk.api.view.MobileTouchListener
|
||||
import com.tencent.tcr.sdk.api.view.PcClickListener
|
||||
import com.tencent.tcr.sdk.api.view.PcTouchListener
|
||||
import com.tencent.tcr.sdk.api.view.TcrRenderView
|
||||
import com.tencent.tcr.sdk.api.view.TcrRenderView.TcrRenderViewType
|
||||
import com.tencent.tcr.sdk.api.view.TcrRenderView.VideoRotation
|
||||
import com.tencent.tcrgamepad.GamepadManager
|
||||
import com.tencent.tcrgamepad.GamepadManager.OnEditListener
|
||||
import com.tencent.tcrgui.keyboard.KeyboardView
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.DecimalFormat
|
||||
|
||||
/**
|
||||
* 该类演示了如何初始化TcrSdk,创建会话、请求远端会话、启动云应用,并将云应用画面展示到界面上的基础流程。<br></br>
|
||||
*
|
||||
* 要使用TcrSdk,你需要先调用[TcrSdk.init]接口初始化TcrSdk,<br></br>
|
||||
* 在[AsyncCallback.onSuccess] 回调以后才能做进一步操作,例如创建[TcrSession]以及[TcrRenderView]。
|
||||
*
|
||||
* 在启动会话[TcrSession.start]后才可以与云端实例进行交互。<br></br>
|
||||
* 具体的流程如下:
|
||||
*
|
||||
* <pre>
|
||||
* `┌──────────────┐ ┌────────────┐ ┌────────────┐ ┌───────────┐
|
||||
* │ MainActivity │ │ TcrSession │ │ App Server │ │ Cloud Api │
|
||||
* └──────┬───────┘ └──────┬─────┘ └──────┬─────┘ └─────┬─────┘
|
||||
* │ setObserver() │ │ │
|
||||
* ├────────────────►│ │ │
|
||||
* │ ├─────┐ │ │
|
||||
* │ │ │ │ │
|
||||
* │ Event.INITED │◄────┘ │ │
|
||||
* │◄────────────────┤ │ │
|
||||
* │ │ │ │
|
||||
* │ startGame(clientSession) │ │
|
||||
* ├─────────────────┬─────────────►│ │
|
||||
* │ │ │ tryLock │
|
||||
* │ │ ├─────────────►│
|
||||
* │ │ │ │
|
||||
* │ │ │createSession │
|
||||
* │ │ ├─────────────►│
|
||||
* │ │ │ │
|
||||
* │ onSuccess(serverSession) │ │
|
||||
* │◄────────────────┬──────────────┤ │
|
||||
* │ │ │ │
|
||||
* │ start() │ │ │
|
||||
* ├────────────────►│ │ │
|
||||
* │ │ │ │
|
||||
* SDK调用流程 后台交互流程
|
||||
` *
|
||||
</pre> *
|
||||
*
|
||||
* 1.调用[TcrSdk.init]初始化SDK,初始化成功后创建TcrSession,
|
||||
* 并通过[TcrSessionConfig.Builder.observer]将Observer设置好,通过Observer的回调拿到TcrSession对外的通知<br></br>
|
||||
* 2.通过Observer的回调得到TcrSession初始化成功[TcrSession.Event.STATE_INITED]的事件后,将事件传递的数据解析为clientSession<br></br>
|
||||
* 3.调用业务后台接口, 将`clientSession`传递给云端实例,并获取`serverSession`。<br></br>
|
||||
* 4.拿到`serverSession`之后,调用[TcrSession.start]启动会话。<br></br>
|
||||
* 5.当会话启动成功之后用户便可以和云端实例进行交互。<br></br>
|
||||
*
|
||||
* 详细的TcrSdk接口如何使用,请参考文档<br></br>
|
||||
* 业务后台的搭建,请参考链接<br></br>
|
||||
* @see [TcrSdK API](https://tencentyun.github.io/cloudgame-android-sdk/tcrsdk/index.html)
|
||||
*
|
||||
* @see [搭建业务后台](https://github.com/tencentyun/gs-server-demo)
|
||||
*/
|
||||
class CloudGameActivity : Activity() {
|
||||
private val mDf = DecimalFormat("#.##")
|
||||
|
||||
// 渲染视图
|
||||
private var mRenderView: TcrRenderView? = null
|
||||
|
||||
// 云渲染会话
|
||||
private var mTcrSession: TcrSession? = null
|
||||
|
||||
// 创建的数据通道
|
||||
private var mCustomDataChannel: CustomDataChannel? = null
|
||||
|
||||
// 云端横竖屏信息
|
||||
private var mScreenConfig: ScreenConfig? = null
|
||||
|
||||
// 视频流分辨率信息
|
||||
private var mVideoStreamConfig: VideoStreamConfig? = null
|
||||
|
||||
// 记录云端屏幕配置是否发生变化
|
||||
private var mScreenConfigChanged = false
|
||||
|
||||
// 记录视频分辨率是否发生变化
|
||||
private var mVideoStreamConfigChanged = false
|
||||
|
||||
// 游戏手柄管理器
|
||||
private var gamepadManager: GamepadManager? = null
|
||||
|
||||
// 键盘管理器
|
||||
private var keyboardView: KeyboardView? = null
|
||||
|
||||
private var isCursorEnabled = true
|
||||
|
||||
private val binding by lazy { ActivityCloudgameBinding.inflate(layoutInflater) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
initWindow()
|
||||
setContentView(binding.root)
|
||||
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initWindow() {
|
||||
// 不显示标题栏
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
// 全屏展示
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
// 屏幕常亮
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.run {
|
||||
joystickBtn.setOnClickListener {
|
||||
if (joystickContainer.isVisible) {
|
||||
joystickContainer.visibility = View.GONE
|
||||
} else {
|
||||
if (mTcrSession == null) return@setOnClickListener
|
||||
|
||||
if (gamepadManager == null) {
|
||||
gamepadManager = GamepadManager(this@CloudGameActivity, mTcrSession)
|
||||
joystickContainer.addView(gamepadManager)
|
||||
|
||||
val customGamePadCfg =
|
||||
readConfigFile(this@CloudGameActivity, "lol_5v5.cfg")
|
||||
gamepadManager?.showGamepad(customGamePadCfg)
|
||||
gamepadManager?.setEditListener(OnEditListener { isChanged: Boolean, newCfg: String ->
|
||||
if (isChanged) {
|
||||
gamepadManager?.showGamepad(newCfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
joystickContainer.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
joystickEditBtn.setOnClickListener {
|
||||
if (mTcrSession == null || gamepadManager == null) return@setOnClickListener
|
||||
|
||||
val customGamePadCfg =
|
||||
readConfigFile(this@CloudGameActivity, "lol_5v5.cfg")
|
||||
gamepadManager?.editGamepad(customGamePadCfg)
|
||||
}
|
||||
|
||||
keyboardBtn.setOnClickListener {
|
||||
if (keyboardContainer.isVisible) {
|
||||
keyboardContainer.visibility = View.GONE
|
||||
} else {
|
||||
if (mTcrSession == null) return@setOnClickListener
|
||||
|
||||
if (keyboardView == null) {
|
||||
keyboardView = KeyboardView(this@CloudGameActivity, mTcrSession)
|
||||
binding.keyboardContainer.addView(keyboardView)
|
||||
}
|
||||
|
||||
keyboardContainer.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
cursorBtn.setOnClickListener {
|
||||
if (mTcrSession == null) return@setOnClickListener
|
||||
|
||||
if (isCursorEnabled) {
|
||||
mRenderView?.setOnTouchListener(null)
|
||||
} else {
|
||||
setTouchHandler(
|
||||
mTcrSession!!,
|
||||
mRenderView!!,
|
||||
PC_GAME
|
||||
)
|
||||
}
|
||||
|
||||
isCursorEnabled = !isCursorEnabled
|
||||
}
|
||||
|
||||
connectBtn.setOnClickListener {
|
||||
// 初始化TcrSdk,初始化成功后创建TcrSession
|
||||
TcrSdk.getInstance().init(this@CloudGameActivity, null, object : AsyncCallback<Void?> {
|
||||
override fun onSuccess(result: Void?) {
|
||||
Log.i(TAG, "init SDK success")
|
||||
showToast("sdk 初始化成功", Toast.LENGTH_SHORT)
|
||||
// 为TcrSession创建配置参数对象。参考https://tencentyun.github.io/cloudgame-android-sdk/tcrsdk/com/tencent/tcr/sdk/api/config/TcrSessionConfig.Builder.html
|
||||
val tcrSessionConfig = TcrSessionConfig.builder()
|
||||
.observer(mSessionEventObserver)
|
||||
.idleThreshold(30000)
|
||||
.build()
|
||||
// 创建会话对象
|
||||
mTcrSession = TcrSdk.getInstance().createTcrSession(tcrSessionConfig)
|
||||
if (mTcrSession == null) {
|
||||
Log.e(TAG, "mTcrSession = null")
|
||||
showToast("创建TcrSession失败,请查看日志", Toast.LENGTH_SHORT)
|
||||
return
|
||||
}
|
||||
// 创建和初始化渲染视图
|
||||
runOnUiThread { initTcrRenderView() }
|
||||
}
|
||||
|
||||
override fun onFailure(code: Int, msg: String) {
|
||||
val errorMsg = "init SDK failed:$code msg:$msg"
|
||||
Log.e(TAG, errorMsg)
|
||||
showToast(errorMsg, Toast.LENGTH_LONG)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
disconnectBtn.setOnClickListener {
|
||||
AppExecutor.ioExecutor.execute {
|
||||
OkHelper.stopServerSession("rbmk")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化渲染视图
|
||||
*/
|
||||
private fun initTcrRenderView() {
|
||||
// 创建渲染视图
|
||||
mRenderView = TcrSdk.getInstance()
|
||||
.createTcrRenderView(this@CloudGameActivity, mTcrSession!!, TcrRenderViewType.SURFACE)
|
||||
if (mRenderView == null) {
|
||||
Log.e(TAG, "mRenderView = null")
|
||||
showToast("创建TcrRenderView失败,请查看日志", Toast.LENGTH_SHORT)
|
||||
return
|
||||
}
|
||||
// 将渲染视图添加到界面上
|
||||
(findViewById<View>(R.id.render_view_parent) as FrameLayout).addView(mRenderView)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过http请求业务后台并获取ServerSession,拿到ServerSession后启动会话<br></br>
|
||||
* 如果您需要启动云应用请调用CloudRenderBiz.getInstance().startProject()<br></br>
|
||||
* 如果您需要启动云游戏请调用CloudRenderBiz.getInstance().startGame()<br></br>
|
||||
* <br></br>
|
||||
* 无论启动的是云应用还是云游戏,都需要接入方准备相应的业务后台环境。<br></br>
|
||||
* 云渲染团队提供了一个测试环境,可通过[TcrTestEnv]工具进行调用,方便您做客户端SDK接入测试。<br></br>
|
||||
*/
|
||||
private fun requestServerSession(clientSession: String) {
|
||||
Log.i(TAG, "init session success:$clientSession")
|
||||
|
||||
ioExecutor.execute {
|
||||
val session = obtainServerSession(clientSession, "rbmk")
|
||||
if (!TextUtils.isEmpty(session)) {
|
||||
mTcrSession!!.start(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转屏幕方向, 以便本地的屏幕方向和云端保持一致<br></br>
|
||||
* 注意: 请确保Manifest中的Activity有android:configChanges="orientation|screenSize"配置, 避免Activity因旋转而被销毁.<br></br>
|
||||
*/
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
private fun updateOrientation() {
|
||||
Log.i(TAG, "updateOrientation:" + mScreenConfig!!.orientation)
|
||||
if (mScreenConfig!!.orientation == "portrait") {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
} else if (mScreenConfig!!.orientation == "landscape") {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据云端屏幕配置旋转视频画面
|
||||
*/
|
||||
private fun updateRotation() {
|
||||
if (!mScreenConfigChanged || !mVideoStreamConfigChanged) {
|
||||
Log.w(
|
||||
TAG, ("updateRotation failed,mScreenConfigChanged=" + mScreenConfigChanged
|
||||
+ " mVideoStreamConfigChanged=" + mScreenConfigChanged)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (mVideoStreamConfig!!.width > mVideoStreamConfig!!.height) {
|
||||
if (mScreenConfig!!.orientation == "portrait") {
|
||||
mRenderView!!.setVideoRotation(VideoRotation.ROTATION_90)
|
||||
} else {
|
||||
mRenderView!!.setVideoRotation(VideoRotation.ROTATION_0)
|
||||
}
|
||||
} else {
|
||||
if (mScreenConfig!!.orientation == "landscape") {
|
||||
mRenderView!!.setVideoRotation(VideoRotation.ROTATION_270)
|
||||
} else {
|
||||
mRenderView!!.setVideoRotation(VideoRotation.ROTATION_0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为不同的云端实例设置处理器
|
||||
*
|
||||
*
|
||||
* 对于云端PC应用可以使用[PcTouchListener]
|
||||
* (云端PC应用在支持windows多点触摸时可使用[MobileTouchListener])
|
||||
*
|
||||
* 对于手游要使用[MobileTouchListener]
|
||||
*/
|
||||
private fun setTouchHandler(session: TcrSession, renderView: TcrRenderView, gameType: Int) {
|
||||
when (gameType) {
|
||||
MOBILE_GAME -> renderView.setOnTouchListener(MobileTouchListener(session))
|
||||
PC_GAME -> {
|
||||
val pcTouchListener = PcTouchListener(session)
|
||||
pcTouchListener.zoomHandler.setZoomRatio(1f, 5f)
|
||||
renderView.setOnTouchListener(pcTouchListener)
|
||||
pcTouchListener.setShortClickListener(PcClickListener(session))
|
||||
}
|
||||
|
||||
else -> Log.e(TAG, "UNKNOWN DeviceMode!!")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showToast(msg: String, duration: Int) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this@CloudGameActivity, msg, duration)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 观察TcrSession通知出的各类事件,处理各类事件通知的消息和数据
|
||||
*
|
||||
* @see TcrSession.Event
|
||||
*/
|
||||
private val mSessionEventObserver = TcrSession.Observer { event, eventData ->
|
||||
when (event) {
|
||||
TcrSession.Event.STATE_INITED -> {
|
||||
// 回调数据中拿到client session并请求ServerSession
|
||||
val clientSession = eventData as String
|
||||
requestServerSession(clientSession)
|
||||
}
|
||||
|
||||
TcrSession.Event.STATE_CONNECTED -> {
|
||||
// 连接成功后设置操作模式
|
||||
// 与云端的交互需在此事件回调后开始调用接口
|
||||
runOnUiThread {
|
||||
setTouchHandler(
|
||||
mTcrSession!!,
|
||||
mRenderView!!, PC_GAME
|
||||
)
|
||||
}
|
||||
createCustomDataChannel()
|
||||
}
|
||||
|
||||
TcrSession.Event.STATE_RECONNECTING -> showToast("重连中...", Toast.LENGTH_LONG)
|
||||
TcrSession.Event.STATE_CLOSED -> {
|
||||
showToast("会话关闭", Toast.LENGTH_SHORT)
|
||||
finish()
|
||||
}
|
||||
|
||||
TcrSession.Event.SCREEN_CONFIG_CHANGE -> {
|
||||
mScreenConfig = eventData as ScreenConfig
|
||||
updateOrientation()
|
||||
mScreenConfigChanged = true
|
||||
updateRotation()
|
||||
}
|
||||
|
||||
TcrSession.Event.VIDEO_STREAM_CONFIG_CHANGED -> {
|
||||
mVideoStreamConfig = eventData as VideoStreamConfig
|
||||
mVideoStreamConfigChanged = true
|
||||
updateRotation()
|
||||
}
|
||||
|
||||
TcrSession.Event.CLIENT_STATS -> {
|
||||
val statsInfo = eventData as StatsInfo
|
||||
runOnUiThread {
|
||||
binding.statsValue.text =
|
||||
(" fps: " + statsInfo.fps + " bitrate: " + mDf.format(
|
||||
statsInfo.bitrate / 1024.0 / 1024.0
|
||||
)
|
||||
+ "Mb/s rtt: " + statsInfo.rtt + "ms")
|
||||
}
|
||||
}
|
||||
|
||||
TcrSession.Event.CURSOR_STATE_CHANGE -> {
|
||||
val cursorState = eventData as CursorState
|
||||
Log.i(
|
||||
TAG,
|
||||
"cursor showing state changed, $cursorState"
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义数据通道
|
||||
*/
|
||||
private fun createCustomDataChannel() {
|
||||
if (mTcrSession == null) {
|
||||
return
|
||||
}
|
||||
// 10000为数据通道端口,请替换为你们自己业务的端口
|
||||
mCustomDataChannel =
|
||||
mTcrSession!!.createCustomDataChannel(10000, object : CustomDataChannel.Observer {
|
||||
override fun onConnected(port: Int) {
|
||||
val msg = "Your message"
|
||||
mCustomDataChannel!!.send(ByteBuffer.wrap(msg.toByteArray(StandardCharsets.UTF_8)))
|
||||
Log.i(
|
||||
TAG,
|
||||
"onConnected() send data to port $port: $msg"
|
||||
)
|
||||
}
|
||||
|
||||
override fun onError(port: Int, code: Int, msg: String) {
|
||||
Log.e(TAG, "onError() $port msg:$msg")
|
||||
}
|
||||
|
||||
override fun onMessage(port: Int, data: ByteBuffer) {
|
||||
Log.i(
|
||||
TAG,
|
||||
"onMessage() port=" + port + " data=" + StandardCharsets.UTF_8.decode(data)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (mTcrSession != null) return
|
||||
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (mTcrSession != null) {
|
||||
mTcrSession!!.release()
|
||||
}
|
||||
if (mRenderView != null) {
|
||||
mRenderView!!.release()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun readConfigFile(context: Context, fileName: String): String? {
|
||||
try {
|
||||
val am = context.assets
|
||||
val `is` = am.open(fileName)
|
||||
val isr = InputStreamReader(`is`, StandardCharsets.UTF_8)
|
||||
val input = CharArray(`is`.available())
|
||||
isr.read(input)
|
||||
isr.close()
|
||||
`is`.close()
|
||||
return String(input)
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"readConfigFile failed:$e"
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CloudGameActivity"
|
||||
|
||||
private const val MOBILE_GAME = 1
|
||||
private const val PC_GAME = 2
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.gh.gamecenter.feature.cloudgame
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.Gravity
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.gh.gamecenter.cloudgame.R
|
||||
import com.gh.gamecenter.common.utils.PackageFlavorHelper
|
||||
import com.gh.gamecenter.common.utils.dip2px
|
||||
import com.lzf.easyfloat.EasyFloat
|
||||
import com.lzf.easyfloat.enums.ShowPattern
|
||||
import com.lzf.easyfloat.enums.SidePattern
|
||||
|
||||
object CloudGameHelper {
|
||||
|
||||
fun showCloudGameFloat(activity: AppCompatActivity) {
|
||||
if (PackageFlavorHelper.IS_TEST_FLAVOR) {
|
||||
EasyFloat.with(activity)
|
||||
.setLayout(R.layout.layout_cloud_window)
|
||||
.setTag("cloud_game_float")
|
||||
.setAnimator(null)
|
||||
.setGravity(Gravity.TOP.xor(Gravity.END), 0, 250F.dip2px())
|
||||
.setSidePattern(SidePattern.RESULT_SIDE)
|
||||
.setDragEnable(true)
|
||||
.setShowPattern(ShowPattern.CURRENT_ACTIVITY)
|
||||
.registerCallback {
|
||||
createResult { _, _, view ->
|
||||
val tv = view?.findViewById<TextView>(R.id.iconTv)
|
||||
tv?.text = "☁\uFE0F\uD83C\uDFAE"
|
||||
view?.setOnClickListener {
|
||||
// Handle click event
|
||||
activity.startActivity(Intent(activity, CloudGameActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.gh.gamecenter.feature.cloudgame
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import com.gh.gamecenter.core.iinterface.IApplication
|
||||
import com.google.auto.service.AutoService
|
||||
|
||||
@AutoService(IApplication::class)
|
||||
class HaloApp : IApplication {
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
override fun onCreate(application: Application) {
|
||||
mApp = application
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var mApp: Application
|
||||
|
||||
@JvmStatic
|
||||
fun getInstance(): Application {
|
||||
return mApp
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package com.gh.gamecenter.feature.cloudgame
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.gh.gamecenter.common.utils.EnvHelper
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONObject
|
||||
|
||||
object OkHelper {
|
||||
|
||||
@WorkerThread
|
||||
fun obtainServerSession(clientSession: String, userId: String): String {
|
||||
val client = OkHttpClient()
|
||||
|
||||
// Define the JSON body as a string
|
||||
val json = """
|
||||
{
|
||||
"user_id": "${userId}",
|
||||
"client_session": "${clientSession}",
|
||||
"application_id": "app-attadogx"
|
||||
}
|
||||
"""
|
||||
|
||||
// Create a RequestBody with the JSON and the appropriate content type
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val requestBody = json.toRequestBody(mediaType)
|
||||
|
||||
// Create the Request object
|
||||
val request = Request.Builder()
|
||||
.url(EnvHelper.getNewHost() + "tencent/car/start_app")
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
try {
|
||||
val response = client.newCall(request).execute()
|
||||
if (response.isSuccessful) {
|
||||
val resultString = response.body?.string() ?: ""
|
||||
|
||||
if (resultString.isNotEmpty()) {
|
||||
return JSONObject(resultString).optString("server_session", "")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
"Request failed with code: ${response.code}" // Handle error case
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"Error: ${e.message}" // Handle exceptions
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun stopServerSession(userId: String): String {
|
||||
val client = OkHttpClient()
|
||||
|
||||
// Define the JSON body as a string
|
||||
val json = """
|
||||
{
|
||||
"user_id": "${userId}"
|
||||
}
|
||||
"""
|
||||
|
||||
// Create a RequestBody with the JSON and the appropriate content type
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val requestBody = json.toRequestBody(mediaType)
|
||||
|
||||
// Create the Request object
|
||||
val request = Request.Builder()
|
||||
.url(EnvHelper.getNewHost() + "tencent/car/stop_app")
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
try {
|
||||
val response = client.newCall(request).execute()
|
||||
if (response.isSuccessful) {
|
||||
val resultString = response.body?.string() ?: ""
|
||||
|
||||
if (resultString.isNotEmpty()) {
|
||||
return JSONObject(resultString).optString("server_session", "")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
"Request failed with code: ${response.code}" // Handle error case
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"Error: ${e.message}" // Handle exceptions
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- First Stroke Layer -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="4dp" />
|
||||
<stroke
|
||||
android:width="3dp"
|
||||
android:color="@color/black" /> <!-- Second stroke color -->
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- Second Stroke Layer -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="4dp" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@color/primary_theme" /> <!-- First stroke color -->
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- Transparent Content Layer -->
|
||||
<item
|
||||
android:bottom="4dp"
|
||||
android:left="4dp"
|
||||
android:right="4dp"
|
||||
android:top="4dp">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="2dp" />
|
||||
<solid android:color="@android:color/transparent" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h48v48h-48z"/>
|
||||
<path
|
||||
android:fillAlpha="0.6"
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M24,24m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M18,17.608C18,16.038 19.728,15.08 21.06,15.913L31.286,22.304C32.54,23.087 32.54,24.913 31.286,25.696L21.06,32.088C19.728,32.92 18,31.962 18,30.392V17.608Z"/>
|
||||
</group>
|
||||
</vector>
|
||||
112
feature/cloud_game/src/main/res/layout/activity_cloudgame.xml
Normal file
112
feature/cloud_game/src/main/res/layout/activity_cloudgame.xml
Normal file
@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/render_view_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/rightController"
|
||||
app:layout_constraintStart_toEndOf="@id/leftController" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/keyboardContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="bottom"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/rightController"
|
||||
app:layout_constraintStart_toEndOf="@id/leftController" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/joystickContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="bottom"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/rightController"
|
||||
app:layout_constraintStart_toEndOf="@id/leftController" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingEnd="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/leftController"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/connectBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="启动" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/disconnectBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="关闭" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/rightController"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/keyboardBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="键盘" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/joystickBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="手柄" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/joystickEditBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="编辑手柄" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/cursorBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="鼠标" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
147
feature/cloud_game/src/main/res/layout/fragment_local_media.xml
Normal file
147
feature/cloud_game/src/main/res/layout/fragment_local_media.xml
Normal file
@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.gh.gamecenter.common.view.MaterializedConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/ui_background_fixed_dark"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.gh.gamecenter.common.view.StatusBarView
|
||||
android:id="@+id/statusBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/ui_surface_fixed_dark" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/normal_toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/appbar_height"
|
||||
app:layout_constraintTop_toBottomOf="@id/statusBar">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/normal_toolbar"
|
||||
style="@style/Base_ToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/ui_surface_fixed_dark"
|
||||
app:contentInsetEnd="0dp"
|
||||
app:contentInsetStartWithNavigation="0dp"
|
||||
app:navigationIcon="@null">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/backContainer"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/backBtn"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:srcCompat="@drawable/ic_function_close" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/normal_title"
|
||||
style="@style/toolbar_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:textColor="@color/text_aw_primary"
|
||||
android:textSize="16sp"
|
||||
tools:text="12345" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/arrowIv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginLeft="-12dp"
|
||||
android:layout_toRightOf="@+id/normal_title"
|
||||
android:padding="16dp"
|
||||
app:srcCompat="@drawable/ic_pin_down" />
|
||||
</RelativeLayout>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/ui_background_fixed_dark"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/normal_toolbar_container">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="2dp"
|
||||
android:layout_marginRight="2dp">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/list_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list_rv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<include
|
||||
android:id="@+id/pieceMediaControl"
|
||||
layout="@layout/piece_media_control" />
|
||||
|
||||
<com.gh.gamecenter.common.view.NavigationBarView
|
||||
android:id="@+id/navigationBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/ui_surface_fixed_dark" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/reuse_ll_loading"
|
||||
layout="@layout/reuse_loading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
<include
|
||||
android:id="@+id/reuse_no_connection"
|
||||
layout="@layout/reuse_no_connection" />
|
||||
|
||||
<include
|
||||
android:id="@+id/reuse_none_data"
|
||||
layout="@layout/reuse_none_data" />
|
||||
|
||||
<include
|
||||
android:id="@+id/reuse_data_exception"
|
||||
layout="@layout/reuse_data_exception" />
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_activity_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</com.gh.gamecenter.common.view.MaterializedConstraintLayout>
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="100dp"
|
||||
app:cardBackgroundColor="@color/secondary_yellow">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/iconTv"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
3
feature/cloud_game/src/main/res/values-night/themes.xml
Normal file
3
feature/cloud_game/src/main/res/values-night/themes.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
</resources>
|
||||
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
|
||||
</resources>
|
||||
10
feature/cloud_game/src/main/res/values/colors.xml
Normal file
10
feature/cloud_game/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
3
feature/cloud_game/src/main/res/values/strings.xml
Normal file
3
feature/cloud_game/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">SimpleDemo</string>
|
||||
</resources>
|
||||
16
feature/cloud_game/src/main/res/values/themes.xml
Normal file
16
feature/cloud_game/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.SimpleDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@ -5,8 +5,6 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.csjad'
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.gh.gamecenter.csjad">
|
||||
|
||||
<!--必要权限-->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
@ -5,7 +5,6 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.floatingwindow'
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter.floatingwindow">
|
||||
|
||||
<application>
|
||||
</application>
|
||||
|
||||
@ -6,8 +6,6 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.jg.push'
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.gh.gamecenter.jg.push">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.hihonor.push.sdk,com.heytap.mcssdk" />
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "kotlin-kapt"
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.selector'
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter.selector">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
|
||||
@ -9,7 +9,6 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.feedback'
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter.feedback">
|
||||
|
||||
<application>
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.gh.gamecenter.feedback">
|
||||
|
||||
<!-- 允许应用程序访问网络连接 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
@ -6,7 +6,6 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.oaid'
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.gh.gamecenter.oaid">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-sdk tools:overrideLibrary="com.bun.miitmdid" />
|
||||
|
||||
@ -9,7 +9,6 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.pkg'
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter.pkg">
|
||||
|
||||
<application>
|
||||
</application>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.gh.gamecenter.pkg">
|
||||
|
||||
<!-- 允许应用程序访问网络连接 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
@ -5,8 +5,6 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.qqgame'
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.gh.gamecenter.qqgame">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application>
|
||||
|
||||
@ -3,7 +3,6 @@ apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
android {
|
||||
namespace 'com.gh.sentry'
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.sentry">
|
||||
|
||||
|
||||
</manifest>
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter.wechat.pay">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
#Wed Jul 19 10:16:09 CST 2017
|
||||
org.gradle.jvmargs=-Xmx4096m -XX\:+HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8
|
||||
org.gradle.jvmargs=-Xmx4096m -XX\:MaxPermSize\=2048m -XX\:+HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8
|
||||
#开启gradle并行编译
|
||||
org.gradle.parallel=true
|
||||
#开启守护进程
|
||||
@ -96,8 +96,4 @@ android.injected.testOnly = false
|
||||
|
||||
# 动态配置插件
|
||||
isRelease = true
|
||||
pluginBasePath=vasdk/
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.enableBuildConfigAsBytecode=true
|
||||
android.nonFinalResIds=false
|
||||
android.defaults.buildfeatures.renderscript=true
|
||||
pluginBasePath=vasdk/
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Mon Oct 30 17:29:06 CST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.0-bin.zip
|
||||
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-7.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
65
init.gradle
65
init.gradle
@ -1,6 +1,7 @@
|
||||
allprojects { project ->
|
||||
buildscript {
|
||||
ext.booster_version = '5.0.0'
|
||||
ext.booster_version = '4.9.0'
|
||||
ext.plugin_version = "0.3.0"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@ -8,10 +9,16 @@ allprojects { project ->
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/public' }
|
||||
maven { url "https://artifact.bytedance.com/repository/byteX/" }
|
||||
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// byteX
|
||||
classpath "com.bytedance.android.byteX:base-plugin:${plugin_version}"
|
||||
classpath "com.bytedance.android.byteX:const-inline-plugin:${plugin_version}"
|
||||
classpath "com.bytedance.android.byteX:method-call-opt-plugin:${plugin_version}"
|
||||
classpath "com.bytedance.android.byteX:field-assign-opt-plugin:${plugin_version}"
|
||||
|
||||
// booster
|
||||
classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
|
||||
@ -38,6 +45,62 @@ allprojects { project ->
|
||||
project.apply plugin: 'com.didiglobal.booster'
|
||||
|
||||
project.apply plugin: "com.gh.gamecenter.plugin"
|
||||
|
||||
project.apply plugin: 'bytex'
|
||||
project.apply plugin: 'bytex.method_call_opt' // 移除 log https://github.com/bytedance/ByteX/blob/master/method-call-opt-plugin/README-zh.md
|
||||
project.apply plugin: 'bytex.field_assign_opt' //去除重复的赋值 https://github.com/bytedance/ByteX/blob/master/field-assign-opt-plugin/README-zh.md
|
||||
//
|
||||
project.method_call_opt {
|
||||
enable true
|
||||
enableInDebug false
|
||||
logLevel "DEBUG"
|
||||
//是否在log中显示删除方法调用指令后的方法指令,一般调试时使用
|
||||
showAfterOptInsLog false
|
||||
//需要删除的方法配置
|
||||
methodList = [
|
||||
//下面的每一项配置必须严格按照数据配置,一个地方不对这一项不生效。
|
||||
//class#method#desc
|
||||
"android/util/Log#v#(Ljava/lang/String;Ljava/lang/String;)I",
|
||||
"android/util/Log#v#(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I",
|
||||
"android/util/Log#d#(Ljava/lang/String;Ljava/lang/String;)I",
|
||||
"android/util/Log#d#(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I",
|
||||
"android/util/Log#i#(Ljava/lang/String;Ljava/lang/String;)I",
|
||||
"android/util/Log#i#(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I",
|
||||
"android/util/Log#w#(Ljava/lang/String;Ljava/lang/String;)I",
|
||||
"android/util/Log#w#(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I",
|
||||
"android/util/Log#e#(Ljava/lang/String;Ljava/lang/String;)I",
|
||||
"android/util/Log#e#(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I",
|
||||
"android/util/Log#println#(ILjava/lang/String;Ljava/lang/String;)I",
|
||||
|
||||
"java/lang/Throwable#printStackTrace#()V",
|
||||
"com/google/devtools/build/android/desugar/runtime/ThrowableExtension#printStackTrace#(Ljava/lang/Throwable;)V",
|
||||
|
||||
//项目中的方法
|
||||
"com/lightgame/utils/Utils#log#(Ljava/lang/String;)V",
|
||||
"com/lightgame/utils/Utils#log#(Ljava/lang/int;Ljava/lang/String;Ljava/lang/String;)V",
|
||||
"com/lightgame/utils/Utils#log#(Ljava/lang/String;Ljava/lang/String;)V",
|
||||
"com/lightgame/utils/Utils#log#(Ljava/lang/String;Ljava/lang/Object;)V",
|
||||
"com/lightgame/utils/Utils#log#(Ljava/lang/Object;)V",
|
||||
"com/gh/gamecenter/common/util/MtaHelper#onEvent#(Ljava/lang/Object;Ljava/lang/String;)V",
|
||||
"com/gh/gamecenter/common/util/MtaHelper#onEventWithTime#(Ljava/lang/String;I[Ljava/lang/String;)V",
|
||||
"com/gh/gamecenter/common/util/MtaHelper#onEventWithBasicDeviceInfo#(Ljava/lang/String;[Ljava/lang/String;)V"
|
||||
]
|
||||
onlyCheckList = []
|
||||
whiteList = [
|
||||
"com/tencent/qqmini/minigame/opensdk/share/OpenSdkShareHelper*",
|
||||
]
|
||||
}
|
||||
|
||||
project.field_assign_opt {
|
||||
enable false
|
||||
enableInDebug false
|
||||
logLevel "INFO"
|
||||
removeLineNumber true // 同时移除赋值对应的行号信息(如果有的话),默认true。
|
||||
whiteList = [
|
||||
//白名单,ClassName.FieldName 。不支持模式匹配
|
||||
//"android.support.constraint.solver.ArrayRow.isSimpleDefinition"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
allprojects { project ->
|
||||
buildscript {
|
||||
ext.booster_version = '5.0.0'
|
||||
ext.booster_version = '4.9.0'
|
||||
ext.plugin_version = "0.3.0"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@ -13,6 +14,12 @@ allprojects { project ->
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// byteX
|
||||
classpath "com.bytedance.android.byteX:base-plugin:${plugin_version}"
|
||||
classpath "com.bytedance.android.byteX:const-inline-plugin:${plugin_version}"
|
||||
classpath "com.bytedance.android.byteX:method-call-opt-plugin:${plugin_version}"
|
||||
classpath "com.bytedance.android.byteX:field-assign-opt-plugin:${plugin_version}"
|
||||
|
||||
// booster
|
||||
classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
|
||||
// classpath "com.didiglobal.booster:booster-transform-shared-preferences:$booster_version"
|
||||
@ -28,6 +35,7 @@ allprojects { project ->
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/public' }
|
||||
maven { url "https://artifact.bytedance.com/repository/byteX/" }
|
||||
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||
}
|
||||
|
||||
@ -37,6 +45,20 @@ allprojects { project ->
|
||||
project.apply plugin: 'com.didiglobal.booster'
|
||||
|
||||
project.apply plugin: "com.gh.gamecenter.plugin"
|
||||
|
||||
project.apply plugin: 'bytex'
|
||||
project.apply plugin: 'bytex.field_assign_opt' //去除重复的赋值 https://github.com/bytedance/ByteX/blob/master/field-assign-opt-plugin/README-zh.md
|
||||
|
||||
project.field_assign_opt {
|
||||
enable false
|
||||
enableInDebug false
|
||||
logLevel "INFO"
|
||||
removeLineNumber true // 同时移除赋值对应的行号信息(如果有的话),默认true。
|
||||
whiteList = [
|
||||
//白名单,ClassName.FieldName 。不支持模式匹配
|
||||
//"android.support.constraint.solver.ArrayRow.isSimpleDefinition"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Submodule libraries/LGLibrary updated: 480388c379...8375fcb09d
@ -20,8 +20,6 @@ android {
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
namespace "com.zhihu.matisse"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android = "http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android = "http://schemas.android.com/apk/res/android"
|
||||
package = "com.zhihu.matisse" >
|
||||
|
||||
<uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name = "android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
@ -9,6 +9,4 @@ android {
|
||||
defaultConfig {
|
||||
consumerProguardFiles 'proguard-library.txt'
|
||||
}
|
||||
|
||||
namespace "com.gh.qqshare"
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android = "http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android = "http://schemas.android.com/apk/res/android"
|
||||
package = "com.gh.qqshare" >
|
||||
|
||||
<uses-permission android:name = "android.permission.INTERNET" />
|
||||
<uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
@ -7,7 +7,6 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.common'
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
@ -74,6 +73,7 @@ dependencies {
|
||||
api "com.tencent.mm.opensdk:wechat-sdk-android-without-mta:${mta}"
|
||||
api "androidx.room:room-runtime:${room}"
|
||||
api "androidx.room:room-rxjava2:${room}"
|
||||
api "androidx.room:room-ktx:${room}"
|
||||
ksp("androidx.room:room-compiler:${room}")
|
||||
api "androidx.collection:collection-ktx:${collection}"
|
||||
api "androidx.activity:activity:${activity}"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter.common">
|
||||
<!-- 允许应用程序获取网络信息状态 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
</manifest>
|
||||
@ -337,6 +337,7 @@ object SensorsBridge {
|
||||
private const val EVENT_CALENDAR_PERMISSIONS_DIALOG_SHOW = "CalendarPermissionsDialogShow"
|
||||
private const val EVENT_CALENDAR_PERMISSIONS_DIALOG_CLICK = "CalendarPermissionsDialogClick"
|
||||
private const val CALENDAR_PERMISSIONS_DIALOG_RESULT = "CalendarPermissionsDialogResult"
|
||||
private const val EVENT_GAME_DETAIL_VIDEO_CLICK= "GameDetailVideoClick"
|
||||
|
||||
private var mIsSensorsEnabled = false
|
||||
|
||||
@ -5340,4 +5341,37 @@ object SensorsBridge {
|
||||
}
|
||||
trackEvent(CALENDAR_PERMISSIONS_DIALOG_RESULT, json)
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件ID:GameDetailVideoClick
|
||||
* 事件名称:游戏详情视频点击事件
|
||||
*/
|
||||
fun trackGameDetailVideoClick(
|
||||
gameName: String,
|
||||
gameId: String,
|
||||
gameType: String,
|
||||
lastPageName: String,
|
||||
lastPageId: String,
|
||||
action: String,
|
||||
playType: String,
|
||||
isFullScreen: Boolean,
|
||||
sequence: Int,
|
||||
tabName: String,
|
||||
playLength: String
|
||||
) {
|
||||
val json = json {
|
||||
KEY_GAME_ID to gameId
|
||||
KEY_GAME_NAME to gameName
|
||||
KEY_GAME_TYPE to gameType
|
||||
KEY_LAST_PAGE_ID to lastPageId
|
||||
KEY_LAST_PAGE_NAME to lastPageName
|
||||
KEY_ACTION to action
|
||||
KEY_PLAY_TYPE to playType
|
||||
"is_full_screen" to isFullScreen
|
||||
KEY_SEQUENCE to sequence
|
||||
"tab_name" to tabName
|
||||
"play_length" to playLength
|
||||
}
|
||||
trackEvent(EVENT_GAME_DETAIL_VIDEO_CLICK, json)
|
||||
}
|
||||
}
|
||||
@ -105,6 +105,7 @@ class SegmentedFilterView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
if (mContainerBackground != null) background = mContainerBackground
|
||||
mContainer = FrameLayout(context)
|
||||
mIndicator = View(context).apply {
|
||||
visibility = View.GONE
|
||||
background = mIndicatorBackground
|
||||
}
|
||||
mRadioGroup = RadioGroup(context).apply {
|
||||
@ -127,6 +128,7 @@ class SegmentedFilterView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
mIndicator.updateLayoutParams<ViewGroup.LayoutParams> {
|
||||
width = mRadioGroup.getChildAt(defaultCheckPosition)?.width ?: mItemWidth
|
||||
}
|
||||
mIndicator.visibility = View.VISIBLE
|
||||
setChecked(defaultCheckPosition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.core'
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter.core">
|
||||
|
||||
|
||||
</manifest>
|
||||
@ -7,7 +7,6 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.gh.gamecenter.feature'
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user