| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- package com.xuqm.sample
- import android.content.BroadcastReceiver
- import android.content.Context
- import android.content.Intent
- import android.content.IntentFilter
- import android.os.Bundle
- import androidx.activity.ComponentActivity
- import androidx.activity.compose.BackHandler
- import androidx.activity.compose.setContent
- import androidx.activity.result.contract.ActivityResultContracts
- import androidx.core.content.ContextCompat
- import androidx.compose.foundation.layout.Arrangement
- import androidx.compose.foundation.layout.Column
- import androidx.compose.foundation.layout.PaddingValues
- import androidx.compose.foundation.layout.fillMaxSize
- import androidx.compose.foundation.layout.fillMaxWidth
- import androidx.compose.foundation.layout.padding
- import androidx.compose.foundation.lazy.LazyColumn
- import androidx.compose.material3.Button
- import androidx.compose.material3.Card
- import androidx.compose.material3.ExperimentalMaterial3Api
- import androidx.compose.material3.AlertDialog
- import androidx.compose.material3.MaterialTheme
- import androidx.compose.material3.Scaffold
- import androidx.compose.material3.Text
- import androidx.compose.material3.TopAppBar
- import androidx.compose.material3.TextButton
- import androidx.compose.runtime.Composable
- import androidx.compose.runtime.DisposableEffect
- import androidx.compose.runtime.getValue
- import androidx.compose.runtime.mutableLongStateOf
- import androidx.compose.runtime.mutableStateOf
- import androidx.compose.runtime.remember
- import androidx.compose.runtime.setValue
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.unit.dp
- import androidx.lifecycle.Lifecycle
- import androidx.lifecycle.LifecycleEventObserver
- import androidx.lifecycle.compose.LocalLifecycleOwner
- import androidx.lifecycle.lifecycleScope
- import com.xuqm.sdk.CoreSDK
- import com.xuqm.sdk.compose.components.AccordionGroup
- import com.xuqm.sdk.compose.components.FeatureCard
- import com.xuqm.sdk.plugin.PluginPackageManager
- import com.xuqm.sdk.ui.ToastCenter
- import com.xuqm.sdk.update.DownloadState
- import com.xuqm.sdk.update.StoragePath
- import com.xuqm.sdk.update.UpdateInfo
- import com.xuqm.sdk.update.VersionCheckResult
- import com.xuqm.sdk.update.VersionCheckStrategy
- import com.xuqm.sdk.utils.DateTimeUtils
- import com.xuqm.sample.update.UpdateRepository
- import com.xuqm.szyx.SzyxSDK
- import com.xuqm.szyx.auth.LoginSession
- import com.xuqm.szyx.auth.UserSessionManager
- import com.xuqm.szyx.login.SzyxLoginActivity
- import kotlinx.coroutines.Job
- import kotlinx.coroutines.launch
- class MainActivity : ComponentActivity() {
- companion object {
- private const val PLUGIN_PACKAGE_NAME = "com.xuqm.plugin.ui"
- private const val PLUGIN_ENTRY_ACTIVITY = "com.xuqm.plugin.ui.PluginUiActivity"
- }
- private val sessionState = mutableStateOf<LoginSession?>(null)
- private val pluginInstalledState = mutableStateOf(false)
- private val pluginDownloadState = mutableStateOf<DownloadState>(DownloadState.Idle)
- private val appDownloadState = mutableStateOf<DownloadState>(DownloadState.Idle)
- private val pendingAppUpdateState = mutableStateOf<UpdateInfo?>(null)
- private var pluginDownloadTaskId: String? = null
- private var appDownloadTaskId: String? = null
- private var pluginDownloadJob: Job? = null
- private var appDownloadJob: Job? = null
- private var loginPromptedOnLaunch = false
- private var reloadPluginAfterInstall = false
- private val updateRepository by lazy { UpdateRepository(BuildConfig.UPDATE_SERVER_BASE_URL) }
- private var currentPluginUpdateInfo: PluginPackageManager.PluginUpdateInfo? = null
- private val packageChangedReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- val packageName = intent?.data?.schemeSpecificPart ?: return
- if (packageName == PLUGIN_PACKAGE_NAME) {
- refreshState()
- ToastCenter.show("plugin-ui 安装状态已更新")
- if (intent.action == Intent.ACTION_PACKAGE_ADDED && reloadPluginAfterInstall) {
- reloadPluginAfterInstall = false
- val pluginUpdateInfo = currentPluginUpdateInfo
- CoreSDK.pluginPackageManager().reloadPlugin(
- packageName = PLUGIN_PACKAGE_NAME,
- entryActivity = pluginUpdateInfo?.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
- extras = pluginUpdateInfo?.extras ?: mapOf("hostPackageName" to this@MainActivity.packageName),
- )
- }
- }
- }
- }
- private val loginLauncher =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- refreshSession()
- }
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- CoreSDK.init(this, CoreSDK.SDKConfig(debugMode = true))
- SzyxSDK.init(
- this,
- SzyxSDK.Config(
- baseUrl = "https://dev.51trust.com/",
- clientId = "2000111111110002",
- hostAppPackageName = packageName,
- debugMode = true,
- ),
- )
- ToastCenter.init(this)
- refreshState()
- ensureLoginOnLaunch()
- registerPackageChangeReceiver()
- setContent {
- MaterialTheme {
- SampleHome(
- session = sessionState.value,
- pluginInstalled = pluginInstalledState.value,
- pluginDownloadState = pluginDownloadState.value,
- appDownloadState = appDownloadState.value,
- pendingAppUpdate = pendingAppUpdateState.value,
- onOpenLogin = ::openLogin,
- onOpenPlugin = ::openPlugin,
- onInstallPlugin = ::downloadPlugin,
- onCancelPluginDownload = ::cancelPluginDownload,
- onUpdateApp = ::downloadApp,
- onConfirmAppUpdate = ::confirmDownloadAppUpdate,
- onDismissAppUpdate = ::dismissAppUpdateDialog,
- onCancelAppDownload = ::cancelAppDownload,
- onRetryCheckPlugin = ::refreshState,
- onExitApp = { finish() },
- )
- }
- }
- }
- override fun onDestroy() {
- pluginDownloadJob?.cancel()
- appDownloadJob?.cancel()
- runCatching { unregisterReceiver(packageChangedReceiver) }
- super.onDestroy()
- }
- override fun onResume() {
- super.onResume()
- refreshState()
- }
- private fun ensureLoginOnLaunch() {
- if (sessionState.value == null && !loginPromptedOnLaunch) {
- loginPromptedOnLaunch = true
- openLogin()
- }
- }
- private fun refreshSession() {
- sessionState.value = UserSessionManager.getSession()
- }
- private fun refreshState() {
- refreshSession()
- pluginInstalledState.value = CoreSDK.pluginPackageManager().isPluginInstalled(PLUGIN_PACKAGE_NAME)
- }
- private fun openLogin() {
- loginLauncher.launch(Intent(this, SzyxLoginActivity::class.java))
- }
- private fun registerPackageChangeReceiver() {
- val filter = IntentFilter().apply {
- addAction(Intent.ACTION_PACKAGE_ADDED)
- addAction(Intent.ACTION_PACKAGE_REMOVED)
- addDataScheme("package")
- }
- ContextCompat.registerReceiver(
- this,
- packageChangedReceiver,
- filter,
- ContextCompat.RECEIVER_NOT_EXPORTED,
- )
- }
- private fun openPlugin() {
- val session = sessionState.value
- if (session == null) {
- ToastCenter.show("请先登录")
- openLogin()
- return
- }
- val pluginManager = CoreSDK.pluginPackageManager()
- pluginManager.cacheCurrentUser(
- userId = session.loginModel.userId,
- sessionId = session.loginModel.sessionId,
- clientId = SzyxSDK.requireConfig().clientId,
- extraData = mapOf("phone" to session.phone),
- )
- val launched = pluginManager.startPlugin(
- packageName = PLUGIN_PACKAGE_NAME,
- entryActivity = PLUGIN_ENTRY_ACTIVITY,
- extras = mapOf("hostPackageName" to packageName),
- )
- if (!launched) {
- ToastCenter.show("未检测到已安装的 plugin-ui,请先下载安装")
- }
- }
- private fun downloadPlugin() {
- lifecycleScope.launch {
- updateRepository.fetchLatestPluginUpdate(PLUGIN_PACKAGE_NAME)
- .onSuccess { remoteUpdate ->
- val pluginUpdateInfo = remoteUpdate.copy(
- entryActivity = remoteUpdate.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
- extras = mapOf("hostPackageName" to packageName),
- )
- val checkResult = CoreSDK.pluginPackageManager().checkPluginUpdate(
- packageName = pluginUpdateInfo.packageName,
- remoteVersionCode = pluginUpdateInfo.versionCode,
- remoteVersionName = pluginUpdateInfo.versionName,
- strategy = VersionCheckStrategy.VERSION_CODE_OR_NAME,
- )
- if (checkResult is VersionCheckResult.UpToDate) {
- ToastCenter.show("当前插件已是最新版本 ${checkResult.current.versionName}(${checkResult.current.versionCode})")
- return@onSuccess
- }
- currentPluginUpdateInfo = pluginUpdateInfo
- val taskId = CoreSDK.pluginPackageManager().downloadPlugin(
- updateInfo = pluginUpdateInfo,
- fileName = "plugin-ui-release.apk",
- storagePath = StoragePath.EXTERNAL_FILES,
- )
- pluginDownloadTaskId = taskId
- observePluginDownload(taskId)
- }
- .onFailure {
- ToastCenter.show(it.message ?: "获取插件更新配置失败")
- }
- }
- }
- private fun cancelPluginDownload() {
- val taskId = pluginDownloadTaskId ?: return
- CoreSDK.downloadManager().cancel(taskId)
- }
- private fun downloadApp() {
- lifecycleScope.launch {
- updateRepository.fetchLatestAppUpdate(packageName)
- .onSuccess { updateInfo ->
- when (
- val checkResult = CoreSDK.appUpdater().checkUpdate(
- updateInfo = updateInfo,
- strategy = VersionCheckStrategy.VERSION_CODE_OR_NAME,
- )
- ) {
- is VersionCheckResult.NeedUpdate -> {
- pendingAppUpdateState.value = updateInfo
- }
- is VersionCheckResult.UpToDate -> {
- ToastCenter.show("当前已是最新版本 ${checkResult.current.versionName}(${checkResult.current.versionCode})")
- }
- }
- }
- .onFailure {
- ToastCenter.show(it.message ?: "获取 App 更新配置失败")
- }
- }
- }
- private fun confirmDownloadAppUpdate() {
- val updateInfo = pendingAppUpdateState.value ?: return
- pendingAppUpdateState.value = null
- val taskId = CoreSDK.appUpdater().downloadUpdate(
- updateInfo = updateInfo,
- fileName = "sample-app-update.apk",
- storagePath = StoragePath.EXTERNAL_FILES,
- )
- appDownloadTaskId = taskId
- observeAppDownload(taskId)
- }
- private fun cancelAppDownload() {
- val taskId = appDownloadTaskId ?: return
- CoreSDK.downloadManager().cancel(taskId)
- }
- private fun dismissAppUpdateDialog() {
- pendingAppUpdateState.value = null
- }
- private fun observePluginDownload(taskId: String) {
- pluginDownloadJob?.cancel()
- pluginDownloadJob = lifecycleScope.launch {
- CoreSDK.downloadManager().observe(taskId)?.collect { state ->
- pluginDownloadState.value = state
- when (state) {
- is DownloadState.Success -> {
- ToastCenter.show("插件下载完成,准备重新加载")
- reloadPluginAfterInstall = true
- if (!CoreSDK.pluginPackageManager().loadPlugin(state.file)) {
- reloadPluginAfterInstall = false
- ToastCenter.show("插件加载拉起失败")
- }
- CoreSDK.downloadManager().clear(taskId)
- pluginDownloadTaskId = null
- pluginDownloadJob?.cancel()
- }
- is DownloadState.Error -> {
- ToastCenter.show("插件下载失败: ${state.message}")
- CoreSDK.downloadManager().clear(taskId)
- pluginDownloadTaskId = null
- pluginDownloadJob?.cancel()
- }
- DownloadState.Cancelled -> {
- ToastCenter.show("插件下载已取消")
- CoreSDK.downloadManager().clear(taskId)
- pluginDownloadTaskId = null
- pluginDownloadJob?.cancel()
- }
- else -> Unit
- }
- }
- }
- }
- private fun observeAppDownload(taskId: String) {
- appDownloadJob?.cancel()
- appDownloadJob = lifecycleScope.launch {
- CoreSDK.downloadManager().observe(taskId)?.collect { state ->
- appDownloadState.value = state
- when (state) {
- is DownloadState.Success -> {
- ToastCenter.show("安装包下载完成,准备安装")
- if (!CoreSDK.appUpdater().installApk(state.file)) {
- ToastCenter.show("应用安装拉起失败")
- }
- CoreSDK.downloadManager().clear(taskId)
- appDownloadTaskId = null
- appDownloadJob?.cancel()
- }
- is DownloadState.Error -> {
- ToastCenter.show("应用下载失败: ${state.message}")
- CoreSDK.downloadManager().clear(taskId)
- appDownloadTaskId = null
- appDownloadJob?.cancel()
- }
- DownloadState.Cancelled -> {
- ToastCenter.show("应用下载已取消")
- CoreSDK.downloadManager().clear(taskId)
- appDownloadTaskId = null
- appDownloadJob?.cancel()
- }
- else -> Unit
- }
- }
- }
- }
- }
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- private fun SampleHome(
- session: LoginSession?,
- pluginInstalled: Boolean,
- pluginDownloadState: DownloadState,
- appDownloadState: DownloadState,
- pendingAppUpdate: UpdateInfo?,
- onOpenLogin: () -> Unit,
- onOpenPlugin: () -> Unit,
- onInstallPlugin: () -> Unit,
- onCancelPluginDownload: () -> Unit,
- onUpdateApp: () -> Unit,
- onConfirmAppUpdate: () -> Unit,
- onDismissAppUpdate: () -> Unit,
- onCancelAppDownload: () -> Unit,
- onRetryCheckPlugin: () -> Unit,
- onExitApp: () -> Unit,
- ) {
- val lifecycleOwner = LocalLifecycleOwner.current
- var lastBackPressedAt by remember { mutableLongStateOf(0L) }
- DisposableEffect(lifecycleOwner) {
- val observer = LifecycleEventObserver { _, event ->
- if (event == Lifecycle.Event.ON_RESUME) {
- onRetryCheckPlugin()
- }
- }
- lifecycleOwner.lifecycle.addObserver(observer)
- onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
- }
- BackHandler(enabled = session == null) {
- val now = System.currentTimeMillis()
- if (now - lastBackPressedAt < 2_000L) {
- onExitApp()
- } else {
- lastBackPressedAt = now
- ToastCenter.show("未登录,双击返回退出应用")
- }
- }
- Scaffold(
- topBar = { TopAppBar(title = { Text("Sample Host") }) },
- ) { innerPadding ->
- pendingAppUpdate?.let { updateInfo ->
- AlertDialog(
- onDismissRequest = onDismissAppUpdate,
- title = { Text(updateInfo.title) },
- text = {
- Text(
- "发现新版本 ${updateInfo.versionName}\n\n${updateInfo.changelog.ifBlank { "检测到可用更新,是否立即下载?" }}",
- )
- },
- confirmButton = {
- TextButton(onClick = onConfirmAppUpdate) {
- Text("立即更新")
- }
- },
- dismissButton = {
- TextButton(onClick = onDismissAppUpdate) {
- Text("稍后再说")
- }
- },
- )
- }
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(innerPadding),
- contentPadding = PaddingValues(16.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- item {
- Card(modifier = Modifier.fillMaxWidth()) {
- Column(
- modifier = Modifier.padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- Text("当前时间: ${DateTimeUtils.now()}")
- Text("登录状态: ${session?.phone ?: "未登录"}")
- Text("插件安装状态: ${if (pluginInstalled) "已安装" else "未安装或当前宿主不可见"}")
- Text("插件下载: ${pluginDownloadState.toDisplayText()}")
- Text("应用下载: ${appDownloadState.toDisplayText()}")
- Button(onClick = onOpenLogin, modifier = Modifier.fillMaxWidth()) {
- Text(if (session == null) "打开登录页" else "重新登录")
- }
- Button(
- onClick = onOpenPlugin,
- enabled = session != null,
- modifier = Modifier.fillMaxWidth(),
- ) {
- Text("启动 plugin-ui")
- }
- Button(onClick = onInstallPlugin, modifier = Modifier.fillMaxWidth()) {
- Text("下载并安装 plugin-ui")
- }
- if (pluginDownloadState is DownloadState.Starting || pluginDownloadState is DownloadState.Progress) {
- Button(onClick = onCancelPluginDownload, modifier = Modifier.fillMaxWidth()) {
- Text("取消插件下载")
- }
- }
- Button(onClick = onUpdateApp, modifier = Modifier.fillMaxWidth()) {
- Text("检查 App 更新")
- }
- if (appDownloadState is DownloadState.Starting || appDownloadState is DownloadState.Progress) {
- Button(onClick = onCancelAppDownload, modifier = Modifier.fillMaxWidth()) {
- Text("取消应用下载")
- }
- }
- }
- }
- }
- item {
- AccordionGroup(title = "当前方案", initiallyExpanded = true) {
- Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- Text("1. 未登录启动时直接进入 lib-szyx 登录页")
- Text("2. 插件和应用更新都在应用内直接下载,并输出实时进度")
- Text("3. plugin-ui 通过宿主共享缓存拿到 sessionId 和 userId")
- }
- }
- }
- item {
- FeatureCard(
- title = "插件结构",
- description = "宿主 sample-app + 业务插件 plugin-ui,二者共享 commonsdk-core / commonsdk-compose / lib-szyx。",
- )
- }
- }
- }
- }
- private fun DownloadState.toDisplayText(): String {
- return when (this) {
- DownloadState.Idle -> "未开始"
- DownloadState.Starting -> "准备下载"
- is DownloadState.Progress -> {
- val progressText = if (progress >= 0) "$progress%" else "未知进度"
- "$progressText (${downloadedBytes}/${totalBytes.coerceAtLeast(0)})"
- }
- is DownloadState.Success -> "已完成: ${file.name}"
- DownloadState.Cancelled -> "已取消"
- is DownloadState.Error -> "失败: $message"
- }
- }
|