diff options
author | Presubmit Automerger Backend <android-build-presubmit-automerger-backend@system.gserviceaccount.com> | 2023-03-06 12:12:32 +0000 |
---|---|---|
committer | Yuri Ufimtsev <yufimtsev@google.com> | 2023-03-06 17:03:35 +0000 |
commit | 145b01a0d967938b11a90025fe1aa3a434956eba (patch) | |
tree | edbcbc1c90735a845f86e2f3670032a973f00776 /PermissionController/src/com/android/permissioncontroller/safetycenter | |
parent | 6c6bbf772ac0e14155dafe93690000484f28c653 (diff) | |
parent | b8bf87e868322bbcb121bf7fe0d4f30bbda4ffac (diff) | |
download | Permission-145b01a0d967938b11a90025fe1aa3a434956eba.tar.gz |
Fix the Safety Center Status showing logic to cover edge cases 2p: b8bf87e868
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/Permission/+/20980501
Bug: 261160581
Change-Id: Ia792283b6e2fdbd438caf38a43bc6a7d5064980f
Diffstat (limited to 'PermissionController/src/com/android/permissioncontroller/safetycenter')
3 files changed, 275 insertions, 78 deletions
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusAnimationSequencer.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusAnimationSequencer.kt new file mode 100644 index 000000000..c48ad734b --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusAnimationSequencer.kt @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permissioncontroller.safetycenter.ui + +import android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN + +/** + * Controls the animation flow and hold all the data necessary to determine the appearance of Status + * icon of [SafetyStatusPreference]. For each lifecycle event (such as [onUpdateReceived], + * [onStartScanningAnimationStart], [onStartScanningAnimationEnd], etc.) it changes its internal + * state and may provide a presentation instruction in the form of [Action]. + */ +internal class SafetyStatusAnimationSequencer { + + private var isIconChangeAnimationRunning: Boolean = false + private var isScanAnimationRunning: Boolean = false + private var shouldStartScanAnimation: Boolean = false + private var queuedIconChangeAnimationSeverityLevel: Int? = null + /** + * Stores the last known Severity Level that user could observe as a static status image, as + * scan animation, or as the beginning state of a changing status animation. + */ + private var currentlyVisibleSeverityLevel: Int = OVERALL_SEVERITY_LEVEL_UNKNOWN + + fun getCurrentlyVisibleSeverityLevel(): Int { + return currentlyVisibleSeverityLevel + } + + fun onUpdateReceived(isRefreshInProgress: Boolean, severityLevel: Int): Action? { + if (isRefreshInProgress) { + if (isIconChangeAnimationRunning) { + shouldStartScanAnimation = true + return null + } else if (!isScanAnimationRunning) { + currentlyVisibleSeverityLevel = severityLevel + return Action.START_SCANNING_ANIMATION + } + // isRefreshInProgress && isScanAnimationRunning && !isIconChangeAnimationRunning + // Next action needs to wait for onStartScanningAnimationEnd or + // onContinueScanningAnimationEnd not to break currently running animation. + return null + } else { + val isDifferentSeverityQueued = + queuedIconChangeAnimationSeverityLevel != null && + queuedIconChangeAnimationSeverityLevel != severityLevel + val shouldChangeIcon = + currentlyVisibleSeverityLevel != severityLevel || isDifferentSeverityQueued + + if (isIconChangeAnimationRunning || shouldChangeIcon && isScanAnimationRunning) { + queuedIconChangeAnimationSeverityLevel = severityLevel + } + if (isScanAnimationRunning) { + return Action.FINISH_SCANNING_ANIMATION + } else if (shouldChangeIcon && !isIconChangeAnimationRunning) { + return Action.START_ICON_CHANGE_ANIMATION + } else if (!isIconChangeAnimationRunning) { + // Possible if status was finalized by Safety Center at the beginning, + // when no scanning animation is launched and refresh is not in progress. + // In this case we need to show the final icon straigt away without any animations. + return Action.CHANGE_ICON_WITHOUT_ANIMATION + } + // !isRefreshInProgress && !isScanAnimationRunning && isIconChangeAnimationRunning + // Next action needs to wait for onIconChangeAnimationEnd not to break currently + // running animation. + return null + } + } + + fun onStartScanningAnimationStart() { + isScanAnimationRunning = true + } + + fun onStartScanningAnimationEnd(): Action { + return Action.CONTINUE_SCANNING_ANIMATION + } + + fun onContinueScanningAnimationEnd(isRefreshInProgress: Boolean, severityLevel: Int): Action? { + if (isRefreshInProgress) { + if (currentlyVisibleSeverityLevel != severityLevel) { + // onUpdateReceived does not handle this case since we should not break + // the animation while it is running. Once current scan cycle is finished, this + // call will return the request to restart animation with updated severity level. + currentlyVisibleSeverityLevel = severityLevel + return Action.RESET_SCANNING_ANIMATION + } else { + return Action.CONTINUE_SCANNING_ANIMATION + } + } else { + // Possible if scanning animation has been ended right after status is updated with + // final data, but before we got the onUpdateReceived call (that is posted to the + // message queue and will happen soon), so no need to do anything right now. + return null + } + } + + fun onFinishScanAnimationEnd(isRefreshing: Boolean, severityLevel: Int): Action { + isScanAnimationRunning = false + currentlyVisibleSeverityLevel = severityLevel + return handleQueuedAction(isRefreshing, severityLevel) + } + + fun onCouldNotStartIconChangeAnimation(isRefreshing: Boolean, severityLevel: Int): Action { + return handleQueuedAction(isRefreshing, severityLevel) + } + + fun onIconChangeAnimationStart() { + isIconChangeAnimationRunning = true + } + + fun onIconChangeAnimationEnd(isRefreshing: Boolean, severityLevel: Int): Action { + isIconChangeAnimationRunning = false + currentlyVisibleSeverityLevel = severityLevel + return handleQueuedAction(isRefreshing, severityLevel) + } + + private fun handleQueuedAction(isRefreshing: Boolean, severityLevel: Int): Action { + if (shouldStartScanAnimation) { + shouldStartScanAnimation = false + if (isRefreshing) { + return Action.START_SCANNING_ANIMATION + } else { + return handleQueuedAction(isRefreshing, severityLevel) + } + } else if (queuedIconChangeAnimationSeverityLevel != null) { + val queuedSeverityLevel = queuedIconChangeAnimationSeverityLevel + queuedIconChangeAnimationSeverityLevel = null + if (currentlyVisibleSeverityLevel != queuedSeverityLevel) { + return Action.START_ICON_CHANGE_ANIMATION + } else { + return handleQueuedAction(isRefreshing, severityLevel) + } + } + currentlyVisibleSeverityLevel = severityLevel + return Action.CHANGE_ICON_WITHOUT_ANIMATION + } + + /** Set of instructions of what should Status icon currently show. */ + enum class Action { + START_SCANNING_ANIMATION, + /** + * Requests to continue the scanning animation with the same Severity Level as stored in + * [currentlyVisibleSeverityLevel]. + */ + CONTINUE_SCANNING_ANIMATION, + /** + * Requests to start scanning animation from the beginning when + * [currentlyVisibleSeverityLevel] has been changed. + */ + RESET_SCANNING_ANIMATION, + FINISH_SCANNING_ANIMATION, + START_ICON_CHANGE_ANIMATION, + CHANGE_ICON_WITHOUT_ANIMATION + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java index 1f6c15183..6c1c7aa90 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java @@ -17,7 +17,6 @@ package com.android.permissioncontroller.safetycenter.ui; import static android.os.Build.VERSION_CODES.TIRAMISU; -import static android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN; import android.content.Context; import android.graphics.drawable.Animatable2; @@ -70,12 +69,8 @@ public class SafetyStatusPreference extends Preference implements ComparablePref setLayoutResource(R.layout.preference_safety_status); } - private boolean mIsScanAnimationRunning; - private boolean mIsIconChangeAnimationRunning; private boolean mIsTextChangeAnimationRunning; - private int mQueuedScanAnimationSeverityLevel; - private int mQueuedIconAnimationSeverityLevel; - private int mSettledSeverityLevel = OVERALL_SEVERITY_LEVEL_UNKNOWN; + private final SafetyStatusAnimationSequencer mSequencer = new SafetyStatusAnimationSequencer(); @Override public void onBindViewHolder(PreferenceViewHolder holder) { @@ -162,29 +157,13 @@ public class SafetyStatusPreference extends Preference implements ComparablePref private void updateStatusIcon(ImageView statusImage, View rescanButton) { int severityLevel = mStatus.getSeverityLevel(); - boolean isRefreshing = mStatus.isRefreshInProgress(); - boolean shouldStartScanAnimation = isRefreshing && !mIsScanAnimationRunning; - boolean shouldEndScanAnimation = !isRefreshing && mIsScanAnimationRunning; - boolean shouldChangeIcon = mSettledSeverityLevel != severityLevel; - - if (shouldStartScanAnimation && !mIsIconChangeAnimationRunning) { - mSettledSeverityLevel = severityLevel; - startScanningAnimation(statusImage); - } else if (shouldStartScanAnimation) { - mQueuedScanAnimationSeverityLevel = severityLevel; - } else if (mIsScanAnimationRunning && shouldChangeIcon) { - mSettledSeverityLevel = severityLevel; - continueScanningAnimation(statusImage); - } else if (shouldEndScanAnimation) { - endScanningAnimation(statusImage, rescanButton); - } else if (shouldChangeIcon && !mIsScanAnimationRunning) { - startIconChangeAnimation(statusImage); - } else if (shouldChangeIcon) { - mQueuedIconAnimationSeverityLevel = severityLevel; - } else if (!mIsScanAnimationRunning && !mIsIconChangeAnimationRunning) { - setSettledStatus(statusImage); - } + + handleAnimationSequencerAction( + mSequencer.onUpdateReceived(isRefreshing, severityLevel), + statusImage, + rescanButton, + /* scanningAnimation= */ null); } private void runTextAnimationIfNeeded(TextView titleView, TextView summaryView) { @@ -212,22 +191,27 @@ public class SafetyStatusPreference extends Preference implements ComparablePref } } - private void startScanningAnimation(ImageView statusImage) { - mIsScanAnimationRunning = true; + private void startScanningAnimation(ImageView statusImage, View rescanButton) { + mSequencer.onStartScanningAnimationStart(); statusImage.setImageResource( - StatusAnimationResolver.getScanningStartAnimation(mSettledSeverityLevel)); + StatusAnimationResolver.getScanningStartAnimation( + mSequencer.getCurrentlyVisibleSeverityLevel())); AnimatedVectorDrawable animation = (AnimatedVectorDrawable) statusImage.getDrawable(); animation.registerAnimationCallback( new Animatable2.AnimationCallback() { @Override public void onAnimationEnd(Drawable drawable) { - continueScanningAnimation(statusImage); + handleAnimationSequencerAction( + mSequencer.onStartScanningAnimationEnd(), + statusImage, + rescanButton, + /* scanningAnimation= */ null); } }); animation.start(); } - private void continueScanningAnimation(ImageView statusImage) { + private void continueScanningAnimation(ImageView statusImage, View rescanButton) { // clear previous scan animation in case we need to continue with different severity level Drawable statusDrawable = statusImage.getDrawable(); if (statusDrawable instanceof AnimatedVectorDrawable) { @@ -235,17 +219,19 @@ public class SafetyStatusPreference extends Preference implements ComparablePref } statusImage.setImageResource( - StatusAnimationResolver.getScanningAnimation(mSettledSeverityLevel)); + StatusAnimationResolver.getScanningAnimation( + mSequencer.getCurrentlyVisibleSeverityLevel())); AnimatedVectorDrawable scanningAnim = (AnimatedVectorDrawable) statusImage.getDrawable(); scanningAnim.registerAnimationCallback( new Animatable2.AnimationCallback() { @Override public void onAnimationEnd(Drawable drawable) { - if (mIsScanAnimationRunning && mStatus.isRefreshInProgress()) { - scanningAnim.start(); - } else { - scanningAnim.clearAnimationCallbacks(); - } + handleAnimationSequencerAction( + mSequencer.onContinueScanningAnimationEnd( + mStatus.isRefreshInProgress(), mStatus.getSeverityLevel()), + statusImage, + rescanButton, + scanningAnim); } }); scanningAnim.start(); @@ -253,18 +239,19 @@ public class SafetyStatusPreference extends Preference implements ComparablePref private void endScanningAnimation(ImageView statusImage, View rescanButton) { Drawable statusDrawable = statusImage.getDrawable(); + int finishingSeverityLevel = mStatus.getSeverityLevel(); if (!(statusDrawable instanceof AnimatedVectorDrawable)) { - finishScanAnimation(statusImage, rescanButton); + finishScanAnimation(statusImage, rescanButton, finishingSeverityLevel); return; } AnimatedVectorDrawable animatedStatusDrawable = (AnimatedVectorDrawable) statusDrawable; if (!animatedStatusDrawable.isRunning()) { - finishScanAnimation(statusImage, rescanButton); + finishScanAnimation(statusImage, rescanButton, finishingSeverityLevel); return; } - int scanningSeverityLevel = mSettledSeverityLevel; + int scanningSeverityLevel = mSequencer.getCurrentlyVisibleSeverityLevel(); animatedStatusDrawable.clearAnimationCallbacks(); animatedStatusDrawable.registerAnimationCallback( new Animatable2.AnimationCallback() { @@ -272,7 +259,7 @@ public class SafetyStatusPreference extends Preference implements ComparablePref public void onAnimationEnd(Drawable drawable) { statusImage.setImageResource( StatusAnimationResolver.getScanningEndAnimation( - scanningSeverityLevel, mStatus.getSeverityLevel())); + scanningSeverityLevel, finishingSeverityLevel)); AnimatedVectorDrawable animatedDrawable = (AnimatedVectorDrawable) statusImage.getDrawable(); animatedDrawable.registerAnimationCallback( @@ -280,7 +267,8 @@ public class SafetyStatusPreference extends Preference implements ComparablePref @Override public void onAnimationEnd(Drawable drawable) { super.onAnimationEnd(drawable); - finishScanAnimation(statusImage, rescanButton); + finishScanAnimation( + statusImage, rescanButton, finishingSeverityLevel); } }); animatedDrawable.start(); @@ -288,22 +276,32 @@ public class SafetyStatusPreference extends Preference implements ComparablePref }); } - private void finishScanAnimation(ImageView statusImage, View rescanButton) { - mIsScanAnimationRunning = false; + private void finishScanAnimation( + ImageView statusImage, View rescanButton, int finishedSeverityLevel) { setRescanButtonState(rescanButton); - setSettledStatus(statusImage); - handleQueuedAction(statusImage); + handleAnimationSequencerAction( + mSequencer.onFinishScanAnimationEnd( + mStatus.isRefreshInProgress(), finishedSeverityLevel), + statusImage, + rescanButton, + /* scanningAnimation= */ null); } - private void startIconChangeAnimation(ImageView statusImage) { + private void startIconChangeAnimation(ImageView statusImage, View rescanButton) { + int finalSeverityLevel = mStatus.getSeverityLevel(); int changeAnimationResId = StatusAnimationResolver.getStatusChangeAnimation( - mSettledSeverityLevel, mStatus.getSeverityLevel()); + mSequencer.getCurrentlyVisibleSeverityLevel(), finalSeverityLevel); if (changeAnimationResId == 0) { - setSettledStatus(statusImage); + handleAnimationSequencerAction( + mSequencer.onCouldNotStartIconChangeAnimation( + mStatus.isRefreshInProgress(), finalSeverityLevel), + statusImage, + rescanButton, + /* scanningAnimation= */ null); return; } - mIsIconChangeAnimationRunning = true; + mSequencer.onIconChangeAnimationStart(); statusImage.setImageResource(changeAnimationResId); AnimatedVectorDrawable animation = (AnimatedVectorDrawable) statusImage.getDrawable(); animation.clearAnimationCallbacks(); @@ -311,32 +309,59 @@ public class SafetyStatusPreference extends Preference implements ComparablePref new Animatable2.AnimationCallback() { @Override public void onAnimationEnd(Drawable drawable) { - mIsIconChangeAnimationRunning = false; - setSettledStatus(statusImage); - handleQueuedAction(statusImage); + handleAnimationSequencerAction( + mSequencer.onIconChangeAnimationEnd( + mStatus.isRefreshInProgress(), finalSeverityLevel), + statusImage, + rescanButton, + /* scanningAnimation= */ null); } }); animation.start(); } + private void handleAnimationSequencerAction( + @Nullable SafetyStatusAnimationSequencer.Action action, + ImageView statusImage, + View rescanButton, + @Nullable AnimatedVectorDrawable scanningAnimation) { + if (action == null) { + return; + } + switch (action) { + case START_SCANNING_ANIMATION: + startScanningAnimation(statusImage, rescanButton); + break; + case CONTINUE_SCANNING_ANIMATION: + if (scanningAnimation != null) { + scanningAnimation.start(); + } else { + continueScanningAnimation(statusImage, rescanButton); + } + break; + case RESET_SCANNING_ANIMATION: + continueScanningAnimation(statusImage, rescanButton); + break; + case FINISH_SCANNING_ANIMATION: + endScanningAnimation(statusImage, rescanButton); + break; + case START_ICON_CHANGE_ANIMATION: + startIconChangeAnimation(statusImage, rescanButton); + break; + case CHANGE_ICON_WITHOUT_ANIMATION: + setSettledStatus(statusImage); + break; + } + } + private void setSettledStatus(ImageView statusImage) { Drawable statusDrawable = statusImage.getDrawable(); if (statusDrawable instanceof AnimatedVectorDrawable) { ((AnimatedVectorDrawable) statusDrawable).clearAnimationCallbacks(); } - - mSettledSeverityLevel = mStatus.getSeverityLevel(); - statusImage.setImageResource(mStatus.getStatusImageResId()); - } - - private void handleQueuedAction(ImageView statusImage) { - if (mQueuedScanAnimationSeverityLevel != 0) { - mQueuedScanAnimationSeverityLevel = 0; - startScanningAnimation(statusImage); - } else if (mQueuedIconAnimationSeverityLevel != 0) { - mQueuedIconAnimationSeverityLevel = 0; - startIconChangeAnimation(statusImage); - } + statusImage.setImageResource( + StatusUiData.Companion.getStatusImageResId( + mSequencer.getCurrentlyVisibleSeverityLevel())); } /** diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/StatusUiData.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/StatusUiData.kt index beeda213c..d83abbe12 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/StatusUiData.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/StatusUiData.kt @@ -1,12 +1,15 @@ package com.android.permissioncontroller.safetycenter.ui.model import android.content.Context +import android.os.Build.VERSION_CODES.TIRAMISU import android.safetycenter.SafetyCenterData import android.safetycenter.SafetyCenterStatus import android.util.Log +import androidx.annotation.RequiresApi import com.android.permissioncontroller.R /** UI model representation of a Status Card. */ +@RequiresApi(TIRAMISU) data class StatusUiData( private val status: SafetyCenterStatus, @get:JvmName("hasIssues") val hasIssues: Boolean = false, @@ -21,16 +24,9 @@ data class StatusUiData( fun copyForPendingActions(hasPendingActions: Boolean) = copy(hasPendingActions = hasPendingActions) - private companion object { - val TAG: String = StatusUiData::class.java.simpleName - } - - val title: CharSequence by status::title - val originalSummary: CharSequence by status::summary - val severityLevel: Int by status::severityLevel - - val statusImageResId: Int - get() = + companion object { + private val TAG: String = StatusUiData::class.java.simpleName + fun getStatusImageResId(severityLevel: Int) = when (severityLevel) { SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN, SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK -> R.drawable.safety_status_info @@ -43,6 +39,14 @@ data class StatusUiData( R.drawable.safety_status_info } } + } + + val title: CharSequence by status::title + val originalSummary: CharSequence by status::summary + val severityLevel: Int by status::severityLevel + + val statusImageResId: Int + get() = getStatusImageResId(severityLevel) fun getSummary(context: Context): CharSequence { return if (hasPendingActions) { |