• 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.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