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 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
21 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
22 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
23 
24 import static java.util.Objects.requireNonNull;
25 
26 import android.app.AlertDialog;
27 import android.app.Dialog;
28 import android.content.Context;
29 import android.os.Bundle;
30 import android.safetycenter.SafetyCenterIssue;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.util.TypedValue;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.ViewGroup.MarginLayoutParams;
37 import android.widget.Button;
38 import android.widget.LinearLayout;
39 import android.widget.Space;
40 import android.widget.TextView;
41 
42 import androidx.annotation.ColorRes;
43 import androidx.annotation.Nullable;
44 import androidx.annotation.RequiresApi;
45 import androidx.appcompat.view.ContextThemeWrapper;
46 import androidx.core.content.ContextCompat;
47 import androidx.fragment.app.DialogFragment;
48 import androidx.fragment.app.FragmentManager;
49 import androidx.preference.Preference;
50 import androidx.preference.PreferenceViewHolder;
51 
52 import com.android.modules.utils.build.SdkLevel;
53 import com.android.permissioncontroller.R;
54 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel;
55 
56 import com.google.android.material.button.MaterialButton;
57 import com.google.android.material.shape.AbsoluteCornerSize;
58 import com.google.android.material.shape.CornerSize;
59 import com.google.android.material.shape.ShapeAppearanceModel;
60 
61 import java.util.Objects;
62 
63 /** A preference that displays a card representing a {@link SafetyCenterIssue}. */
64 @RequiresApi(TIRAMISU)
65 public class IssueCardPreference extends Preference implements ComparablePreference {
66 
67     public static final String TAG = IssueCardPreference.class.getSimpleName();
68 
69     private final IssueCardAnimator mIssueCardAnimator =
70             new IssueCardAnimator(this::markIssueResolvedUiCompleted);
71     private final SafetyCenterViewModel mSafetyCenterViewModel;
72     private final SafetyCenterIssue mIssue;
73     private final FragmentManager mDialogFragmentManager;
74     @Nullable private String mResolvedIssueActionId;
75     @Nullable private final Integer mTaskId;
76     private final boolean mIsDismissed;
77     private final PositionInCardList mPositionInCardList;
78 
IssueCardPreference( Context context, SafetyCenterViewModel safetyCenterViewModel, SafetyCenterIssue issue, @Nullable String resolvedIssueActionId, FragmentManager dialogFragmentManager, @Nullable Integer launchTaskId, boolean isDismissed, PositionInCardList positionInCardList)79     public IssueCardPreference(
80             Context context,
81             SafetyCenterViewModel safetyCenterViewModel,
82             SafetyCenterIssue issue,
83             @Nullable String resolvedIssueActionId,
84             FragmentManager dialogFragmentManager,
85             @Nullable Integer launchTaskId,
86             boolean isDismissed,
87             PositionInCardList positionInCardList) {
88         super(context);
89         setLayoutResource(R.layout.preference_issue_card);
90 
91         mSafetyCenterViewModel = requireNonNull(safetyCenterViewModel);
92         mIssue = requireNonNull(issue);
93         mDialogFragmentManager = dialogFragmentManager;
94         mResolvedIssueActionId = resolvedIssueActionId;
95         mTaskId = launchTaskId;
96         mIsDismissed = isDismissed;
97         mPositionInCardList = positionInCardList;
98     }
99 
100     @Override
onBindViewHolder(PreferenceViewHolder holder)101     public void onBindViewHolder(PreferenceViewHolder holder) {
102         super.onBindViewHolder(holder);
103 
104         holder.itemView.setBackgroundResource(mPositionInCardList.getBackgroundDrawableResId());
105         int topMargin = getTopMargin(mPositionInCardList, getContext());
106         MarginLayoutParams layoutParams = (MarginLayoutParams) holder.itemView.getLayoutParams();
107         if (layoutParams.topMargin != topMargin) {
108             layoutParams.topMargin = topMargin;
109             holder.itemView.setLayoutParams(layoutParams);
110         }
111 
112         // Set default group visibility in case view is being reused
113         holder.findViewById(R.id.default_issue_content).setVisibility(View.VISIBLE);
114         holder.findViewById(R.id.resolved_issue_content).setVisibility(View.GONE);
115 
116         configureDismissButton(holder.findViewById(R.id.issue_card_dismiss_btn));
117 
118         TextView titleTextView = (TextView) holder.findViewById(R.id.issue_card_title);
119         titleTextView.setText(mIssue.getTitle());
120         ((TextView) holder.findViewById(R.id.issue_card_summary)).setText(mIssue.getSummary());
121 
122         TextView attributionTitleTextView =
123                 (TextView) holder.findViewById(R.id.issue_card_attribution_title);
124         maybeDisplayText(
125                 SdkLevel.isAtLeastU() ? mIssue.getAttributionTitle() : null,
126                 attributionTitleTextView);
127 
128         TextView subtitleTextView = (TextView) holder.findViewById(R.id.issue_card_subtitle);
129         maybeDisplayText(mIssue.getSubtitle(), subtitleTextView);
130 
131         holder.itemView.setClickable(false);
132 
133         configureContentDescription(attributionTitleTextView, titleTextView);
134         configureButtonList(holder);
135         configureSafetyProtectionView(holder);
136         maybeStartResolutionAnimation(holder);
137 
138         mSafetyCenterViewModel.getInteractionLogger().recordIssueViewed(mIssue, mIsDismissed);
139     }
140 
maybeDisplayText(@ullable CharSequence maybeText, TextView textView)141     private void maybeDisplayText(@Nullable CharSequence maybeText, TextView textView) {
142         if (TextUtils.isEmpty(maybeText)) {
143             textView.setVisibility(View.GONE);
144         } else {
145             textView.setText(maybeText);
146             textView.setVisibility(View.VISIBLE);
147         }
148     }
149 
configureContentDescription( TextView attributionTitleTextView, TextView titleTextView)150     private void configureContentDescription(
151             TextView attributionTitleTextView, TextView titleTextView) {
152         TextView firstVisibleTextView;
153         if (attributionTitleTextView.getVisibility() == View.VISIBLE) {
154             // Attribution title might not be present for an issue, title always is.
155             firstVisibleTextView = attributionTitleTextView;
156 
157             // Clear the modified title description in case this view is reused.
158             titleTextView.setContentDescription(null);
159         } else {
160             firstVisibleTextView = titleTextView;
161         }
162 
163         // We would like to say "alert" before reading the content of the issue card. Best way to
164         // do that is by modifying the content description of the first view that would be read
165         // in the issue card.
166         firstVisibleTextView.setContentDescription(
167                 getContext()
168                         .getString(
169                                 R.string.safety_center_issue_card_prefix_content_description,
170                                 firstVisibleTextView.getText()));
171     }
172 
configureButtonList(PreferenceViewHolder holder)173     private void configureButtonList(PreferenceViewHolder holder) {
174         LinearLayout buttonList =
175                 ((LinearLayout) holder.findViewById(R.id.issue_card_action_button_list));
176         buttonList.removeAllViews(); // This view may be recycled from another issue
177 
178         for (int i = 0; i < mIssue.getActions().size(); i++) {
179             SafetyCenterIssue.Action action = mIssue.getActions().get(i);
180             ActionButtonBuilder builder =
181                     new ActionButtonBuilder(action, holder.itemView.getContext())
182                             .setIndex(i)
183                             .setActionButtonListSize(mIssue.getActions().size())
184                             .setIsDismissed(mIsDismissed)
185                             .setIsLargeScreen(buttonList instanceof EqualWidthContainer);
186             builder.buildAndAddToView(buttonList);
187         }
188     }
189 
getTopMargin(PositionInCardList position, Context context)190     private int getTopMargin(PositionInCardList position, Context context) {
191         switch (position) {
192             case LIST_START_END:
193             case LIST_START_CARD_END:
194                 return context.getResources()
195                         .getDimensionPixelSize(
196                                 mIsDismissed ? R.dimen.sc_card_margin : R.dimen.sc_spacing_large);
197             default:
198                 return position.getTopMargin(context);
199         }
200     }
201 
configureSafetyProtectionView(PreferenceViewHolder holder)202     private void configureSafetyProtectionView(PreferenceViewHolder holder) {
203         View safetyProtectionSectionView =
204                 holder.findViewById(R.id.issue_card_protected_by_android);
205         if (safetyProtectionSectionView.getVisibility() == View.GONE) {
206             holder.itemView.setPaddingRelative(
207                     holder.itemView.getPaddingStart(),
208                     holder.itemView.getPaddingTop(),
209                     holder.itemView.getPaddingEnd(),
210                     /* bottom= */ getContext()
211                             .getResources()
212                             .getDimensionPixelSize(R.dimen.sc_card_margin_bottom));
213         } else {
214             holder.itemView.setPaddingRelative(
215                     holder.itemView.getPaddingStart(),
216                     holder.itemView.getPaddingTop(),
217                     holder.itemView.getPaddingEnd(),
218                     /* bottom= */ 0);
219         }
220     }
221 
maybeStartResolutionAnimation(PreferenceViewHolder holder)222     private void maybeStartResolutionAnimation(PreferenceViewHolder holder) {
223         if (mResolvedIssueActionId == null) {
224             return;
225         }
226 
227         for (SafetyCenterIssue.Action action : mIssue.getActions()) {
228             if (action.getId().equals(mResolvedIssueActionId)) {
229                 mIssueCardAnimator.transitionToIssueResolvedThenMarkComplete(
230                         getContext(), holder, action);
231             }
232         }
233     }
234 
getSeverityLevel()235     public int getSeverityLevel() {
236         return mIssue.getSeverityLevel();
237     }
238 
configureDismissButton(View dismissButton)239     private void configureDismissButton(View dismissButton) {
240         if (mIssue.isDismissible() && !mIsDismissed) {
241             dismissButton.setOnClickListener(
242                     mIssue.shouldConfirmDismissal()
243                             ? new ConfirmDismissalOnClickListener()
244                             : new DismissOnClickListener());
245             dismissButton.setVisibility(View.VISIBLE);
246 
247             SafetyCenterTouchTarget.configureSize(
248                     dismissButton, R.dimen.sc_icon_button_touch_target_size);
249         } else {
250             dismissButton.setVisibility(View.GONE);
251         }
252     }
253 
254     @Override
isSameItem(Preference preference)255     public boolean isSameItem(Preference preference) {
256         return (preference instanceof IssueCardPreference)
257                 && TextUtils.equals(
258                         mIssue.getId(), ((IssueCardPreference) preference).mIssue.getId());
259     }
260 
261     @Override
hasSameContents(Preference preference)262     public boolean hasSameContents(Preference preference) {
263         return (preference instanceof IssueCardPreference)
264                 && mIssue.equals(((IssueCardPreference) preference).mIssue)
265                 && Objects.equals(
266                         mResolvedIssueActionId,
267                         ((IssueCardPreference) preference).mResolvedIssueActionId)
268                 && mIsDismissed == ((IssueCardPreference) preference).mIsDismissed
269                 && mPositionInCardList == ((IssueCardPreference) preference).mPositionInCardList;
270     }
271 
272     private class DismissOnClickListener implements View.OnClickListener {
273         @Override
onClick(View v)274         public void onClick(View v) {
275             mSafetyCenterViewModel.dismissIssue(mIssue);
276             mSafetyCenterViewModel
277                     .getInteractionLogger()
278                     .recordForIssue(Action.ISSUE_DISMISS_CLICKED, mIssue, mIsDismissed);
279         }
280     }
281 
282     private class ConfirmDismissalOnClickListener implements View.OnClickListener {
283         @Override
onClick(View v)284         public void onClick(View v) {
285             ConfirmDismissalDialogFragment.newInstance(mIssue)
286                     .showNow(mDialogFragmentManager, /* tag= */ null);
287         }
288     }
289 
290     /** Fragment to display a dismissal confirmation dialog for an {@link IssueCardPreference}. */
291     public static class ConfirmDismissalDialogFragment extends DialogFragment {
292         private static final String ISSUE_KEY = "confirm_dialog_sc_issue";
293 
newInstance(SafetyCenterIssue issue)294         private static ConfirmDismissalDialogFragment newInstance(SafetyCenterIssue issue) {
295             ConfirmDismissalDialogFragment fragment = new ConfirmDismissalDialogFragment();
296 
297             Bundle args = new Bundle();
298             args.putParcelable(ISSUE_KEY, issue);
299             fragment.setArguments(args);
300 
301             return fragment;
302         }
303 
304         @Override
onCreateDialog(@ullable Bundle savedInstanceState)305         public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
306             SafetyCenterViewModel safetyCenterViewModel =
307                     ((SafetyCenterFragment) requireParentFragment()).getSafetyCenterViewModel();
308             SafetyCenterIssue issue =
309                     requireNonNull(
310                             requireArguments().getParcelable(ISSUE_KEY, SafetyCenterIssue.class));
311             return new AlertDialog.Builder(getContext())
312                     .setTitle(R.string.safety_center_issue_card_dismiss_confirmation_title)
313                     .setMessage(R.string.safety_center_issue_card_dismiss_confirmation_message)
314                     .setPositiveButton(
315                             R.string.safety_center_issue_card_confirm_dismiss_button,
316                             (dialog, which) -> {
317                                 safetyCenterViewModel.dismissIssue(issue);
318                                 safetyCenterViewModel
319                                         .getInteractionLogger()
320                                         .recordForIssue(
321                                                 Action.ISSUE_DISMISS_CLICKED,
322                                                 issue,
323                                                 // You can only dismiss non-dismissed issues
324                                                 /* isDismissed= */ false);
325                             })
326                     .setNegativeButton(
327                             R.string.safety_center_issue_card_cancel_dismiss_button, null)
328                     .create();
329         }
330     }
331 
332     /** A dialog to prompt for a confirmation to performn an Action. */
333     @RequiresApi(UPSIDE_DOWN_CAKE)
334     public static class ConfirmActionDialogFragment extends DialogFragment {
335         private static final String ISSUE_KEY = "issue";
336         private static final String ACTION_KEY = "action";
337         private static final String TASK_ID_KEY = "taskId";
338         private static final String IS_PRIMARY_BUTTON_KEY = "isPrimaryButton";
339         private static final String IS_DISMISSED_KEY = "isDismissed";
340 
341         /** Create new fragment with the data it will need. */
newInstance( SafetyCenterIssue issue, SafetyCenterIssue.Action action, @Nullable Integer taskId, boolean isFirstButton, boolean isDismissed)342         public static ConfirmActionDialogFragment newInstance(
343                 SafetyCenterIssue issue,
344                 SafetyCenterIssue.Action action,
345                 @Nullable Integer taskId,
346                 boolean isFirstButton,
347                 boolean isDismissed) {
348             ConfirmActionDialogFragment fragment = new ConfirmActionDialogFragment();
349 
350             Bundle args = new Bundle();
351             args.putParcelable(ISSUE_KEY, issue);
352             args.putParcelable(ACTION_KEY, action);
353             args.putBoolean(IS_PRIMARY_BUTTON_KEY, isFirstButton);
354             args.putBoolean(IS_DISMISSED_KEY, isDismissed);
355 
356             if (taskId != null) {
357                 args.putInt(TASK_ID_KEY, taskId);
358             }
359 
360             fragment.setArguments(args);
361 
362             return fragment;
363         }
364 
365         @Override
onCreateDialog(Bundle savedInstanceState)366         public Dialog onCreateDialog(Bundle savedInstanceState) {
367             SafetyCenterViewModel safetyCenterViewModel =
368                     ((SafetyCenterFragment) requireParentFragment()).getSafetyCenterViewModel();
369             SafetyCenterIssue issue =
370                     requireNonNull(
371                             requireArguments().getParcelable(ISSUE_KEY, SafetyCenterIssue.class));
372             SafetyCenterIssue.Action action =
373                     requireNonNull(
374                             requireArguments()
375                                     .getParcelable(ACTION_KEY, SafetyCenterIssue.Action.class));
376             boolean isPrimaryButton = requireArguments().getBoolean(IS_PRIMARY_BUTTON_KEY);
377             boolean isDismissed = requireArguments().getBoolean(IS_DISMISSED_KEY);
378 
379             Integer taskId =
380                     requireArguments().containsKey(TASK_ID_KEY)
381                             ? requireArguments().getInt(TASK_ID_KEY)
382                             : null;
383 
384             return new AlertDialog.Builder(getContext())
385                     .setTitle(action.getConfirmationDialogDetails().getTitle())
386                     .setMessage(action.getConfirmationDialogDetails().getText())
387                     .setPositiveButton(
388                             action.getConfirmationDialogDetails().getAcceptButtonText(),
389                             (dialog, which) -> {
390                                 safetyCenterViewModel.executeIssueAction(issue, action, taskId);
391                                 // TODO(b/269097766): Is this the best logging model?
392                                 safetyCenterViewModel
393                                         .getInteractionLogger()
394                                         .recordForIssue(
395                                                 isPrimaryButton
396                                                         ? Action.ISSUE_PRIMARY_ACTION_CLICKED
397                                                         : Action.ISSUE_SECONDARY_ACTION_CLICKED,
398                                                 issue,
399                                                 isDismissed);
400                             })
401                     .setNegativeButton(
402                             action.getConfirmationDialogDetails().getDenyButtonText(), null)
403                     .create();
404         }
405     }
406 
407     private void markIssueResolvedUiCompleted() {
408         if (mResolvedIssueActionId != null) {
409             mResolvedIssueActionId = null;
410             mSafetyCenterViewModel.markIssueResolvedUiCompleted(mIssue.getId());
411         }
412     }
413 
414     private class ActionButtonBuilder {
415         private final SafetyCenterIssue.Action mAction;
416         private final Context mContext;
417         private final ContextThemeWrapper mContextThemeWrapper;
418         private int mIndex;
419         private int mActionButtonListSize;
420         private boolean mIsDismissed = false;
421         private boolean mIsLargeScreen = false;
422 
423         ActionButtonBuilder(SafetyCenterIssue.Action action, Context context) {
424             mAction = action;
425             mContext = context;
426 
427             TypedValue buttonThemeValue = new TypedValue();
428             mContext.getTheme()
429                     .resolveAttribute(
430                             R.attr.scActionButtonTheme,
431                             buttonThemeValue,
432                             /* resolveRefs= */ false);
433             mContextThemeWrapper = new ContextThemeWrapper(context, buttonThemeValue.data);
434         }
435 
436         public ActionButtonBuilder setIndex(int index) {
437             mIndex = index;
438             return this;
439         }
440 
441         public ActionButtonBuilder setActionButtonListSize(int actionButtonListSize) {
442             mActionButtonListSize = actionButtonListSize;
443             return this;
444         }
445 
446         public ActionButtonBuilder setIsDismissed(boolean isDismissed) {
447             mIsDismissed = isDismissed;
448             return this;
449         }
450 
451         public ActionButtonBuilder setIsLargeScreen(boolean isLargeScreen) {
452             mIsLargeScreen = isLargeScreen;
453             return this;
454         }
455 
456         private boolean isPrimaryButton() {
457             return mIndex == 0;
458         }
459 
460         private boolean isLastButton() {
461             return mIndex == (mActionButtonListSize - 1);
462         }
463 
464         private boolean isFilled() {
465             return isPrimaryButton() && !mIsDismissed;
466         }
467 
468         public void buildAndAddToView(LinearLayout buttonList) {
469             MaterialButton button = new MaterialButton(mContextThemeWrapper, null, getStyle());
470             if (SdkLevel.isAtLeastU() && !mIsLargeScreen) {
471                 configureGroupStyleCorners(button);
472             }
473             setButtonColors(button);
474             setButtonLayout(button);
475             button.setText(mAction.getLabel());
476             button.setEnabled(!mAction.isInFlight());
477             button.setOnClickListener(
478                     view -> {
479                         if (SdkLevel.isAtLeastU()
480                                 && mAction.getConfirmationDialogDetails() != null) {
481                             ConfirmActionDialogFragment.newInstance(
482                                             mIssue,
483                                             mAction,
484                                             mTaskId,
485                                             isPrimaryButton(),
486                                             mIsDismissed)
487                                     .showNow(mDialogFragmentManager, /* tag= */ null);
488                         } else {
489                             if (mAction.willResolve()) {
490                                 // Without a confirmation, the button remains tappable. Disable the
491                                 // button to prevent double-taps.
492                                 // We ideally want to do this on any button press, however out of an
493                                 // abundance of caution we only do it with actions that indicate
494                                 // they will resolve (and therefore we can rely on a model update to
495                                 // redraw state - either to isInFlight() or simply resolving the
496                                 // issue.
497                                 button.setEnabled(false);
498                             }
499                             mSafetyCenterViewModel.executeIssueAction(mIssue, mAction, mTaskId);
500                             mSafetyCenterViewModel
501                                     .getInteractionLogger()
502                                     .recordForIssue(
503                                             isPrimaryButton()
504                                                     ? Action.ISSUE_PRIMARY_ACTION_CLICKED
505                                                     : Action.ISSUE_SECONDARY_ACTION_CLICKED,
506                                             mIssue,
507                                             mIsDismissed);
508                         }
509                     });
510 
511             maybeAddSpaceToView(buttonList);
512             buttonList.addView(button);
513         }
514 
515         /**
516          * Configures "group-style" corners for this button, where the first button in the list has
517          * large corners on top and the last button in the list has large corners on bottom.
518          */
519         @RequiresApi(UPSIDE_DOWN_CAKE)
520         private void configureGroupStyleCorners(MaterialButton button) {
521             button.setCornerRadiusResource(R.dimen.sc_button_corner_radius_small);
522             ShapeAppearanceModel.Builder shapeAppearanceModelBuilder =
523                     button.getShapeAppearanceModel().toBuilder();
524 
525             CornerSize largeCornerSize =
526                     new AbsoluteCornerSize(
527                             mContext.getResources()
528                                     .getDimensionPixelSize(R.dimen.sc_button_corner_radius));
529             if (isPrimaryButton()) {
530                 shapeAppearanceModelBuilder
531                         .setTopLeftCornerSize(largeCornerSize)
532                         .setTopRightCornerSize(largeCornerSize);
533             }
534             if (isLastButton()) {
535                 shapeAppearanceModelBuilder
536                         .setBottomLeftCornerSize(largeCornerSize)
537                         .setBottomRightCornerSize(largeCornerSize);
538             }
539 
540             button.setShapeAppearanceModel(shapeAppearanceModelBuilder.build());
541         }
542 
543         private void maybeAddSpaceToView(LinearLayout buttonList) {
544             if (isPrimaryButton()) {
545                 return;
546             }
547 
548             int marginRes =
549                     mIsLargeScreen
550                             ? R.dimen.sc_action_button_list_margin_large_screen
551                             : R.dimen.sc_action_button_list_margin;
552             int margin = mContext.getResources().getDimensionPixelSize(marginRes);
553             Space space = new Space(mContext);
554             space.setLayoutParams(new ViewGroup.LayoutParams(margin, margin));
555             buttonList.addView(space);
556         }
557 
558         private int getStyle() {
559             return isFilled() ? R.attr.scActionButtonStyle : R.attr.scSecondaryActionButtonStyle;
560         }
561 
562         private void setButtonColors(MaterialButton button) {
563             if (isFilled()) {
564                 button.setBackgroundTintList(
565                         ContextCompat.getColorStateList(
566                                 mContext,
567                                 getPrimaryButtonColorFromSeverity(mIssue.getSeverityLevel())));
568             } else {
569                 button.setStrokeColor(
570                         ContextCompat.getColorStateList(
571                                 mContext,
572                                 getSecondaryButtonStrokeColorFromSeverity(
573                                         mIssue.getSeverityLevel())));
574             }
575         }
576 
577         private void setButtonLayout(Button button) {
578             MarginLayoutParams layoutParams = new MarginLayoutParams(layoutWidth(), WRAP_CONTENT);
579             button.setLayoutParams(layoutParams);
580         }
581 
582         private int layoutWidth() {
583             if (mIsLargeScreen) {
584                 return WRAP_CONTENT;
585             } else {
586                 return MATCH_PARENT;
587             }
588         }
589 
590         @ColorRes
591         private int getPrimaryButtonColorFromSeverity(int issueSeverityLevel) {
592             return pickColorForSeverityLevel(
593                     issueSeverityLevel,
594                     R.color.safety_center_button_info,
595                     R.color.safety_center_button_recommend,
596                     R.color.safety_center_button_warn);
597         }
598 
599         @ColorRes
600         private int getSecondaryButtonStrokeColorFromSeverity(int issueSeverityLevel) {
601             return pickColorForSeverityLevel(
602                     issueSeverityLevel,
603                     R.color.safety_center_outline_button_info,
604                     R.color.safety_center_outline_button_recommend,
605                     R.color.safety_center_outline_button_warn);
606         }
607 
608         @ColorRes
609         private int pickColorForSeverityLevel(
610                 int issueSeverityLevel,
611                 @ColorRes int infoColor,
612                 @ColorRes int recommendColor,
613                 @ColorRes int warnColor) {
614             switch (issueSeverityLevel) {
615                 case SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK:
616                     return infoColor;
617                 case SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION:
618                     return recommendColor;
619                 case SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING:
620                     return warnColor;
621                 default:
622                     Log.w(
623                             TAG,
624                             String.format("Unexpected issueSeverityLevel: %s", issueSeverityLevel));
625                     return infoColor;
626             }
627         }
628     }
629 }
630