• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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