package expo.modules.updates import android.app.Activity import android.content.Context import android.net.Uri import android.os.Bundle import com.facebook.react.bridge.ReactContext import com.facebook.react.devsupport.interfaces.DevSupportManager import expo.modules.easclient.EASClientID import expo.modules.kotlin.exception.CodedException import expo.modules.kotlin.exception.toCodedException import expo.modules.updates.db.BuildData import expo.modules.updates.db.DatabaseHolder import expo.modules.updates.db.UpdatesDatabase import expo.modules.updates.db.entity.UpdateEntity import expo.modules.updates.events.IUpdatesEventManager import expo.modules.updates.events.UpdatesEventManager import expo.modules.updates.launcher.Launcher.LauncherCallback import expo.modules.updates.loader.FileDownloader import expo.modules.updates.logging.UpdatesErrorCode import expo.modules.updates.logging.UpdatesLogReader import expo.modules.updates.logging.UpdatesLogger import expo.modules.updates.manifest.EmbeddedManifestUtils import expo.modules.updates.manifest.ManifestMetadata import expo.modules.updates.procedures.CheckForUpdateProcedure import expo.modules.updates.procedures.FetchUpdateProcedure import expo.modules.updates.procedures.RelaunchProcedure import expo.modules.updates.procedures.StartupProcedure import expo.modules.updates.reloadscreen.ReloadScreenManager import expo.modules.updates.selectionpolicy.SelectionPolicy import expo.modules.updates.selectionpolicy.SelectionPolicyFactory import expo.modules.updates.statemachine.UpdatesStateMachine import expo.modules.updates.statemachine.UpdatesStateValue import expo.modules.updatesinterface.UpdatesMetricsInterface import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.File import java.lang.ref.WeakReference import java.util.UUID import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.time.DurationUnit import kotlin.time.toDuration /** * Updates controller for applications that have updates enabled and properly-configured. */ class EnabledUpdatesController( private val context: Context, private var updatesConfiguration: UpdatesConfiguration, override val updatesDirectory: File ) : IUpdatesController, UpdatesMetricsInterface { /** Keep the activity for [RelaunchProcedure] to relaunch the app. */ private var weakActivity: WeakReference? = null private val logger = UpdatesLogger(context.filesDir) override val eventManager: IUpdatesEventManager = UpdatesEventManager(logger) private val persistedOverride = UpdatesConfigurationOverride.load(context) private val hasPersistedOverride: Boolean by lazy { persistedOverride != null && updatesConfiguration.disableAntiBrickingMeasures } private var hasConfigOverride = hasPersistedOverride private val selectionPolicy: SelectionPolicy get() = resolveSelectionPolicy() private val controllerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val stateMachine = UpdatesStateMachine(logger, eventManager, UpdatesStateValue.entries.toSet(), controllerScope) private val fileDownloader: FileDownloader get() = FileDownloader( context.filesDir, EASClientID(context).uuid.toString(), resolveConfiguration(), logger, databaseHolder.database ) private val databaseHolder = DatabaseHolder(UpdatesDatabase.getInstance(context, Dispatchers.IO)) private val startupFinishedDeferred = CompletableDeferred() private val startupFinishedMutex = Mutex() override val reloadScreenManager = ReloadScreenManager() private fun purgeUpdatesLogsOlderThanOneDay() { UpdatesLogReader(context.filesDir).purgeLogEntries { if (it != null) { logger.error("UpdatesLogReader: error in purgeLogEntries", it, UpdatesErrorCode.Unknown) } } } private var isStarted = false private var isStartupFinished = false private var startupStartTimeMillis: Long? = null private var startupEndTimeMillis: Long? = null @Synchronized private fun onStartupProcedureFinished() { controllerScope.launch { startupFinishedMutex.withLock { if (!startupFinishedDeferred.isCompleted) { startupFinishedDeferred.complete(Unit) } } } isStartupFinished = true startupEndTimeMillis = System.currentTimeMillis() } private fun resolveConfiguration(): UpdatesConfiguration { if (!hasPersistedOverride || persistedOverride == null) { return updatesConfiguration } if (updatesConfiguration.hasUpdatesOverride) { return updatesConfiguration } return UpdatesConfiguration.create(context, updatesConfiguration, persistedOverride) } private fun resolveSelectionPolicy(): SelectionPolicy { val resolvedConfig = resolveConfiguration() return SelectionPolicyFactory.createFilterAwarePolicy( resolvedConfig.getRuntimeVersion(), resolvedConfig, filterByChannel = hasConfigOverride ) } private fun createStartupProcedure(): StartupProcedure { val resolvedConfig = resolveConfiguration() return StartupProcedure( context, resolvedConfig, databaseHolder, updatesDirectory, fileDownloader, resolveSelectionPolicy(), logger, object : StartupProcedure.StartupProcedureCallback { override fun onFinished() { onStartupProcedureFinished() } override fun onRequestRelaunch(shouldRunReaper: Boolean, callback: LauncherCallback) { relaunchReactApplication(shouldRunReaper, callback) } }, controllerScope ) } private val startupProcedure by lazy { createStartupProcedure() } private val launchedUpdate get() = startupProcedure.launchedUpdate private val launchDuration get() = startupStartTimeMillis?.let { start -> startupEndTimeMillis?.let { end -> (end - start).toDuration(DurationUnit.MILLISECONDS) } } private val isUsingEmbeddedAssets get() = startupProcedure.isUsingEmbeddedAssets private val localAssetFiles get() = startupProcedure.localAssetFiles override val launchAssetFile: String? get() { runBlocking { startupFinishedDeferred.await() } return startupProcedure.launchAssetFile } override val bundleAssetName: String? get() = startupProcedure.bundleAssetName override fun onEventListenerStartObserving() { stateMachine.sendContextToJS() } override fun onDidCreateDevSupportManager(devSupportManager: DevSupportManager) { startupProcedure.onDidCreateDevSupportManager(devSupportManager) } override fun onDidCreateReactInstance(reactContext: ReactContext) { weakActivity = WeakReference(reactContext.currentActivity) } override fun onReactInstanceException(exception: Exception) { startupProcedure.onReactInstanceException(exception) } override val isActiveController = true @Synchronized override fun start() { if (isStarted) { return } isStarted = true startupStartTimeMillis = System.currentTimeMillis() purgeUpdatesLogsOlderThanOneDay() val resolvedConfig = resolveConfiguration() if (!resolvedConfig.hasUpdatesOverride) { BuildData.ensureBuildDataIsConsistent(resolvedConfig, databaseHolder.database) } stateMachine.queueExecution(startupProcedure) } private fun relaunchReactApplication(shouldRunReaper: Boolean, callback: LauncherCallback) { val resolvedConfig = resolveConfiguration() val procedure = RelaunchProcedure( context, weakActivity, resolvedConfig, logger, databaseHolder, updatesDirectory, fileDownloader, resolveSelectionPolicy(), getCurrentLauncher = { startupProcedure.launcher!! }, setCurrentLauncher = { currentLauncher -> startupProcedure.setLauncher(currentLauncher) }, shouldRunReaper = shouldRunReaper, reloadScreenManager = reloadScreenManager, callback, controllerScope ) stateMachine.queueExecution(procedure) } private fun getEmbeddedUpdate(): UpdateEntity? { val resolvedConfig = resolveConfiguration() return EmbeddedManifestUtils.getEmbeddedUpdate(context, resolvedConfig)?.updateEntity } override fun getConstantsForModule(): IUpdatesController.UpdatesModuleConstants { val resolvedConfig = resolveConfiguration() return IUpdatesController.UpdatesModuleConstants( launchedUpdate = launchedUpdate, launchDuration = launchDuration, embeddedUpdate = getEmbeddedUpdate(), emergencyLaunchException = startupProcedure.emergencyLaunchException, isEnabled = true, isUsingEmbeddedAssets = isUsingEmbeddedAssets, runtimeVersion = resolvedConfig.runtimeVersionRaw, checkOnLaunch = resolvedConfig.checkOnLaunch, requestHeaders = resolvedConfig.requestHeaders, localAssetFiles = localAssetFiles, shouldDeferToNativeForAPIMethodAvailabilityInDevelopment = false, initialContext = stateMachine.context ) } override suspend fun relaunchReactApplicationForModule() = suspendCancellableCoroutine { continuation -> val canRelaunch = launchedUpdate != null if (!canRelaunch) { continuation.resumeWithException(object : CodedException("ERR_UPDATES_RELOAD", "Cannot relaunch without a launched update.", null) {}) } else { relaunchReactApplication( shouldRunReaper = true, object : LauncherCallback { override fun onFailure(e: Exception) { continuation.resumeWithException(e.toCodedException()) } override fun onSuccess() { continuation.resume(Unit) } } ) } } override suspend fun checkForUpdate() = suspendCancellableCoroutine { continuation -> val resolvedConfig = resolveConfiguration() val resolvedSelectionPolicy = resolveSelectionPolicy() val procedure = CheckForUpdateProcedure(context, resolvedConfig, databaseHolder, logger, fileDownloader, resolvedSelectionPolicy, launchedUpdate) { continuation.resume(it) } stateMachine.queueExecution(procedure) } override suspend fun fetchUpdate() = suspendCancellableCoroutine { continuation -> val resolvedConfig = resolveConfiguration() val resolvedSelectionPolicy = resolveSelectionPolicy() val procedure = FetchUpdateProcedure(context, resolvedConfig, logger, databaseHolder, updatesDirectory, fileDownloader, resolvedSelectionPolicy, launchedUpdate) { continuation.resume(it) } stateMachine.queueExecution(procedure) } override suspend fun getExtraParams() = suspendCancellableCoroutine { continuation -> val resolvedConfig = resolveConfiguration() controllerScope.launch { try { val result = ManifestMetadata.getExtraParams( databaseHolder.database, resolvedConfig ) val resultMap = when (result) { null -> Bundle() else -> { Bundle().apply { result.forEach { putString(it.key, it.value) } } } } continuation.resume(resultMap) } catch (e: Exception) { continuation.resumeWithException(e.toCodedException()) } } } override suspend fun setExtraParam(key: String, value: String?) = suspendCancellableCoroutine { continuation -> val resolvedConfig = resolveConfiguration() controllerScope.launch { runCatching { ManifestMetadata.setExtraParam( databaseHolder.database, resolvedConfig, key, value ) continuation.resume(Unit) }.onFailure { e -> continuation.resumeWithException(e.toCodedException()) } } } override fun shutdown() { controllerScope.cancel() } override fun setUpdateURLAndRequestHeadersOverride(configOverride: UpdatesConfigurationOverride?) { if (!updatesConfiguration.disableAntiBrickingMeasures) { throw CodedException("ERR_UPDATES_RUNTIME_OVERRIDE", "Must set disableAntiBrickingMeasures configuration to use updates overriding", null) } UpdatesConfigurationOverride.save(context, configOverride) hasConfigOverride = true updatesConfiguration = UpdatesConfiguration.create(context, updatesConfiguration, configOverride) } override fun setUpdateRequestHeadersOverride(requestHeaders: Map?) { val isValidRequestHeaders = UpdatesConfiguration.isValidRequestHeadersOverride( updatesConfiguration.originalEmbeddedRequestHeaders, requestHeaders ) if (!isValidRequestHeaders) { throw CodedException("ERR_UPDATES_RUNTIME_OVERRIDE", "Invalid update requestHeaders override: $requestHeaders", null) } val configOverride = UpdatesConfigurationOverride.saveRequestHeaders(context, requestHeaders) hasConfigOverride = true updatesConfiguration = UpdatesConfiguration.create(context, updatesConfiguration, configOverride) } // UpdatesMetricsInterface implementations override val runtimeVersion: String? get() = updatesConfiguration.runtimeVersionRaw override val updateUrl: Uri? get() = updatesConfiguration.updateUrl override val launchedUpdateId: UUID? get() = startupProcedure.launchedUpdate?.id override val embeddedUpdateId: UUID? get() = getEmbeddedUpdate()?.id companion object { private val TAG = EnabledUpdatesController::class.java.simpleName } }