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