diff options
author | Yuri Ufimtsev <yufimtsev@google.com> | 2023-01-16 13:18:14 +0000 |
---|---|---|
committer | Yuri Ufimtsev <yufimtsev@google.com> | 2023-03-06 12:11:54 +0000 |
commit | b8bf87e868322bbcb121bf7fe0d4f30bbda4ffac (patch) | |
tree | 830f32d03c4210728bf1994d361577e793908d36 /PermissionController/src/com/android/permissioncontroller/safetycenter | |
parent | 0a8a7fc9934588c4d5b85a0ba88a6dab3ae8b4c1 (diff) | |
download | Permission-b8bf87e868322bbcb121bf7fe0d4f30bbda4ffac.tar.gz |
Fix the Safety Center Status showing logic to cover edge cases
In old logic it was possible to end in a state where refresh is not in
progress with a request to continue scanning animation, rendering the
first frame of scanning animation instead of the final status icon.
Event sequence for this case:
1) receive initial refreshing SafetyCenterData with Severity Level A
2) wait until scanning animation is running in loops
3) do not receive any refreshing SafetyCenterData with Severity Level B
4) receive final non-refreshing SafetyCenterData with Severity Level B
Test: atest PermissionControllerMockingTests
Bug: 261160581
Relnote: Fixed a bug
Change-Id: Ifff75d4e3cce0c812b87836797efc33b007ac2d2
Diffstat (limited to 'PermissionController/src/com/android/permissioncontroller/safetycenter')
2 files changed, 265 insertions, 73 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 f081017b4..a46501a21 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java @@ -82,12 +82,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) { @@ -150,7 +146,7 @@ public class SafetyStatusPreference extends Preference implements ComparablePref holder.itemView.getPaddingStart(), holder.itemView.getPaddingTop(), holder.itemView.getPaddingEnd(), - /* bottom = */ getContext() + /* bottom= */ getContext() .getResources() .getDimensionPixelSize(R.dimen.sc_card_margin_bottom)); } else { @@ -158,7 +154,7 @@ public class SafetyStatusPreference extends Preference implements ComparablePref holder.itemView.getPaddingStart(), holder.itemView.getPaddingTop(), holder.itemView.getPaddingEnd(), - /* bottom = */ 0); + /* bottom= */ 0); } } @@ -172,29 +168,13 @@ public class SafetyStatusPreference extends Preference implements ComparablePref private void updateStatusIcon(ImageView statusImage, View rescanButton) { int severityLevel = mStatus.getSeverityLevel(); - boolean isRefreshing = 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) { @@ -205,10 +185,11 @@ public class SafetyStatusPreference extends Preference implements ComparablePref String summaryText = getSummaryText().toString(); boolean titleEquals = titleView.getText().toString().equals(titleText); boolean summaryEquals = summaryView.getText().toString().equals(summaryText); - Runnable onFinish = () -> { - mIsTextChangeAnimationRunning = false; - runTextAnimationIfNeeded(titleView, summaryView); - }; + Runnable onFinish = + () -> { + mIsTextChangeAnimationRunning = false; + runTextAnimationIfNeeded(titleView, summaryView); + }; mIsTextChangeAnimationRunning = !titleEquals || !summaryEquals; if (!titleEquals && !summaryEquals) { Pair<TextView, String> titleChange = new Pair<>(titleView, titleText); @@ -235,22 +216,27 @@ public class SafetyStatusPreference extends Preference implements ComparablePref || refreshStatus == SafetyCenterStatus.REFRESH_STATUS_DATA_FETCH_IN_PROGRESS; } - 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) { @@ -258,17 +244,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 && isRefreshInProgress()) { - scanningAnim.start(); - } else { - scanningAnim.clearAnimationCallbacks(); - } + handleAnimationSequencerAction( + mSequencer.onContinueScanningAnimationEnd( + isRefreshInProgress(), mStatus.getSeverityLevel()), + statusImage, + rescanButton, + scanningAnim); } }); scanningAnim.start(); @@ -276,18 +264,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() { @@ -295,7 +284,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( @@ -304,7 +293,7 @@ public class SafetyStatusPreference extends Preference implements ComparablePref public void onAnimationEnd(Drawable drawable) { super.onAnimationEnd(drawable); finishScanAnimation( - statusImage, rescanButton); + statusImage, rescanButton, finishingSeverityLevel); } }); animatedDrawable.start(); @@ -312,22 +301,31 @@ 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(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( + isRefreshInProgress(), finalSeverityLevel), + statusImage, + rescanButton, + /* scanningAnimation= */ null); return; } - mIsIconChangeAnimationRunning = true; + mSequencer.onIconChangeAnimationStart(); statusImage.setImageResource(changeAnimationResId); AnimatedVectorDrawable animation = (AnimatedVectorDrawable) statusImage.getDrawable(); animation.clearAnimationCallbacks(); @@ -335,32 +333,58 @@ 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( + 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(toStatusImageResId(mSettledSeverityLevel)); - } - - private void handleQueuedAction(ImageView statusImage) { - if (mQueuedScanAnimationSeverityLevel != 0) { - mQueuedScanAnimationSeverityLevel = 0; - startScanningAnimation(statusImage); - } else if (mQueuedIconAnimationSeverityLevel != 0) { - mQueuedIconAnimationSeverityLevel = 0; - startIconChangeAnimation(statusImage); - } + statusImage.setImageResource( + toStatusImageResId(mSequencer.getCurrentlyVisibleSeverityLevel())); } /** |