MainActivity.kt 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. package com.xuqm.sample
  2. import android.content.BroadcastReceiver
  3. import android.content.Context
  4. import android.content.Intent
  5. import android.content.IntentFilter
  6. import android.os.Bundle
  7. import androidx.activity.ComponentActivity
  8. import androidx.activity.compose.BackHandler
  9. import androidx.activity.compose.setContent
  10. import androidx.activity.result.contract.ActivityResultContracts
  11. import androidx.core.content.ContextCompat
  12. import androidx.compose.foundation.layout.Arrangement
  13. import androidx.compose.foundation.layout.Column
  14. import androidx.compose.foundation.layout.PaddingValues
  15. import androidx.compose.foundation.layout.fillMaxSize
  16. import androidx.compose.foundation.layout.fillMaxWidth
  17. import androidx.compose.foundation.layout.padding
  18. import androidx.compose.foundation.lazy.LazyColumn
  19. import androidx.compose.material3.Button
  20. import androidx.compose.material3.Card
  21. import androidx.compose.material3.ExperimentalMaterial3Api
  22. import androidx.compose.material3.AlertDialog
  23. import androidx.compose.material3.MaterialTheme
  24. import androidx.compose.material3.Scaffold
  25. import androidx.compose.material3.Text
  26. import androidx.compose.material3.TopAppBar
  27. import androidx.compose.material3.TextButton
  28. import androidx.compose.runtime.Composable
  29. import androidx.compose.runtime.DisposableEffect
  30. import androidx.compose.runtime.getValue
  31. import androidx.compose.runtime.mutableLongStateOf
  32. import androidx.compose.runtime.mutableStateOf
  33. import androidx.compose.runtime.remember
  34. import androidx.compose.runtime.setValue
  35. import androidx.compose.ui.Modifier
  36. import androidx.compose.ui.unit.dp
  37. import androidx.lifecycle.Lifecycle
  38. import androidx.lifecycle.LifecycleEventObserver
  39. import androidx.lifecycle.compose.LocalLifecycleOwner
  40. import androidx.lifecycle.lifecycleScope
  41. import com.xuqm.sdk.CoreSDK
  42. import com.xuqm.sdk.compose.components.AccordionGroup
  43. import com.xuqm.sdk.compose.components.FeatureCard
  44. import com.xuqm.sdk.plugin.PluginPackageManager
  45. import com.xuqm.sdk.ui.ToastCenter
  46. import com.xuqm.sdk.update.DownloadState
  47. import com.xuqm.sdk.update.StoragePath
  48. import com.xuqm.sdk.update.UpdateInfo
  49. import com.xuqm.sdk.update.VersionCheckResult
  50. import com.xuqm.sdk.update.VersionCheckStrategy
  51. import com.xuqm.sdk.utils.DateTimeUtils
  52. import com.xuqm.sample.update.UpdateRepository
  53. import com.xuqm.szyx.SzyxSDK
  54. import com.xuqm.szyx.auth.LoginSession
  55. import com.xuqm.szyx.auth.UserSessionManager
  56. import com.xuqm.szyx.login.SzyxLoginActivity
  57. import kotlinx.coroutines.Job
  58. import kotlinx.coroutines.launch
  59. class MainActivity : ComponentActivity() {
  60. companion object {
  61. private const val PLUGIN_PACKAGE_NAME = "com.xuqm.plugin.ui"
  62. private const val PLUGIN_ENTRY_ACTIVITY = "com.xuqm.plugin.ui.PluginUiActivity"
  63. }
  64. private val sessionState = mutableStateOf<LoginSession?>(null)
  65. private val pluginInstalledState = mutableStateOf(false)
  66. private val pluginDownloadState = mutableStateOf<DownloadState>(DownloadState.Idle)
  67. private val appDownloadState = mutableStateOf<DownloadState>(DownloadState.Idle)
  68. private val pendingAppUpdateState = mutableStateOf<UpdateInfo?>(null)
  69. private var pluginDownloadTaskId: String? = null
  70. private var appDownloadTaskId: String? = null
  71. private var pluginDownloadJob: Job? = null
  72. private var appDownloadJob: Job? = null
  73. private var loginPromptedOnLaunch = false
  74. private var reloadPluginAfterInstall = false
  75. private val updateRepository by lazy { UpdateRepository(BuildConfig.UPDATE_SERVER_BASE_URL) }
  76. private var currentPluginUpdateInfo: PluginPackageManager.PluginUpdateInfo? = null
  77. private val packageChangedReceiver = object : BroadcastReceiver() {
  78. override fun onReceive(context: Context?, intent: Intent?) {
  79. val packageName = intent?.data?.schemeSpecificPart ?: return
  80. if (packageName == PLUGIN_PACKAGE_NAME) {
  81. refreshState()
  82. ToastCenter.show("plugin-ui 安装状态已更新")
  83. if (intent.action == Intent.ACTION_PACKAGE_ADDED && reloadPluginAfterInstall) {
  84. reloadPluginAfterInstall = false
  85. val pluginUpdateInfo = currentPluginUpdateInfo
  86. CoreSDK.pluginPackageManager().reloadPlugin(
  87. packageName = PLUGIN_PACKAGE_NAME,
  88. entryActivity = pluginUpdateInfo?.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
  89. extras = pluginUpdateInfo?.extras ?: mapOf("hostPackageName" to this@MainActivity.packageName),
  90. )
  91. }
  92. }
  93. }
  94. }
  95. private val loginLauncher =
  96. registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
  97. refreshSession()
  98. }
  99. override fun onCreate(savedInstanceState: Bundle?) {
  100. super.onCreate(savedInstanceState)
  101. CoreSDK.init(this, CoreSDK.SDKConfig(debugMode = true))
  102. SzyxSDK.init(
  103. this,
  104. SzyxSDK.Config(
  105. baseUrl = "https://dev.51trust.com/",
  106. clientId = "2000111111110002",
  107. hostAppPackageName = packageName,
  108. debugMode = true,
  109. ),
  110. )
  111. ToastCenter.init(this)
  112. refreshState()
  113. ensureLoginOnLaunch()
  114. registerPackageChangeReceiver()
  115. setContent {
  116. MaterialTheme {
  117. SampleHome(
  118. session = sessionState.value,
  119. pluginInstalled = pluginInstalledState.value,
  120. pluginDownloadState = pluginDownloadState.value,
  121. appDownloadState = appDownloadState.value,
  122. pendingAppUpdate = pendingAppUpdateState.value,
  123. onOpenLogin = ::openLogin,
  124. onOpenPlugin = ::openPlugin,
  125. onInstallPlugin = ::downloadPlugin,
  126. onCancelPluginDownload = ::cancelPluginDownload,
  127. onUpdateApp = ::downloadApp,
  128. onConfirmAppUpdate = ::confirmDownloadAppUpdate,
  129. onDismissAppUpdate = ::dismissAppUpdateDialog,
  130. onCancelAppDownload = ::cancelAppDownload,
  131. onRetryCheckPlugin = ::refreshState,
  132. onExitApp = { finish() },
  133. )
  134. }
  135. }
  136. }
  137. override fun onDestroy() {
  138. pluginDownloadJob?.cancel()
  139. appDownloadJob?.cancel()
  140. runCatching { unregisterReceiver(packageChangedReceiver) }
  141. super.onDestroy()
  142. }
  143. override fun onResume() {
  144. super.onResume()
  145. refreshState()
  146. }
  147. private fun ensureLoginOnLaunch() {
  148. if (sessionState.value == null && !loginPromptedOnLaunch) {
  149. loginPromptedOnLaunch = true
  150. openLogin()
  151. }
  152. }
  153. private fun refreshSession() {
  154. sessionState.value = UserSessionManager.getSession()
  155. }
  156. private fun refreshState() {
  157. refreshSession()
  158. pluginInstalledState.value = CoreSDK.pluginPackageManager().isPluginInstalled(PLUGIN_PACKAGE_NAME)
  159. }
  160. private fun openLogin() {
  161. loginLauncher.launch(Intent(this, SzyxLoginActivity::class.java))
  162. }
  163. private fun registerPackageChangeReceiver() {
  164. val filter = IntentFilter().apply {
  165. addAction(Intent.ACTION_PACKAGE_ADDED)
  166. addAction(Intent.ACTION_PACKAGE_REMOVED)
  167. addDataScheme("package")
  168. }
  169. ContextCompat.registerReceiver(
  170. this,
  171. packageChangedReceiver,
  172. filter,
  173. ContextCompat.RECEIVER_NOT_EXPORTED,
  174. )
  175. }
  176. private fun openPlugin() {
  177. val session = sessionState.value
  178. if (session == null) {
  179. ToastCenter.show("请先登录")
  180. openLogin()
  181. return
  182. }
  183. val pluginManager = CoreSDK.pluginPackageManager()
  184. pluginManager.cacheCurrentUser(
  185. userId = session.loginModel.userId,
  186. sessionId = session.loginModel.sessionId,
  187. clientId = SzyxSDK.requireConfig().clientId,
  188. extraData = mapOf("phone" to session.phone),
  189. )
  190. val launched = pluginManager.startPlugin(
  191. packageName = PLUGIN_PACKAGE_NAME,
  192. entryActivity = PLUGIN_ENTRY_ACTIVITY,
  193. extras = mapOf("hostPackageName" to packageName),
  194. )
  195. if (!launched) {
  196. ToastCenter.show("未检测到已安装的 plugin-ui,请先下载安装")
  197. }
  198. }
  199. private fun downloadPlugin() {
  200. lifecycleScope.launch {
  201. updateRepository.fetchLatestPluginUpdate(PLUGIN_PACKAGE_NAME)
  202. .onSuccess { remoteUpdate ->
  203. val pluginUpdateInfo = remoteUpdate.copy(
  204. entryActivity = remoteUpdate.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
  205. extras = mapOf("hostPackageName" to packageName),
  206. )
  207. val checkResult = CoreSDK.pluginPackageManager().checkPluginUpdate(
  208. packageName = pluginUpdateInfo.packageName,
  209. remoteVersionCode = pluginUpdateInfo.versionCode,
  210. remoteVersionName = pluginUpdateInfo.versionName,
  211. strategy = VersionCheckStrategy.VERSION_CODE_OR_NAME,
  212. )
  213. if (checkResult is VersionCheckResult.UpToDate) {
  214. ToastCenter.show("当前插件已是最新版本 ${checkResult.current.versionName}(${checkResult.current.versionCode})")
  215. return@onSuccess
  216. }
  217. currentPluginUpdateInfo = pluginUpdateInfo
  218. val taskId = CoreSDK.pluginPackageManager().downloadPlugin(
  219. updateInfo = pluginUpdateInfo,
  220. fileName = "plugin-ui-release.apk",
  221. storagePath = StoragePath.EXTERNAL_FILES,
  222. )
  223. pluginDownloadTaskId = taskId
  224. observePluginDownload(taskId)
  225. }
  226. .onFailure {
  227. ToastCenter.show(it.message ?: "获取插件更新配置失败")
  228. }
  229. }
  230. }
  231. private fun cancelPluginDownload() {
  232. val taskId = pluginDownloadTaskId ?: return
  233. CoreSDK.downloadManager().cancel(taskId)
  234. }
  235. private fun downloadApp() {
  236. lifecycleScope.launch {
  237. updateRepository.fetchLatestAppUpdate(packageName)
  238. .onSuccess { updateInfo ->
  239. when (
  240. val checkResult = CoreSDK.appUpdater().checkUpdate(
  241. updateInfo = updateInfo,
  242. strategy = VersionCheckStrategy.VERSION_CODE_OR_NAME,
  243. )
  244. ) {
  245. is VersionCheckResult.NeedUpdate -> {
  246. pendingAppUpdateState.value = updateInfo
  247. }
  248. is VersionCheckResult.UpToDate -> {
  249. ToastCenter.show("当前已是最新版本 ${checkResult.current.versionName}(${checkResult.current.versionCode})")
  250. }
  251. }
  252. }
  253. .onFailure {
  254. ToastCenter.show(it.message ?: "获取 App 更新配置失败")
  255. }
  256. }
  257. }
  258. private fun confirmDownloadAppUpdate() {
  259. val updateInfo = pendingAppUpdateState.value ?: return
  260. pendingAppUpdateState.value = null
  261. val taskId = CoreSDK.appUpdater().downloadUpdate(
  262. updateInfo = updateInfo,
  263. fileName = "sample-app-update.apk",
  264. storagePath = StoragePath.EXTERNAL_FILES,
  265. )
  266. appDownloadTaskId = taskId
  267. observeAppDownload(taskId)
  268. }
  269. private fun cancelAppDownload() {
  270. val taskId = appDownloadTaskId ?: return
  271. CoreSDK.downloadManager().cancel(taskId)
  272. }
  273. private fun dismissAppUpdateDialog() {
  274. pendingAppUpdateState.value = null
  275. }
  276. private fun observePluginDownload(taskId: String) {
  277. pluginDownloadJob?.cancel()
  278. pluginDownloadJob = lifecycleScope.launch {
  279. CoreSDK.downloadManager().observe(taskId)?.collect { state ->
  280. pluginDownloadState.value = state
  281. when (state) {
  282. is DownloadState.Success -> {
  283. ToastCenter.show("插件下载完成,准备重新加载")
  284. reloadPluginAfterInstall = true
  285. if (!CoreSDK.pluginPackageManager().loadPlugin(state.file)) {
  286. reloadPluginAfterInstall = false
  287. ToastCenter.show("插件加载拉起失败")
  288. }
  289. CoreSDK.downloadManager().clear(taskId)
  290. pluginDownloadTaskId = null
  291. pluginDownloadJob?.cancel()
  292. }
  293. is DownloadState.Error -> {
  294. ToastCenter.show("插件下载失败: ${state.message}")
  295. CoreSDK.downloadManager().clear(taskId)
  296. pluginDownloadTaskId = null
  297. pluginDownloadJob?.cancel()
  298. }
  299. DownloadState.Cancelled -> {
  300. ToastCenter.show("插件下载已取消")
  301. CoreSDK.downloadManager().clear(taskId)
  302. pluginDownloadTaskId = null
  303. pluginDownloadJob?.cancel()
  304. }
  305. else -> Unit
  306. }
  307. }
  308. }
  309. }
  310. private fun observeAppDownload(taskId: String) {
  311. appDownloadJob?.cancel()
  312. appDownloadJob = lifecycleScope.launch {
  313. CoreSDK.downloadManager().observe(taskId)?.collect { state ->
  314. appDownloadState.value = state
  315. when (state) {
  316. is DownloadState.Success -> {
  317. ToastCenter.show("安装包下载完成,准备安装")
  318. if (!CoreSDK.appUpdater().installApk(state.file)) {
  319. ToastCenter.show("应用安装拉起失败")
  320. }
  321. CoreSDK.downloadManager().clear(taskId)
  322. appDownloadTaskId = null
  323. appDownloadJob?.cancel()
  324. }
  325. is DownloadState.Error -> {
  326. ToastCenter.show("应用下载失败: ${state.message}")
  327. CoreSDK.downloadManager().clear(taskId)
  328. appDownloadTaskId = null
  329. appDownloadJob?.cancel()
  330. }
  331. DownloadState.Cancelled -> {
  332. ToastCenter.show("应用下载已取消")
  333. CoreSDK.downloadManager().clear(taskId)
  334. appDownloadTaskId = null
  335. appDownloadJob?.cancel()
  336. }
  337. else -> Unit
  338. }
  339. }
  340. }
  341. }
  342. }
  343. @OptIn(ExperimentalMaterial3Api::class)
  344. @Composable
  345. private fun SampleHome(
  346. session: LoginSession?,
  347. pluginInstalled: Boolean,
  348. pluginDownloadState: DownloadState,
  349. appDownloadState: DownloadState,
  350. pendingAppUpdate: UpdateInfo?,
  351. onOpenLogin: () -> Unit,
  352. onOpenPlugin: () -> Unit,
  353. onInstallPlugin: () -> Unit,
  354. onCancelPluginDownload: () -> Unit,
  355. onUpdateApp: () -> Unit,
  356. onConfirmAppUpdate: () -> Unit,
  357. onDismissAppUpdate: () -> Unit,
  358. onCancelAppDownload: () -> Unit,
  359. onRetryCheckPlugin: () -> Unit,
  360. onExitApp: () -> Unit,
  361. ) {
  362. val lifecycleOwner = LocalLifecycleOwner.current
  363. var lastBackPressedAt by remember { mutableLongStateOf(0L) }
  364. DisposableEffect(lifecycleOwner) {
  365. val observer = LifecycleEventObserver { _, event ->
  366. if (event == Lifecycle.Event.ON_RESUME) {
  367. onRetryCheckPlugin()
  368. }
  369. }
  370. lifecycleOwner.lifecycle.addObserver(observer)
  371. onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
  372. }
  373. BackHandler(enabled = session == null) {
  374. val now = System.currentTimeMillis()
  375. if (now - lastBackPressedAt < 2_000L) {
  376. onExitApp()
  377. } else {
  378. lastBackPressedAt = now
  379. ToastCenter.show("未登录,双击返回退出应用")
  380. }
  381. }
  382. Scaffold(
  383. topBar = { TopAppBar(title = { Text("Sample Host") }) },
  384. ) { innerPadding ->
  385. pendingAppUpdate?.let { updateInfo ->
  386. AlertDialog(
  387. onDismissRequest = onDismissAppUpdate,
  388. title = { Text(updateInfo.title) },
  389. text = {
  390. Text(
  391. "发现新版本 ${updateInfo.versionName}\n\n${updateInfo.changelog.ifBlank { "检测到可用更新,是否立即下载?" }}",
  392. )
  393. },
  394. confirmButton = {
  395. TextButton(onClick = onConfirmAppUpdate) {
  396. Text("立即更新")
  397. }
  398. },
  399. dismissButton = {
  400. TextButton(onClick = onDismissAppUpdate) {
  401. Text("稍后再说")
  402. }
  403. },
  404. )
  405. }
  406. LazyColumn(
  407. modifier = Modifier
  408. .fillMaxSize()
  409. .padding(innerPadding),
  410. contentPadding = PaddingValues(16.dp),
  411. verticalArrangement = Arrangement.spacedBy(16.dp),
  412. ) {
  413. item {
  414. Card(modifier = Modifier.fillMaxWidth()) {
  415. Column(
  416. modifier = Modifier.padding(16.dp),
  417. verticalArrangement = Arrangement.spacedBy(8.dp),
  418. ) {
  419. Text("当前时间: ${DateTimeUtils.now()}")
  420. Text("登录状态: ${session?.phone ?: "未登录"}")
  421. Text("插件安装状态: ${if (pluginInstalled) "已安装" else "未安装或当前宿主不可见"}")
  422. Text("插件下载: ${pluginDownloadState.toDisplayText()}")
  423. Text("应用下载: ${appDownloadState.toDisplayText()}")
  424. Button(onClick = onOpenLogin, modifier = Modifier.fillMaxWidth()) {
  425. Text(if (session == null) "打开登录页" else "重新登录")
  426. }
  427. Button(
  428. onClick = onOpenPlugin,
  429. enabled = session != null,
  430. modifier = Modifier.fillMaxWidth(),
  431. ) {
  432. Text("启动 plugin-ui")
  433. }
  434. Button(onClick = onInstallPlugin, modifier = Modifier.fillMaxWidth()) {
  435. Text("下载并安装 plugin-ui")
  436. }
  437. if (pluginDownloadState is DownloadState.Starting || pluginDownloadState is DownloadState.Progress) {
  438. Button(onClick = onCancelPluginDownload, modifier = Modifier.fillMaxWidth()) {
  439. Text("取消插件下载")
  440. }
  441. }
  442. Button(onClick = onUpdateApp, modifier = Modifier.fillMaxWidth()) {
  443. Text("检查 App 更新")
  444. }
  445. if (appDownloadState is DownloadState.Starting || appDownloadState is DownloadState.Progress) {
  446. Button(onClick = onCancelAppDownload, modifier = Modifier.fillMaxWidth()) {
  447. Text("取消应用下载")
  448. }
  449. }
  450. }
  451. }
  452. }
  453. item {
  454. AccordionGroup(title = "当前方案", initiallyExpanded = true) {
  455. Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
  456. Text("1. 未登录启动时直接进入 lib-szyx 登录页")
  457. Text("2. 插件和应用更新都在应用内直接下载,并输出实时进度")
  458. Text("3. plugin-ui 通过宿主共享缓存拿到 sessionId 和 userId")
  459. }
  460. }
  461. }
  462. item {
  463. FeatureCard(
  464. title = "插件结构",
  465. description = "宿主 sample-app + 业务插件 plugin-ui,二者共享 commonsdk-core / commonsdk-compose / lib-szyx。",
  466. )
  467. }
  468. }
  469. }
  470. }
  471. private fun DownloadState.toDisplayText(): String {
  472. return when (this) {
  473. DownloadState.Idle -> "未开始"
  474. DownloadState.Starting -> "准备下载"
  475. is DownloadState.Progress -> {
  476. val progressText = if (progress >= 0) "$progress%" else "未知进度"
  477. "$progressText (${downloadedBytes}/${totalBytes.coerceAtLeast(0)})"
  478. }
  479. is DownloadState.Success -> "已完成: ${file.name}"
  480. DownloadState.Cancelled -> "已取消"
  481. is DownloadState.Error -> "失败: $message"
  482. }
  483. }