1 /* 2 * Copyright (C) 2022 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 static android.os.Build.VERSION_CODES.TIRAMISU; 20 21 import android.content.Context; 22 import android.graphics.drawable.Animatable2; 23 import android.graphics.drawable.AnimatedVectorDrawable; 24 import android.graphics.drawable.Drawable; 25 import android.os.Handler; 26 import android.os.Looper; 27 import android.safetycenter.SafetyCenterStatus; 28 import android.text.TextUtils; 29 import android.util.AttributeSet; 30 import android.widget.ImageView; 31 import android.widget.TextView; 32 33 import androidx.annotation.Nullable; 34 import androidx.annotation.RequiresApi; 35 import androidx.preference.Preference; 36 import androidx.preference.PreferenceViewHolder; 37 38 import com.android.permissioncontroller.R; 39 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel; 40 import com.android.permissioncontroller.safetycenter.ui.model.StatusUiData; 41 import com.android.permissioncontroller.safetycenter.ui.view.StatusCardView; 42 43 import kotlin.Pair; 44 45 import java.util.List; 46 import java.util.Objects; 47 48 /** Preference which displays a visual representation of {@link SafetyCenterStatus}. */ 49 @RequiresApi(TIRAMISU) 50 public class SafetyStatusPreference extends Preference implements ComparablePreference { 51 52 @Nullable private StatusUiData mStatus; 53 @Nullable private SafetyCenterViewModel mViewModel; 54 55 private final TextFadeAnimator mTitleTextAnimator = new TextFadeAnimator(R.id.status_title); 56 57 private final TextFadeAnimator mSummaryTextAnimator = new TextFadeAnimator(R.id.status_summary); 58 59 private final TextFadeAnimator mAllTextAnimator = 60 new TextFadeAnimator(List.of(R.id.status_title, R.id.status_summary)); 61 62 private boolean mFirstBind = true; 63 SafetyStatusPreference(Context context, AttributeSet attrs)64 public SafetyStatusPreference(Context context, AttributeSet attrs) { 65 super(context, attrs); 66 setLayoutResource(R.layout.preference_safety_status); 67 } 68 69 private boolean mIsTextChangeAnimationRunning; 70 private final SafetyStatusAnimationSequencer mSequencer = new SafetyStatusAnimationSequencer(); 71 72 @Override onBindViewHolder(PreferenceViewHolder holder)73 public void onBindViewHolder(PreferenceViewHolder holder) { 74 super.onBindViewHolder(holder); 75 76 if (mStatus == null) { 77 return; 78 } 79 80 Context context = getContext(); 81 StatusCardView statusCardView = (StatusCardView) holder.itemView; 82 configureButtons(context, statusCardView); 83 statusCardView 84 .getTitleAndSummaryContainerView() 85 .setContentDescription(mStatus.getContentDescription(context)); 86 87 updateStatusIcon(statusCardView); 88 89 updateStatusText(statusCardView.getTitleView(), statusCardView.getSummaryView()); 90 91 mFirstBind = false; 92 } 93 configureButtons(Context context, StatusCardView statusCardView)94 private void configureButtons(Context context, StatusCardView statusCardView) { 95 statusCardView 96 .getRescanButton() 97 .setOnClickListener( 98 unused -> { 99 SafetyCenterViewModel viewModel = requireViewModel(); 100 viewModel.rescan(); 101 viewModel.getInteractionLogger().record(Action.SCAN_INITIATED); 102 }); 103 statusCardView 104 .getReviewSettingsButton() 105 .setOnClickListener( 106 unused -> { 107 SafetyCenterViewModel viewModel = requireViewModel(); 108 viewModel.navigateToSafetyCenter( 109 context, NavigationSource.QUICK_SETTINGS_TILE); 110 viewModel.getInteractionLogger().record(Action.REVIEW_SETTINGS_CLICKED); 111 }); 112 113 updateButtonState(statusCardView); 114 } 115 updateButtonState(StatusCardView statusCardView)116 private void updateButtonState(StatusCardView statusCardView) { 117 if (mStatus == null) return; // Shouldn't happen in practice but we do it for null safety. 118 statusCardView.showButtons(mStatus); 119 } 120 updateStatusText(TextView title, TextView summary)121 private void updateStatusText(TextView title, TextView summary) { 122 if (mFirstBind) { 123 title.setText(mStatus.getTitle()); 124 summary.setText(mStatus.getSummary(getContext())); 125 } 126 runTextAnimationIfNeeded(title, summary); 127 } 128 updateStatusIcon(StatusCardView statusCardView)129 private void updateStatusIcon(StatusCardView statusCardView) { 130 int severityLevel = mStatus.getSeverityLevel(); 131 boolean isRefreshing = mStatus.isRefreshInProgress(); 132 133 handleAnimationSequencerAction( 134 mSequencer.onUpdateReceived(isRefreshing, severityLevel), 135 statusCardView, 136 /* scanningAnimation= */ null); 137 } 138 runTextAnimationIfNeeded(TextView titleView, TextView summaryView)139 private void runTextAnimationIfNeeded(TextView titleView, TextView summaryView) { 140 if (mIsTextChangeAnimationRunning) { 141 return; 142 } 143 String titleText = mStatus.getTitle().toString(); 144 String summaryText = mStatus.getSummary(getContext()).toString(); 145 boolean titleEquals = titleView.getText().toString().equals(titleText); 146 boolean summaryEquals = summaryView.getText().toString().equals(summaryText); 147 Runnable onFinish = 148 () -> { 149 mIsTextChangeAnimationRunning = false; 150 runTextAnimationIfNeeded(titleView, summaryView); 151 }; 152 mIsTextChangeAnimationRunning = !titleEquals || !summaryEquals; 153 if (!titleEquals && !summaryEquals) { 154 Pair<TextView, String> titleChange = new Pair<>(titleView, titleText); 155 Pair<TextView, String> summaryChange = new Pair<>(summaryView, summaryText); 156 mAllTextAnimator.animateChangeText(List.of(titleChange, summaryChange), onFinish); 157 } else if (!titleEquals) { 158 mTitleTextAnimator.animateChangeText(titleView, titleText, onFinish); 159 } else if (!summaryEquals) { 160 mSummaryTextAnimator.animateChangeText(summaryView, summaryText, onFinish); 161 } 162 } 163 startScanningAnimation(StatusCardView statusCardView)164 private void startScanningAnimation(StatusCardView statusCardView) { 165 mSequencer.onStartScanningAnimationStart(); 166 ImageView statusImage = statusCardView.getStatusImageView(); 167 statusImage.setImageResource( 168 StatusAnimationResolver.getScanningStartAnimation( 169 mSequencer.getCurrentlyVisibleSeverityLevel())); 170 AnimatedVectorDrawable animation = (AnimatedVectorDrawable) statusImage.getDrawable(); 171 animation.registerAnimationCallback( 172 new Animatable2.AnimationCallback() { 173 @Override 174 public void onAnimationEnd(Drawable drawable) { 175 handleAnimationSequencerAction( 176 mSequencer.onStartScanningAnimationEnd(), 177 statusCardView, 178 /* scanningAnimation= */ null); 179 } 180 }); 181 animation.start(); 182 } 183 continueScanningAnimation(StatusCardView statusCardView)184 private void continueScanningAnimation(StatusCardView statusCardView) { 185 ImageView statusImage = statusCardView.getStatusImageView(); 186 187 // clear previous scan animation in case we need to continue with different severity level 188 Drawable statusDrawable = statusImage.getDrawable(); 189 if (statusDrawable instanceof AnimatedVectorDrawable) { 190 ((AnimatedVectorDrawable) statusDrawable).clearAnimationCallbacks(); 191 } 192 193 statusImage.setImageResource( 194 StatusAnimationResolver.getScanningAnimation( 195 mSequencer.getCurrentlyVisibleSeverityLevel())); 196 AnimatedVectorDrawable scanningAnim = (AnimatedVectorDrawable) statusImage.getDrawable(); 197 scanningAnim.registerAnimationCallback( 198 new Animatable2.AnimationCallback() { 199 @Override 200 public void onAnimationEnd(Drawable drawable) { 201 handleAnimationSequencerAction( 202 mSequencer.onContinueScanningAnimationEnd( 203 mStatus.isRefreshInProgress(), mStatus.getSeverityLevel()), 204 statusCardView, 205 scanningAnim); 206 } 207 }); 208 scanningAnim.start(); 209 } 210 endScanningAnimation(StatusCardView statusCardView)211 private void endScanningAnimation(StatusCardView statusCardView) { 212 ImageView statusImage = statusCardView.getStatusImageView(); 213 Drawable statusDrawable = statusImage.getDrawable(); 214 int finishingSeverityLevel = mStatus.getSeverityLevel(); 215 if (!(statusDrawable instanceof AnimatedVectorDrawable)) { 216 finishScanAnimation(statusCardView, finishingSeverityLevel); 217 return; 218 } 219 AnimatedVectorDrawable animatedStatusDrawable = (AnimatedVectorDrawable) statusDrawable; 220 221 if (!animatedStatusDrawable.isRunning()) { 222 finishScanAnimation(statusCardView, finishingSeverityLevel); 223 return; 224 } 225 226 int scanningSeverityLevel = mSequencer.getCurrentlyVisibleSeverityLevel(); 227 animatedStatusDrawable.clearAnimationCallbacks(); 228 animatedStatusDrawable.registerAnimationCallback( 229 new Animatable2.AnimationCallback() { 230 @Override 231 public void onAnimationEnd(Drawable drawable) { 232 statusImage.setImageResource( 233 StatusAnimationResolver.getScanningEndAnimation( 234 scanningSeverityLevel, finishingSeverityLevel)); 235 AnimatedVectorDrawable animatedDrawable = 236 (AnimatedVectorDrawable) statusImage.getDrawable(); 237 animatedDrawable.registerAnimationCallback( 238 new Animatable2.AnimationCallback() { 239 @Override 240 public void onAnimationEnd(Drawable drawable) { 241 super.onAnimationEnd(drawable); 242 finishScanAnimation(statusCardView, finishingSeverityLevel); 243 } 244 }); 245 animatedDrawable.start(); 246 } 247 }); 248 } 249 finishScanAnimation(StatusCardView statusCardView, int finishedSeverityLevel)250 private void finishScanAnimation(StatusCardView statusCardView, int finishedSeverityLevel) { 251 updateButtonState(statusCardView); 252 handleAnimationSequencerAction( 253 mSequencer.onFinishScanAnimationEnd( 254 mStatus.isRefreshInProgress(), finishedSeverityLevel), 255 statusCardView, 256 /* scanningAnimation= */ null); 257 } 258 startIconChangeAnimation(StatusCardView statusCardView)259 private void startIconChangeAnimation(StatusCardView statusCardView) { 260 int finalSeverityLevel = mStatus.getSeverityLevel(); 261 int changeAnimationResId = 262 StatusAnimationResolver.getStatusChangeAnimation( 263 mSequencer.getCurrentlyVisibleSeverityLevel(), finalSeverityLevel); 264 if (changeAnimationResId == 0) { 265 handleAnimationSequencerAction( 266 mSequencer.onCouldNotStartIconChangeAnimation( 267 mStatus.isRefreshInProgress(), finalSeverityLevel), 268 statusCardView, 269 /* scanningAnimation= */ null); 270 return; 271 } 272 mSequencer.onIconChangeAnimationStart(); 273 statusCardView.getStatusImageView().setImageResource(changeAnimationResId); 274 AnimatedVectorDrawable animation = 275 (AnimatedVectorDrawable) statusCardView.getStatusImageView().getDrawable(); 276 animation.clearAnimationCallbacks(); 277 animation.registerAnimationCallback( 278 new Animatable2.AnimationCallback() { 279 @Override 280 public void onAnimationEnd(Drawable drawable) { 281 handleAnimationSequencerAction( 282 mSequencer.onIconChangeAnimationEnd( 283 mStatus.isRefreshInProgress(), finalSeverityLevel), 284 statusCardView, 285 /* scanningAnimation= */ null); 286 } 287 }); 288 animation.start(); 289 } 290 handleAnimationSequencerAction( @ullable SafetyStatusAnimationSequencer.Action action, StatusCardView statusCardView, @Nullable AnimatedVectorDrawable scanningAnimation)291 private void handleAnimationSequencerAction( 292 @Nullable SafetyStatusAnimationSequencer.Action action, 293 StatusCardView statusCardView, 294 @Nullable AnimatedVectorDrawable scanningAnimation) { 295 if (action == null) { 296 return; 297 } 298 switch (action) { 299 case START_SCANNING_ANIMATION: 300 startScanningAnimation(statusCardView); 301 break; 302 case CONTINUE_SCANNING_ANIMATION: 303 if (scanningAnimation != null) { 304 scanningAnimation.start(); 305 } else { 306 continueScanningAnimation(statusCardView); 307 } 308 break; 309 case RESET_SCANNING_ANIMATION: 310 continueScanningAnimation(statusCardView); 311 break; 312 case FINISH_SCANNING_ANIMATION: 313 endScanningAnimation(statusCardView); 314 break; 315 case START_ICON_CHANGE_ANIMATION: 316 startIconChangeAnimation(statusCardView); 317 break; 318 case CHANGE_ICON_WITHOUT_ANIMATION: 319 setSettledStatus(statusCardView); 320 break; 321 } 322 } 323 setSettledStatus(StatusCardView statusCardView)324 private void setSettledStatus(StatusCardView statusCardView) { 325 Drawable statusDrawable = statusCardView.getStatusImageView().getDrawable(); 326 if (statusDrawable instanceof AnimatedVectorDrawable) { 327 ((AnimatedVectorDrawable) statusDrawable).clearAnimationCallbacks(); 328 } 329 statusCardView 330 .getStatusImageView() 331 .setImageResource( 332 StatusUiData.Companion.getStatusImageResId( 333 mSequencer.getCurrentlyVisibleSeverityLevel())); 334 } 335 setData(StatusUiData statusUiData)336 void setData(StatusUiData statusUiData) { 337 mStatus = statusUiData; 338 safeNotifyChanged(); 339 } 340 setViewModel(SafetyCenterViewModel viewModel)341 void setViewModel(SafetyCenterViewModel viewModel) { 342 mViewModel = Objects.requireNonNull(viewModel); 343 } 344 requireViewModel()345 private SafetyCenterViewModel requireViewModel() { 346 return Objects.requireNonNull(mViewModel); 347 } 348 349 // Calling notifyChanged while recyclerview is scrolling or computing layout will result in an 350 // IllegalStateException. Post to handler to wait for UI to settle. safeNotifyChanged()351 private void safeNotifyChanged() { 352 new Handler(Looper.getMainLooper()).post(this::notifyChanged); 353 } 354 355 @Override isSameItem(Preference preference)356 public boolean isSameItem(Preference preference) { 357 return preference instanceof SafetyStatusPreference 358 && TextUtils.equals(getKey(), preference.getKey()); 359 } 360 361 @Override hasSameContents(Preference preference)362 public boolean hasSameContents(Preference preference) { 363 if (!(preference instanceof SafetyStatusPreference)) { 364 return false; 365 } 366 SafetyStatusPreference other = (SafetyStatusPreference) preference; 367 return Objects.equals(mStatus, other.mStatus); 368 } 369 } 370