1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.permissioncontroller.safetycenter.ui 18 19 import android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN 20 21 /** 22 * Controls the animation flow and hold all the data necessary to determine the appearance of Status 23 * icon of [SafetyStatusPreference]. For each lifecycle event (such as [onUpdateReceived], 24 * [onStartScanningAnimationStart], [onStartScanningAnimationEnd], etc.) it changes its internal 25 * state and may provide a presentation instruction in the form of [Action]. 26 */ 27 class SafetyStatusAnimationSequencer { 28 29 private var isIconChangeAnimationRunning: Boolean = false 30 private var isScanAnimationRunning: Boolean = false 31 private var shouldStartScanAnimation: Boolean = false 32 private var queuedIconChangeAnimationSeverityLevel: Int? = null 33 /** 34 * Stores the last known Severity Level that user could observe as a static status image, as 35 * scan animation, or as the beginning state of a changing status animation. 36 */ 37 private var currentlyVisibleSeverityLevel: Int = OVERALL_SEVERITY_LEVEL_UNKNOWN 38 getCurrentlyVisibleSeverityLevelnull39 fun getCurrentlyVisibleSeverityLevel(): Int { 40 return currentlyVisibleSeverityLevel 41 } 42 onUpdateReceivednull43 fun onUpdateReceived(isRefreshInProgress: Boolean, severityLevel: Int): Action? { 44 if (isRefreshInProgress) { 45 if (isIconChangeAnimationRunning) { 46 shouldStartScanAnimation = true 47 return null 48 } else if (!isScanAnimationRunning) { 49 currentlyVisibleSeverityLevel = severityLevel 50 return Action.START_SCANNING_ANIMATION 51 } 52 // isRefreshInProgress && isScanAnimationRunning && !isIconChangeAnimationRunning 53 // Next action needs to wait for onStartScanningAnimationEnd or 54 // onContinueScanningAnimationEnd not to break currently running animation. 55 return null 56 } else { 57 val isDifferentSeverityQueued = 58 queuedIconChangeAnimationSeverityLevel != null && 59 queuedIconChangeAnimationSeverityLevel != severityLevel 60 val shouldChangeIcon = 61 currentlyVisibleSeverityLevel != severityLevel || isDifferentSeverityQueued 62 63 if (isIconChangeAnimationRunning || shouldChangeIcon && isScanAnimationRunning) { 64 queuedIconChangeAnimationSeverityLevel = severityLevel 65 } 66 if (isScanAnimationRunning) { 67 return Action.FINISH_SCANNING_ANIMATION 68 } else if (shouldChangeIcon && !isIconChangeAnimationRunning) { 69 return Action.START_ICON_CHANGE_ANIMATION 70 } else if (!isIconChangeAnimationRunning) { 71 // Possible if status was finalized by Safety Center at the beginning, 72 // when no scanning animation is launched and refresh is not in progress. 73 // In this case we need to show the final icon straigt away without any animations. 74 return Action.CHANGE_ICON_WITHOUT_ANIMATION 75 } 76 // !isRefreshInProgress && !isScanAnimationRunning && isIconChangeAnimationRunning 77 // Next action needs to wait for onIconChangeAnimationEnd not to break currently 78 // running animation. 79 return null 80 } 81 } 82 onStartScanningAnimationStartnull83 fun onStartScanningAnimationStart() { 84 isScanAnimationRunning = true 85 } 86 onStartScanningAnimationEndnull87 fun onStartScanningAnimationEnd(): Action { 88 return Action.CONTINUE_SCANNING_ANIMATION 89 } 90 onContinueScanningAnimationEndnull91 fun onContinueScanningAnimationEnd(isRefreshInProgress: Boolean, severityLevel: Int): Action? { 92 if (isRefreshInProgress) { 93 if (currentlyVisibleSeverityLevel != severityLevel) { 94 // onUpdateReceived does not handle this case since we should not break 95 // the animation while it is running. Once current scan cycle is finished, this 96 // call will return the request to restart animation with updated severity level. 97 currentlyVisibleSeverityLevel = severityLevel 98 return Action.RESET_SCANNING_ANIMATION 99 } else { 100 return Action.CONTINUE_SCANNING_ANIMATION 101 } 102 } else { 103 // Possible if scanning animation has been ended right after status is updated with 104 // final data, but before we got the onUpdateReceived call (that is posted to the 105 // message queue and will happen soon), so no need to do anything right now. 106 return null 107 } 108 } 109 onFinishScanAnimationEndnull110 fun onFinishScanAnimationEnd(isRefreshing: Boolean, severityLevel: Int): Action { 111 isScanAnimationRunning = false 112 currentlyVisibleSeverityLevel = severityLevel 113 return handleQueuedAction(isRefreshing, severityLevel) 114 } 115 onCouldNotStartIconChangeAnimationnull116 fun onCouldNotStartIconChangeAnimation(isRefreshing: Boolean, severityLevel: Int): Action { 117 return handleQueuedAction(isRefreshing, severityLevel) 118 } 119 onIconChangeAnimationStartnull120 fun onIconChangeAnimationStart() { 121 isIconChangeAnimationRunning = true 122 } 123 onIconChangeAnimationEndnull124 fun onIconChangeAnimationEnd(isRefreshing: Boolean, severityLevel: Int): Action { 125 isIconChangeAnimationRunning = false 126 currentlyVisibleSeverityLevel = severityLevel 127 return handleQueuedAction(isRefreshing, severityLevel) 128 } 129 handleQueuedActionnull130 private fun handleQueuedAction(isRefreshing: Boolean, severityLevel: Int): Action { 131 if (shouldStartScanAnimation) { 132 shouldStartScanAnimation = false 133 if (isRefreshing) { 134 return Action.START_SCANNING_ANIMATION 135 } else { 136 return handleQueuedAction(isRefreshing, severityLevel) 137 } 138 } else if (queuedIconChangeAnimationSeverityLevel != null) { 139 val queuedSeverityLevel = queuedIconChangeAnimationSeverityLevel 140 queuedIconChangeAnimationSeverityLevel = null 141 if (currentlyVisibleSeverityLevel != queuedSeverityLevel) { 142 return Action.START_ICON_CHANGE_ANIMATION 143 } else { 144 return handleQueuedAction(isRefreshing, severityLevel) 145 } 146 } 147 currentlyVisibleSeverityLevel = severityLevel 148 return Action.CHANGE_ICON_WITHOUT_ANIMATION 149 } 150 151 /** Set of instructions of what should Status icon currently show. */ 152 enum class Action { 153 START_SCANNING_ANIMATION, 154 /** 155 * Requests to continue the scanning animation with the same Severity Level as stored in 156 * [currentlyVisibleSeverityLevel]. 157 */ 158 CONTINUE_SCANNING_ANIMATION, 159 /** 160 * Requests to start scanning animation from the beginning when 161 * [currentlyVisibleSeverityLevel] has been changed. 162 */ 163 RESET_SCANNING_ANIMATION, 164 FINISH_SCANNING_ANIMATION, 165 START_ICON_CHANGE_ANIMATION, 166 CHANGE_ICON_WITHOUT_ANIMATION 167 } 168 } 169