blob: 62fb8ca4e2662f142b0d7afa7f80e68bd69be874 [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 static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static java.util.Objects.requireNonNull;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.safetycenter.SafetyCenterIssue;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.permissioncontroller.R;
import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel;
import com.android.safetycenter.internaldata.SafetyCenterIds;
import com.android.safetycenter.internaldata.SafetyCenterIssueId;
import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
import com.google.android.material.button.MaterialButton;
import java.util.Objects;
/** A preference that displays a card representing a {@link SafetyCenterIssue}. */
@RequiresApi(TIRAMISU)
public class IssueCardPreference extends Preference implements ComparablePreference {
public static final String TAG = IssueCardPreference.class.getSimpleName();
private final IssueCardAnimator mIssueCardAnimator =
new IssueCardAnimator(this::markIssueResolvedUiCompleted);
private final SafetyCenterViewModel mSafetyCenterViewModel;
private final SafetyCenterIssue mIssue;
private final FragmentManager mDialogFragmentManager;
private final SafetyCenterIssueId mDecodedIssueId;
@Nullable private String mResolvedIssueActionId;
@Nullable private final Integer mTaskId;
public IssueCardPreference(
Context context,
SafetyCenterViewModel safetyCenterViewModel,
SafetyCenterIssue issue,
@Nullable String resolvedIssueActionId,
FragmentManager dialogFragmentManager,
@Nullable Integer launchTaskId) {
super(context);
setLayoutResource(R.layout.preference_issue_card);
mSafetyCenterViewModel = requireNonNull(safetyCenterViewModel);
mIssue = requireNonNull(issue);
mDialogFragmentManager = dialogFragmentManager;
mDecodedIssueId = SafetyCenterIds.issueIdFromString(mIssue.getId());
mResolvedIssueActionId = resolvedIssueActionId;
mTaskId = launchTaskId;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
// Set default group visibility in case view is being reused
holder.findViewById(R.id.default_issue_content).setVisibility(View.VISIBLE);
holder.findViewById(R.id.resolved_issue_content).setVisibility(View.GONE);
configureDismissButton(holder.findViewById(R.id.issue_card_dismiss_btn));
((TextView) holder.findViewById(R.id.issue_card_title)).setText(mIssue.getTitle());
((TextView) holder.findViewById(R.id.issue_card_summary)).setText(mIssue.getSummary());
CharSequence subtitle = mIssue.getSubtitle();
TextView subtitleTextView = (TextView) holder.findViewById(R.id.issue_card_subtitle);
CharSequence contentDescription;
if (TextUtils.isEmpty(subtitle)) {
subtitleTextView.setVisibility(View.GONE);
contentDescription =
getContext()
.getString(
R.string.safety_center_issue_card_content_description,
mIssue.getTitle(),
mIssue.getSummary());
} else {
subtitleTextView.setText(subtitle);
subtitleTextView.setVisibility(View.VISIBLE);
int contentDescriptionResId =
R.string.safety_center_issue_card_content_description_with_subtitle;
contentDescription =
getContext()
.getString(
contentDescriptionResId,
mIssue.getTitle(),
mIssue.getSubtitle(),
mIssue.getSummary());
}
holder.itemView.setContentDescription(contentDescription);
holder.itemView.setClickable(false);
LinearLayout buttonList =
((LinearLayout) holder.findViewById(R.id.issue_card_action_button_list));
buttonList.removeAllViews(); // This view may be recycled from another issue
boolean isFirstButton = true;
for (SafetyCenterIssue.Action action : mIssue.getActions()) {
buttonList.addView(
buildActionButton(action, holder.itemView.getContext(), isFirstButton));
isFirstButton = false;
if (mResolvedIssueActionId != null && mResolvedIssueActionId.equals(action.getId())) {
mIssueCardAnimator.transitionToIssueResolvedThenMarkComplete(
getContext(), holder, action);
}
}
configureSafetyProtectionView(holder);
mSafetyCenterViewModel
.getInteractionLogger()
.recordForIssue(Action.SAFETY_ISSUE_VIEWED, mIssue);
}
private void configureSafetyProtectionView(PreferenceViewHolder holder) {
View safetyProtectionSectionView =
holder.findViewById(R.id.issue_card_protected_by_android);
if (safetyProtectionSectionView.getVisibility() == View.GONE) {
holder.itemView.setPaddingRelative(
holder.itemView.getPaddingStart(),
holder.itemView.getPaddingTop(),
holder.itemView.getPaddingEnd(),
/* bottom = */ getContext()
.getResources()
.getDimensionPixelSize(R.dimen.safety_center_card_margin_bottom));
} else {
holder.itemView.setPaddingRelative(
holder.itemView.getPaddingStart(),
holder.itemView.getPaddingTop(),
holder.itemView.getPaddingEnd(),
/* bottom = */ 0);
}
}
public int getSeverityLevel() {
return mIssue.getSeverityLevel();
}
/** Returns the {@link SafetyCenterIssueKey} associated with this {@link IssueCardPreference} */
public SafetyCenterIssueKey getIssueKey() {
return mDecodedIssueId.getSafetyCenterIssueKey();
}
private void configureDismissButton(View dismissButton) {
if (mIssue.isDismissible()) {
dismissButton.setOnClickListener(
mIssue.shouldConfirmDismissal()
? new ConfirmDismissalOnClickListener()
: new DismissOnClickListener());
dismissButton.setVisibility(View.VISIBLE);
SafetyCenterTouchTarget.configureSize(
dismissButton, R.dimen.safety_center_icon_button_touch_target_size);
} else {
dismissButton.setVisibility(View.GONE);
}
}
@Override
public boolean isSameItem(@NonNull Preference preference) {
return (preference instanceof IssueCardPreference)
&& TextUtils.equals(
mIssue.getId(), ((IssueCardPreference) preference).mIssue.getId());
}
@Override
public boolean hasSameContents(@NonNull Preference preference) {
return (preference instanceof IssueCardPreference)
&& mIssue.equals(((IssueCardPreference) preference).mIssue)
&& Objects.equals(
mResolvedIssueActionId,
((IssueCardPreference) preference).mResolvedIssueActionId);
}
private class DismissOnClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
mSafetyCenterViewModel.dismissIssue(mIssue);
mSafetyCenterViewModel
.getInteractionLogger()
.recordForIssue(Action.ISSUE_DISMISS_CLICKED, mIssue);
}
}
private class ConfirmDismissalOnClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
ConfirmDismissalDialogFragment.newInstance(mIssue)
.showNow(mDialogFragmentManager, /* tag= */ null);
}
}
/** Fragment to display a dismissal confirmation dialog for an {@link IssueCardPreference}. */
public static class ConfirmDismissalDialogFragment extends DialogFragment {
private static final String ISSUE_KEY = "confirm_dialog_sc_issue";
private static ConfirmDismissalDialogFragment newInstance(SafetyCenterIssue issue) {
ConfirmDismissalDialogFragment fragment = new ConfirmDismissalDialogFragment();
Bundle args = new Bundle();
args.putParcelable(ISSUE_KEY, issue);
fragment.setArguments(args);
return fragment;
}
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
SafetyCenterViewModel safetyCenterViewModel =
((SafetyCenterDashboardFragment) requireParentFragment())
.getSafetyCenterViewModel();
SafetyCenterIssue issue =
requireNonNull(
requireArguments().getParcelable(ISSUE_KEY, SafetyCenterIssue.class));
return new AlertDialog.Builder(getContext())
.setTitle(R.string.safety_center_issue_card_dismiss_confirmation_title)
.setMessage(R.string.safety_center_issue_card_dismiss_confirmation_message)
.setPositiveButton(
R.string.safety_center_issue_card_confirm_dismiss_button,
(dialog, which) -> {
safetyCenterViewModel.dismissIssue(issue);
safetyCenterViewModel
.getInteractionLogger()
.recordForIssue(Action.ISSUE_DISMISS_CLICKED, issue);
})
.setNegativeButton(
R.string.safety_center_issue_card_cancel_dismiss_button, null)
.create();
}
}
private Button buildActionButton(
SafetyCenterIssue.Action action, Context context, boolean isFirstButton) {
Button button =
isFirstButton ? createFirstButton(context) : createSubsequentButton(context);
button.setText(action.getLabel());
button.setEnabled(!action.isInFlight());
button.setOnClickListener(
(view) -> {
if (action.willResolve()) {
// Disable the button to prevent double-taps.
// We ideally want to do this on any button press, however out of an
// abundance of caution we only do it with actions that indicate they will
// resolve (and therefore we can rely on a model update to redraw state).
// We expect the model to update with either isInFlight() or simply
// removing/updating the issue.
button.setEnabled(false);
}
mSafetyCenterViewModel.executeIssueAction(mIssue, action, mTaskId);
mSafetyCenterViewModel
.getInteractionLogger()
.recordForIssue(
isFirstButton
? Action.ISSUE_PRIMARY_ACTION_CLICKED
: Action.ISSUE_SECONDARY_ACTION_CLICKED,
mIssue);
});
return button;
}
private Button createFirstButton(Context context) {
ContextThemeWrapper themedContext =
new ContextThemeWrapper(context, R.style.Theme_MaterialComponents_DayNight);
Button button = new MaterialButton(themedContext, null, R.attr.scActionButtonStyle);
button.setBackgroundTintList(
ContextCompat.getColorStateList(
context, getPrimaryButtonColorFromSeverity(mIssue.getSeverityLevel())));
button.setLayoutParams(new ViewGroup.MarginLayoutParams(MATCH_PARENT, WRAP_CONTENT));
return button;
}
private Button createSubsequentButton(Context context) {
ContextThemeWrapper themedContext =
new ContextThemeWrapper(context, R.style.Theme_MaterialComponents_DayNight);
MaterialButton button =
new MaterialButton(themedContext, null, R.attr.scSecondaryActionButtonStyle);
button.setStrokeColor(
ContextCompat.getColorStateList(
context,
getSecondaryButtonStrokeColorFromSeverity(mIssue.getSeverityLevel())));
int margin =
context.getResources()
.getDimensionPixelSize(R.dimen.safety_center_action_button_list_margin);
ViewGroup.MarginLayoutParams layoutParams =
new ViewGroup.MarginLayoutParams(MATCH_PARENT, WRAP_CONTENT);
layoutParams.setMargins(0, margin, 0, 0);
button.setLayoutParams(layoutParams);
return button;
}
@ColorRes
private static int getPrimaryButtonColorFromSeverity(int issueSeverityLevel) {
return pickColorForSeverityLevel(
issueSeverityLevel,
R.color.safety_center_button_info,
R.color.safety_center_button_recommend,
R.color.safety_center_button_warn);
}
@ColorRes
private static int getSecondaryButtonStrokeColorFromSeverity(int issueSeverityLevel) {
return pickColorForSeverityLevel(
issueSeverityLevel,
R.color.safety_center_outline_button_info,
R.color.safety_center_outline_button_recommend,
R.color.safety_center_outline_button_warn);
}
@ColorRes
private static int pickColorForSeverityLevel(
int issueSeverityLevel,
@ColorRes int infoColor,
@ColorRes int recommendColor,
@ColorRes int warnColor) {
switch (issueSeverityLevel) {
case SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK:
return infoColor;
case SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION:
return recommendColor;
case SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING:
return warnColor;
default:
Log.w(TAG, String.format("Unexpected issueSeverityLevel: %s", issueSeverityLevel));
return infoColor;
}
}
private void markIssueResolvedUiCompleted() {
if (mResolvedIssueActionId != null) {
mResolvedIssueActionId = null;
mSafetyCenterViewModel.markIssueResolvedUiCompleted(mIssue.getId());
}
}
}