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