blob: c022daa474d881b969483d843b1f88cbee863f2b [file] [log] [blame]
/*
* Copyright (C) 2022 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 static android.os.Build.VERSION_CODES.TIRAMISU;
import android.content.Context;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.safetycenter.SafetyCenterStatus;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.permissioncontroller.R;
import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel;
import com.android.permissioncontroller.safetycenter.ui.model.StatusUiData;
import com.android.permissioncontroller.safetycenter.ui.view.StatusCardView;
import kotlin.Pair;
import java.util.List;
import java.util.Objects;
/** Preference which displays a visual representation of {@link SafetyCenterStatus}. */
@RequiresApi(TIRAMISU)
public class SafetyStatusPreference extends Preference implements ComparablePreference {
private static final String TAG = "SafetyStatusPreference";
@Nullable private StatusUiData mStatus;
@Nullable private SafetyCenterViewModel mViewModel;
private final TextFadeAnimator mTitleTextAnimator = new TextFadeAnimator(R.id.status_title);
private final TextFadeAnimator mSummaryTextAnimator = new TextFadeAnimator(R.id.status_summary);
private final TextFadeAnimator mAllTextAnimator =
new TextFadeAnimator(List.of(R.id.status_title, R.id.status_summary));
private boolean mFirstBind = true;
public SafetyStatusPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.preference_safety_status);
}
private boolean mIsTextChangeAnimationRunning;
private final SafetyStatusAnimationSequencer mSequencer = new SafetyStatusAnimationSequencer();
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
Log.v(TAG, String.format("onBindViewHolder called for status %s", mStatus));
if (mStatus == null) {
return;
}
Context context = getContext();
StatusCardView statusCardView = (StatusCardView) holder.itemView;
configureButtons(context, statusCardView);
statusCardView
.getTitleAndSummaryContainerView()
.setContentDescription(mStatus.getContentDescription(context));
updateStatusIcon(statusCardView);
updateStatusText(statusCardView.getTitleView(), statusCardView.getSummaryView());
mFirstBind = false;
}
private void configureButtons(Context context, StatusCardView statusCardView) {
statusCardView
.getRescanButton()
.setOnClickListener(
unused -> {
SafetyCenterViewModel viewModel = requireViewModel();
viewModel.rescan();
viewModel.getInteractionLogger().record(Action.SCAN_INITIATED);
});
statusCardView
.getReviewSettingsButton()
.setOnClickListener(
unused -> {
SafetyCenterViewModel viewModel = requireViewModel();
viewModel.navigateToSafetyCenter(
context, NavigationSource.QUICK_SETTINGS_TILE);
viewModel.getInteractionLogger().record(Action.REVIEW_SETTINGS_CLICKED);
});
updateButtonState(statusCardView);
}
private void updateButtonState(StatusCardView statusCardView) {
if (mStatus == null) return; // Shouldn't happen in practice but we do it for null safety.
statusCardView.showButtons(mStatus);
}
private void updateStatusText(TextView title, TextView summary) {
if (mFirstBind) {
title.setText(mStatus.getTitle());
summary.setText(mStatus.getSummary(getContext()));
}
runTextAnimationIfNeeded(title, summary);
}
private void updateStatusIcon(StatusCardView statusCardView) {
int severityLevel = mStatus.getSeverityLevel();
boolean isRefreshing = mStatus.isRefreshInProgress();
handleAnimationSequencerAction(
mSequencer.onUpdateReceived(isRefreshing, severityLevel),
statusCardView,
/* scanningAnimation= */ null);
}
private void runTextAnimationIfNeeded(TextView titleView, TextView summaryView) {
if (mIsTextChangeAnimationRunning) {
return;
}
Log.v(TAG, "Starting status text animation");
String titleText = mStatus.getTitle().toString();
String summaryText = mStatus.getSummary(getContext()).toString();
boolean titleEquals = titleView.getText().toString().equals(titleText);
boolean summaryEquals = summaryView.getText().toString().equals(summaryText);
Runnable onFinish =
() -> {
Log.v(TAG, "Finishing status text animation");
mIsTextChangeAnimationRunning = false;
runTextAnimationIfNeeded(titleView, summaryView);
};
mIsTextChangeAnimationRunning = !titleEquals || !summaryEquals;
if (!titleEquals && !summaryEquals) {
Pair<TextView, String> titleChange = new Pair<>(titleView, titleText);
Pair<TextView, String> summaryChange = new Pair<>(summaryView, summaryText);
mAllTextAnimator.animateChangeText(List.of(titleChange, summaryChange), onFinish);
} else if (!titleEquals) {
mTitleTextAnimator.animateChangeText(titleView, titleText, onFinish);
} else if (!summaryEquals) {
mSummaryTextAnimator.animateChangeText(summaryView, summaryText, onFinish);
}
}
private void startScanningAnimation(StatusCardView statusCardView) {
mSequencer.onStartScanningAnimationStart();
ImageView statusImage = statusCardView.getStatusImageView();
statusImage.setImageResource(
StatusAnimationResolver.getScanningStartAnimation(
mSequencer.getCurrentlyVisibleSeverityLevel()));
AnimatedVectorDrawable animation = (AnimatedVectorDrawable) statusImage.getDrawable();
animation.registerAnimationCallback(
new Animatable2.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
handleAnimationSequencerAction(
mSequencer.onStartScanningAnimationEnd(),
statusCardView,
/* scanningAnimation= */ null);
}
});
animation.start();
}
private void continueScanningAnimation(StatusCardView statusCardView) {
ImageView statusImage = statusCardView.getStatusImageView();
// clear previous scan animation in case we need to continue with different severity level
Drawable statusDrawable = statusImage.getDrawable();
if (statusDrawable instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) statusDrawable).clearAnimationCallbacks();
}
statusImage.setImageResource(
StatusAnimationResolver.getScanningAnimation(
mSequencer.getCurrentlyVisibleSeverityLevel()));
AnimatedVectorDrawable scanningAnim = (AnimatedVectorDrawable) statusImage.getDrawable();
scanningAnim.registerAnimationCallback(
new Animatable2.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
handleAnimationSequencerAction(
mSequencer.onContinueScanningAnimationEnd(
mStatus.isRefreshInProgress(), mStatus.getSeverityLevel()),
statusCardView,
scanningAnim);
}
});
scanningAnim.start();
}
private void endScanningAnimation(StatusCardView statusCardView) {
ImageView statusImage = statusCardView.getStatusImageView();
Drawable statusDrawable = statusImage.getDrawable();
int finishingSeverityLevel = mStatus.getSeverityLevel();
if (!(statusDrawable instanceof AnimatedVectorDrawable)) {
finishScanAnimation(statusCardView, finishingSeverityLevel);
return;
}
AnimatedVectorDrawable animatedStatusDrawable = (AnimatedVectorDrawable) statusDrawable;
if (!animatedStatusDrawable.isRunning()) {
finishScanAnimation(statusCardView, finishingSeverityLevel);
return;
}
int scanningSeverityLevel = mSequencer.getCurrentlyVisibleSeverityLevel();
animatedStatusDrawable.clearAnimationCallbacks();
animatedStatusDrawable.registerAnimationCallback(
new Animatable2.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
statusImage.setImageResource(
StatusAnimationResolver.getScanningEndAnimation(
scanningSeverityLevel, finishingSeverityLevel));
AnimatedVectorDrawable animatedDrawable =
(AnimatedVectorDrawable) statusImage.getDrawable();
animatedDrawable.registerAnimationCallback(
new Animatable2.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
super.onAnimationEnd(drawable);
finishScanAnimation(statusCardView, finishingSeverityLevel);
}
});
animatedDrawable.start();
}
});
}
private void finishScanAnimation(StatusCardView statusCardView, int finishedSeverityLevel) {
updateButtonState(statusCardView);
handleAnimationSequencerAction(
mSequencer.onFinishScanAnimationEnd(
mStatus.isRefreshInProgress(), finishedSeverityLevel),
statusCardView,
/* scanningAnimation= */ null);
}
private void startIconChangeAnimation(StatusCardView statusCardView) {
int finalSeverityLevel = mStatus.getSeverityLevel();
int changeAnimationResId =
StatusAnimationResolver.getStatusChangeAnimation(
mSequencer.getCurrentlyVisibleSeverityLevel(), finalSeverityLevel);
if (changeAnimationResId == 0) {
handleAnimationSequencerAction(
mSequencer.onCouldNotStartIconChangeAnimation(
mStatus.isRefreshInProgress(), finalSeverityLevel),
statusCardView,
/* scanningAnimation= */ null);
return;
}
mSequencer.onIconChangeAnimationStart();
statusCardView.getStatusImageView().setImageResource(changeAnimationResId);
AnimatedVectorDrawable animation =
(AnimatedVectorDrawable) statusCardView.getStatusImageView().getDrawable();
animation.clearAnimationCallbacks();
animation.registerAnimationCallback(
new Animatable2.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
handleAnimationSequencerAction(
mSequencer.onIconChangeAnimationEnd(
mStatus.isRefreshInProgress(), finalSeverityLevel),
statusCardView,
/* scanningAnimation= */ null);
}
});
animation.start();
}
private void handleAnimationSequencerAction(
@Nullable SafetyStatusAnimationSequencer.Action action,
StatusCardView statusCardView,
@Nullable AnimatedVectorDrawable scanningAnimation) {
if (action == null) {
return;
}
switch (action) {
case START_SCANNING_ANIMATION:
startScanningAnimation(statusCardView);
break;
case CONTINUE_SCANNING_ANIMATION:
if (scanningAnimation != null) {
scanningAnimation.start();
} else {
continueScanningAnimation(statusCardView);
}
break;
case RESET_SCANNING_ANIMATION:
continueScanningAnimation(statusCardView);
break;
case FINISH_SCANNING_ANIMATION:
endScanningAnimation(statusCardView);
break;
case START_ICON_CHANGE_ANIMATION:
startIconChangeAnimation(statusCardView);
break;
case CHANGE_ICON_WITHOUT_ANIMATION:
setSettledStatus(statusCardView);
break;
}
}
private void setSettledStatus(StatusCardView statusCardView) {
Drawable statusDrawable = statusCardView.getStatusImageView().getDrawable();
if (statusDrawable instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) statusDrawable).clearAnimationCallbacks();
}
statusCardView
.getStatusImageView()
.setImageResource(
StatusUiData.Companion.getStatusImageResId(
mSequencer.getCurrentlyVisibleSeverityLevel()));
}
void setData(StatusUiData statusUiData) {
mStatus = statusUiData;
Log.v(TAG, String.format("setData called for status %s", mStatus));
safeNotifyChanged();
}
void setViewModel(SafetyCenterViewModel viewModel) {
mViewModel = Objects.requireNonNull(viewModel);
}
private SafetyCenterViewModel requireViewModel() {
return Objects.requireNonNull(mViewModel);
}
// Calling notifyChanged while recyclerview is scrolling or computing layout will result in an
// IllegalStateException. Post to handler to wait for UI to settle.
private void safeNotifyChanged() {
new Handler(Looper.getMainLooper())
.post(
() -> {
Log.v(
TAG,
String.format("Calling notifyChanged for status %s", mStatus));
notifyChanged();
});
}
@Override
public boolean isSameItem(Preference preference) {
return preference instanceof SafetyStatusPreference
&& TextUtils.equals(getKey(), preference.getKey());
}
@Override
public boolean hasSameContents(Preference preference) {
if (!(preference instanceof SafetyStatusPreference)) {
return false;
}
SafetyStatusPreference other = (SafetyStatusPreference) preference;
return Objects.equals(mStatus, other.mStatus);
}
}