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(null) private val pluginInstalledState = mutableStateOf(false) private val pluginDownloadState = mutableStateOf(DownloadState.Idle) private val appDownloadState = mutableStateOf(DownloadState.Idle) private val pendingAppUpdateState = mutableStateOf(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" } }