package expo.modules.updates import android.app.Activity import android.content.Context 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.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 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 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 { /** 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 selectionPolicy: SelectionPolicy get() = SelectionPolicyFactory.createFilterAwarePolicy(updatesConfiguration.getRuntimeVersion(), updatesConfiguration) 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(), updatesConfiguration, logger) 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 val startupProcedure = StartupProcedure( context, updatesConfiguration, databaseHolder, updatesDirectory, fileDownloader, selectionPolicy, logger, object : StartupProcedure.StartupProcedureCallback { override fun onFinished() { onStartupProcedureFinished() } override fun onRequestRelaunch(shouldRunReaper: Boolean, callback: LauncherCallback) { relaunchReactApplication(shouldRunReaper, callback) } }, controllerScope ) 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() if (!updatesConfiguration.hasUpdatesOverride) { BuildData.ensureBuildDataIsConsistent(updatesConfiguration, databaseHolder.database) } stateMachine.queueExecution(startupProcedure) } private fun relaunchReactApplication(shouldRunReaper: Boolean, callback: LauncherCallback) { val procedure = RelaunchProcedure( context, weakActivity, updatesConfiguration, logger, databaseHolder, updatesDirectory, fileDownloader, selectionPolicy, getCurrentLauncher = { startupProcedure.launcher!! }, setCurrentLauncher = { currentLauncher -> startupProcedure.setLauncher(currentLauncher) }, shouldRunReaper = shouldRunReaper, reloadScreenManager = reloadScreenManager, callback, controllerScope ) stateMachine.queueExecution(procedure) } override fun getConstantsForModule(): IUpdatesController.UpdatesModuleConstants { return IUpdatesController.UpdatesModuleConstants( launchedUpdate = launchedUpdate, launchDuration = launchDuration, embeddedUpdate = EmbeddedManifestUtils.getEmbeddedUpdate(context, updatesConfiguration)?.updateEntity, emergencyLaunchException = startupProcedure.emergencyLaunchException, isEnabled = true, isUsingEmbeddedAssets = isUsingEmbeddedAssets, runtimeVersion = updatesConfiguration.runtimeVersionRaw, checkOnLaunch = updatesConfiguration.checkOnLaunch, requestHeaders = updatesConfiguration.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 procedure = CheckForUpdateProcedure(context, updatesConfiguration, databaseHolder, logger, fileDownloader, selectionPolicy, launchedUpdate) { continuation.resume(it) } stateMachine.queueExecution(procedure) } override suspend fun fetchUpdate() = suspendCancellableCoroutine { continuation -> val procedure = FetchUpdateProcedure(context, updatesConfiguration, logger, databaseHolder, updatesDirectory, fileDownloader, selectionPolicy, launchedUpdate) { continuation.resume(it) } stateMachine.queueExecution(procedure) } override suspend fun getExtraParams() = suspendCancellableCoroutine { continuation -> controllerScope.launch { try { val result = ManifestMetadata.getExtraParams( databaseHolder.database, updatesConfiguration ) 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 -> controllerScope.launch { runCatching { ManifestMetadata.setExtraParam( databaseHolder.database, updatesConfiguration, 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) 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) updatesConfiguration = UpdatesConfiguration.create(context, updatesConfiguration, configOverride) } companion object { private val TAG = EnabledUpdatesController::class.java.simpleName } }