package app.pulse.updates.errorrecovery import android.os.Handler import android.os.Looper import android.os.Message import app.pulse.updates.pulseLog import app.pulse.updates.pulseLogError /** * Keeps track of and executes tasks in the error recovery pipeline. The pipeline allows us to * predictably and serially respond to unpredictably ordered events. * * This error recovery flow is intended to be lightweight and is *not* a full safety net whose * purpose is to avoid crashes at all costs. Rather, its primary purpose is to prevent bad updates * from "bricking" an app by causing crashes before there is ever a chance to download a fix. * * When an error is caught, the pipeline is started and executes the following tasks serially: * (a) check for a new update and start downloading if there is one * (b) if there is a new update, reload and launch the new update * (c) if not, or if another error occurs, fall back to an older working update (if one exists) * (d) crash. */ internal class PulseErrorRecoveryHandler( looper: Looper, private val delegate: PulseErrorRecoveryDelegate ) : Handler(looper) { private val pipeline = arrayListOf( Task.WAIT_FOR_REMOTE_UPDATE, Task.LAUNCH_NEW_UPDATE, Task.LAUNCH_CACHED_UPDATE, Task.CRASH ) private var isPipelineRunning = false private var isWaitingForRemoteUpdate = false private var hasContentAppeared = false private val encounteredErrors = ArrayList() object MessageType { const val EXCEPTION_ENCOUNTERED = 0 const val CONTENT_APPEARED = 1 const val REMOTE_LOAD_STATUS_CHANGED = 2 } private enum class Task { WAIT_FOR_REMOTE_UPDATE, LAUNCH_NEW_UPDATE, LAUNCH_CACHED_UPDATE, CRASH } override fun handleMessage(msg: Message) { when (msg.what) { MessageType.EXCEPTION_ENCOUNTERED -> maybeStartPipeline(msg.obj as Exception) MessageType.CONTENT_APPEARED -> handleContentAppeared() MessageType.REMOTE_LOAD_STATUS_CHANGED -> handleRemoteLoadStatusChanged(msg.obj as PulseErrorRecoveryDelegate.RemoteLoadStatus) else -> throw RuntimeException("PulseErrorRecoveryHandler cannot handle message " + msg.what) } } private fun handleContentAppeared() { hasContentAppeared = true // the launch now counts as "successful" so we don't want to roll back; // remove any extraneous tasks from the pipeline as such pipeline.retainAll(setOf(Task.WAIT_FOR_REMOTE_UPDATE, Task.CRASH)) delegate.markSuccessfulLaunchForLaunchedUpdate() } private fun handleRemoteLoadStatusChanged(newStatus: PulseErrorRecoveryDelegate.RemoteLoadStatus) { if (!isWaitingForRemoteUpdate) { return } isWaitingForRemoteUpdate = false if (newStatus != PulseErrorRecoveryDelegate.RemoteLoadStatus.NEW_UPDATE_LOADED) { pipeline.remove(Task.LAUNCH_NEW_UPDATE) } runNextTask() } private fun runNextTask() { when (pipeline.removeAt(0)) { Task.WAIT_FOR_REMOTE_UPDATE -> { pulseLog(TAG, "ErrorRecovery: attempting to fetch a new update, waiting") waitForRemoteUpdate() } Task.LAUNCH_NEW_UPDATE -> { pulseLog(TAG, "ErrorRecovery: launching new update") tryRelaunchFromCache() } Task.LAUNCH_CACHED_UPDATE -> { pulseLog(TAG, "ErrorRecovery: falling back to older update") tryRelaunchFromCache() } Task.CRASH -> { pulseLogError(TAG, "ErrorRecovery: could not recover from error, crashing") crash() } } } private fun maybeStartPipeline(exception: Exception) { encounteredErrors.add(exception) if (delegate.getLaunchedUpdateSuccessfulLaunchCount() > 0) { pipeline.remove(Task.LAUNCH_CACHED_UPDATE) } else if (!hasContentAppeared) { delegate.markFailedLaunchForLaunchedUpdate() } if (!isPipelineRunning) { isPipelineRunning = true runNextTask() } } private fun waitForRemoteUpdate() { val remoteLoadStatus = delegate.getRemoteLoadStatus() if (remoteLoadStatus == PulseErrorRecoveryDelegate.RemoteLoadStatus.NEW_UPDATE_LOADED) { runNextTask() } else if (remoteLoadStatus == PulseErrorRecoveryDelegate.RemoteLoadStatus.NEW_UPDATE_LOADING || delegate.getCheckOnLaunchConfiguration().uppercase() != "NEVER" ) { isWaitingForRemoteUpdate = true if (delegate.getRemoteLoadStatus() != PulseErrorRecoveryDelegate.RemoteLoadStatus.NEW_UPDATE_LOADING) { delegate.loadRemoteUpdate() } postDelayed({ handleRemoteLoadStatusChanged(PulseErrorRecoveryDelegate.RemoteLoadStatus.IDLE) }, REMOTE_LOAD_TIMEOUT_MS) } else { pipeline.remove(Task.LAUNCH_NEW_UPDATE) runNextTask() } } private fun tryRelaunchFromCache() { delegate.relaunch(object : PulseErrorRecoveryDelegate.LauncherCallback { override fun onFailure(e: Exception) { post { encounteredErrors.add(e) pipeline.removeAll(setOf(Task.LAUNCH_NEW_UPDATE, Task.LAUNCH_CACHED_UPDATE)) runNextTask() } } override fun onSuccess() { post { isPipelineRunning = false } } }) } private fun crash() { delegate.throwException(encounteredErrors[0]) } companion object { private const val TAG = "PulseErrorRecoveryHandler" const val REMOTE_LOAD_TIMEOUT_MS = 5000L } }