Compare commits

..

21 Commits

Author SHA1 Message Date
718ed2d209 feat: 简单对接云游戏 SDK 2025-04-22 15:21:47 +08:00
e3f883b784 Merge branch 'feat/GHZSCY-7828' into 'dev'
feat: 游戏预约相关优化—客户端 https://jira.shanqu.cc/browse/GHZSCY-7828

See merge request halo/android/assistant-android!2150
2025-04-08 15:31:12 +08:00
fbb81d7e45 feat: 游戏预约相关优化—客户端 https://jira.shanqu.cc/browse/GHZSCY-7828 2025-04-08 15:28:21 +08:00
750ec04c9d Merge branch 'fix/game_detail_cover_tab' into 'dev'
fix: 修复游戏详情视频/图集Tab闪烁的问题

See merge request halo/android/assistant-android!2149
2025-04-07 18:00:37 +08:00
e23a3d3938 fix: 修复游戏详情视频/图集Tab闪烁的问题 2025-04-07 17:57:14 +08:00
d75020cdb5 Merge branch 'feat/GHZSCY-7781' into 'dev'
fix: 游戏详情-视频/图集tab 神策埋点补充—客户端 https://jira.shanqu.cc/browse/GHZSCY-7781

See merge request halo/android/assistant-android!2148
2025-04-07 09:54:40 +08:00
c56c71d1f1 fix: 游戏详情-视频/图集tab 神策埋点补充—客户端 https://jira.shanqu.cc/browse/GHZSCY-7781 2025-04-07 09:54:40 +08:00
ccfa50d748 Merge branch 'feat/GHZSCY-7515' into 'dev'
feat:开服订阅通知频率优化—客户端 https://jira.shanqu.cc/browse/GHZSCY-7515

See merge request halo/android/assistant-android!2145
2025-04-03 11:33:41 +08:00
7176a5a4c4 feat:开服订阅通知频率优化—客户端 https://jira.shanqu.cc/browse/GHZSCY-7515 2025-04-03 11:33:41 +08:00
d007092193 Merge branch 'feat/GHZSCY-7702' into 'dev'
feat: 实名认证字符优化 (光环助手和畅玩)

See merge request halo/android/assistant-android!2147
2025-04-03 10:45:54 +08:00
2552743ff2 Merge branch 'feat/GHZSCY-7702' into 'dev'
feat: 实名认证字符优化 (光环助手和畅玩)

See merge request halo/android/assistant-android!2146
2025-04-03 10:42:01 +08:00
4faf15ffe7 feat: 实名认证字符优化 (光环助手和畅玩) 2025-04-03 10:42:01 +08:00
33d6ed75ad ci: revert 2025-04-03 10:39:55 +08:00
2eb4878b12 Merge branch 'fix/GHZSCY-7806' into 'dev'
fix:【光环助手】通知栏目 滚动问题 https://jira.shanqu.cc/browse/GHZSCY-7806

See merge request halo/android/assistant-android!2144
2025-04-03 10:07:41 +08:00
2bea3c72d7 fix:【光环助手】通知栏目 滚动问题 https://jira.shanqu.cc/browse/GHZSCY-7806 2025-04-03 09:59:57 +08:00
fa3db9d521 fix: 实名认证页面首页,右上角多显示了“·”的按钮 https://jira.shanqu.cc/browse/GHZSCY-7702,https://jira.shanqu.cc/browse/GHZSCY-7791 2025-04-02 16:36:30 +08:00
efee9409bf Merge branch 'feat/add-room-ktx' into 'dev'
为了Room能和Flow一起使用,需要引入room-ktx

See merge request halo/android/assistant-android!2141
2025-04-01 09:31:26 +08:00
38a7eaf780 为了Room能和Flow一起使用,需要引入room-ktx 2025-04-01 09:28:05 +08:00
a1b4233fdb Merge branch 'feat/GHZSCY-7784' into 'dev'
feat:处理以 LiveData 形式直接监听数据库变更的代码 https://jira.shanqu.cc/browse/GHZSCY-7784

See merge request halo/android/assistant-android!2140
2025-03-31 17:20:33 +08:00
60c24e7457 feat:处理以 LiveData 形式直接监听数据库变更的代码 https://jira.shanqu.cc/browse/GHZSCY-7784 2025-03-31 17:20:33 +08:00
729685e764 feat: 实名认证字符优化 (光环助手和畅玩)
https://jira.shanqu.cc/browse/GHZSCY-7702https://jira.shanqu.cc/browse/GHZSCY-7704
2025-03-25 17:54:35 +08:00
121 changed files with 1931 additions and 1298 deletions

View File

@ -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')

View File

@ -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.* { *; }

View File

@ -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" />

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
)
}
}
}
}

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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;

View File

@ -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()
}
}
}
}

View File

@ -38,6 +38,8 @@ class VDownloadManagerViewModel(application: Application) :
load(it)
}
val vGames = VHelper.vGameDao.getAllGames()
fun refresh() {
loadPublishSubject.onNext(LoadType.REFRESH)
}

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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">

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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"

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
}
}
}

View File

@ -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("========================")
}
}

View File

@ -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")
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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"

View File

@ -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
View File

@ -0,0 +1 @@
/build

View 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
View 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

View 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>

File diff suppressed because one or more lines are too long

View File

@ -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
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}
}

View File

@ -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 ""
}
}

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -0,0 +1,3 @@
<resources xmlns:tools="http://schemas.android.com/tools">
</resources>

View File

@ -0,0 +1,3 @@
<resources>
</resources>

View 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>

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">SimpleDemo</string>
</resources>

View 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>

View File

@ -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 {

View File

@ -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" />

View File

@ -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 {

View File

@ -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>

View File

@ -6,8 +6,6 @@ plugins {
}
android {
namespace 'com.gh.gamecenter.jg.push'
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {

View File

@ -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" />

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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>

View File

@ -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" />

View File

@ -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 {

View File

@ -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" />

View File

@ -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 {

View File

@ -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>

View File

@ -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" />

View File

@ -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 {

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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"/>

View File

@ -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/

View File

@ -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

View File

@ -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"
]
}
}
}

View File

@ -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"
]
}
}
}

View File

@ -20,8 +20,6 @@ android {
lintOptions {
abortOnError false
}
namespace "com.zhihu.matisse"
}
dependencies {

View File

@ -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" />

View File

@ -9,6 +9,4 @@ android {
defaultConfig {
consumerProguardFiles 'proguard-library.txt'
}
namespace "com.gh.qqshare"
}

View File

@ -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" />

View File

@ -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}"

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -6,7 +6,6 @@ plugins {
}
android {
namespace 'com.gh.gamecenter.core'
compileSdkVersion rootProject.ext.compileSdkVersion

View File

@ -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>

View File

@ -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