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.Manifest.permission_group.CAMERA;
20 import static android.Manifest.permission_group.LOCATION;
21 import static android.Manifest.permission_group.MICROPHONE;
22 import static android.os.Build.VERSION_CODES.TIRAMISU;
23 
24 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID;
25 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID;
26 
27 import android.content.Context;
28 import android.content.Intent;
29 import android.graphics.Color;
30 import android.graphics.Insets;
31 import android.graphics.drawable.Drawable;
32 import android.graphics.drawable.LayerDrawable;
33 import android.os.Bundle;
34 import android.os.UserHandle;
35 import android.permission.PermissionGroupUsage;
36 import android.permission.PermissionManager;
37 import android.transition.AutoTransition;
38 import android.transition.TransitionManager;
39 import android.util.ArrayMap;
40 import android.util.TypedValue;
41 import android.view.Gravity;
42 import android.view.LayoutInflater;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.view.WindowInsets;
46 import android.widget.Button;
47 import android.widget.ImageView;
48 import android.widget.LinearLayout;
49 import android.widget.TextView;
50 
51 import androidx.annotation.ColorInt;
52 import androidx.annotation.Nullable;
53 import androidx.annotation.RequiresApi;
54 import androidx.constraintlayout.widget.ConstraintLayout;
55 import androidx.core.view.ViewCompat;
56 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
57 import androidx.fragment.app.Fragment;
58 import androidx.lifecycle.ViewModelProvider;
59 
60 import com.android.permissioncontroller.R;
61 import com.android.permissioncontroller.permission.utils.KotlinUtils;
62 import com.android.permissioncontroller.permission.utils.Utils;
63 import com.android.permissioncontroller.safetycenter.ui.model.LiveSafetyCenterViewModelFactory;
64 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterQsViewModel;
65 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterQsViewModel.SensorState;
66 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterQsViewModelFactory;
67 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel;
68 import com.android.settingslib.RestrictedLockUtils;
69 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
70 
71 import com.google.android.material.button.MaterialButton;
72 
73 import java.util.ArrayList;
74 import java.util.Collections;
75 import java.util.List;
76 import java.util.Map;
77 
78 /**
79  * The Quick Settings fragment for the safety center. Displays information to the user about the
80  * current safety and privacy status of their device, including showing mic/camera usage, and having
81  * mic/camera/location toggles.
82  */
83 @RequiresApi(TIRAMISU)
84 public class SafetyCenterQsFragment extends Fragment {
85     private static final List<String> TOGGLE_BUTTONS = List.of(CAMERA, MICROPHONE, LOCATION);
86     private static final String SETTINGS_TOGGLE_TAG = "settings_toggle";
87     private static final int MAX_TOGGLES_PER_ROW = 2;
88 
89     private Context mContext;
90     private long mSessionId;
91     private List<PermissionGroupUsage> mPermGroupUsages;
92     private SafetyCenterQsViewModel mViewModel;
93     private boolean mIsPermissionUsageReady;
94     private boolean mAreSensorTogglesReady;
95 
96     private SafetyCenterViewModel mSafetyCenterViewModel;
97 
98     /**
99      * Create instance of SafetyCenterDashboardFragment with the arguments set
100      *
101      * @param sessionId The current session Id
102      * @param usages ArrayList of PermissionGroupUsage
103      * @return SafetyCenterQsFragment with the arguments set
104      */
newInstance( long sessionId, ArrayList<PermissionGroupUsage> usages)105     public static SafetyCenterQsFragment newInstance(
106             long sessionId, ArrayList<PermissionGroupUsage> usages) {
107         Bundle args = new Bundle();
108         args.putLong(EXTRA_SESSION_ID, sessionId);
109         args.putParcelableArrayList(PermissionManager.EXTRA_PERMISSION_USAGES, usages);
110         SafetyCenterQsFragment frag = new SafetyCenterQsFragment();
111         frag.setArguments(args);
112         return frag;
113     }
114 
115     @Override
onCreate(Bundle savedInstanceState)116     public void onCreate(Bundle savedInstanceState) {
117         super.onCreate(savedInstanceState);
118 
119         mSessionId = INVALID_SESSION_ID;
120         if (getArguments() != null) {
121             mSessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
122         }
123         mContext = getContext();
124 
125         mPermGroupUsages =
126                 getArguments().getParcelableArrayList(PermissionManager.EXTRA_PERMISSION_USAGES);
127         if (mPermGroupUsages == null) {
128             mPermGroupUsages = new ArrayList<>();
129         }
130 
131         getActivity().setTheme(R.style.Theme_SafetyCenterQs);
132 
133         SafetyCenterQsViewModelFactory factory =
134                 new SafetyCenterQsViewModelFactory(
135                         getActivity().getApplication(), mSessionId, mPermGroupUsages);
136         mViewModel =
137                 new ViewModelProvider(requireActivity(), factory)
138                         .get(SafetyCenterQsViewModel.class);
139         mViewModel.getSensorPrivacyLiveData().observe(this, this::setSensorToggleState);
140         // LightAppPermGroupLiveDatas are kept track of in the view model,
141         // we need to start observing them here
142         if (!mPermGroupUsages.isEmpty()) {
143             mViewModel.getPermDataLoadedLiveData().observe(this, this::onPermissionGroupsLoaded);
144         } else {
145             mIsPermissionUsageReady = true;
146         }
147     }
148 
149     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)150     public View onCreateView(
151             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
152         ViewGroup root = (ViewGroup) inflater.inflate(R.layout.safety_center_qs, container, false);
153         root.setVisibility(View.GONE);
154         root.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS);
155         root.setOnApplyWindowInsetsListener((v, w) -> {
156             final Insets insets = w.getInsets(WindowInsets.Type.systemBars());
157             v.setPadding(insets.left, insets.top, insets.right, insets.bottom);
158             return WindowInsets.CONSUMED;
159         });
160 
161         View closeButton = root.findViewById(R.id.close_button);
162         closeButton.setOnClickListener((v) -> requireActivity().finish());
163         SafetyCenterTouchTarget.configureSize(
164                 closeButton, R.dimen.sc_icon_button_touch_target_size);
165 
166         mSafetyCenterViewModel =
167                 new ViewModelProvider(
168                                 requireActivity(),
169                                 new LiveSafetyCenterViewModelFactory(
170                                         requireActivity().getApplication()))
171                         .get(SafetyCenterViewModel.class);
172 
173         getChildFragmentManager()
174                 .beginTransaction()
175                 .add(
176                         R.id.safety_center_prefs,
177                         SafetyCenterDashboardFragment.newInstance(
178                                 mSessionId, /* isQuickSettingsFragment= */ true))
179                 .commitNow();
180         return root;
181     }
182 
maybeEnableView(@ullable View rootView)183     private void maybeEnableView(@Nullable View rootView) {
184         if (rootView == null) {
185             return;
186         }
187         if (mIsPermissionUsageReady && mAreSensorTogglesReady) {
188             rootView.setVisibility(View.VISIBLE);
189         }
190     }
191 
onPermissionGroupsLoaded(boolean initialized)192     private void onPermissionGroupsLoaded(boolean initialized) {
193         if (initialized) {
194             if (!mIsPermissionUsageReady) {
195                 mIsPermissionUsageReady = true;
196                 maybeEnableView(getView());
197             }
198             addPermissionUsageInformation(getView());
199         }
200     }
201 
addPermissionUsageInformation(@ullable View rootView)202     private void addPermissionUsageInformation(@Nullable View rootView) {
203         if (rootView == null) {
204             return;
205         }
206         View permissionSectionTitleView = rootView.findViewById(R.id.permission_section_title);
207         View statusSectionTitleView = rootView.findViewById(R.id.status_section_title);
208         if (mPermGroupUsages == null || mPermGroupUsages.isEmpty()) {
209             permissionSectionTitleView.setVisibility(View.GONE);
210             statusSectionTitleView.setVisibility(View.GONE);
211             return;
212         }
213         permissionSectionTitleView.setVisibility(View.VISIBLE);
214         statusSectionTitleView.setVisibility(View.VISIBLE);
215         LinearLayout usageLayout = rootView.findViewById(R.id.permission_usage);
216         Collections.sort(
217                 mPermGroupUsages,
218                 (pguA, pguB) ->
219                         getAppLabel(pguA).toString().compareTo(getAppLabel(pguB).toString()));
220 
221         for (PermissionGroupUsage usage : mPermGroupUsages) {
222             View cardView = View.inflate(mContext, R.layout.indicator_card, usageLayout);
223             cardView.setId(View.generateViewId());
224             ConstraintLayout parentIndicatorLayout = cardView.findViewById(R.id.indicator_layout);
225             parentIndicatorLayout.setId(View.generateViewId());
226             ImageView expandView = parentIndicatorLayout.findViewById(R.id.expand_view);
227 
228             // Update UI for the parent indicator card
229             updateIndicatorParentUi(
230                     parentIndicatorLayout,
231                     usage.getPermissionGroupName(),
232                     generateUsageLabel(usage),
233                     usage.isActive());
234 
235             // If sensor usage is due to an active phone call, don't allow any actions
236             if (usage.isPhoneCall()) {
237                 expandView.setVisibility(View.GONE);
238                 continue;
239             }
240 
241             ConstraintLayout expandedLayout = cardView.findViewById(R.id.expanded_layout);
242             expandedLayout.setId(View.generateViewId());
243 
244             // Handle redraw on orientation changes if permission has been revoked
245             if (mViewModel.getRevokedUsages().contains(usage)) {
246                 disableIndicatorCardUi(parentIndicatorLayout, expandView);
247                 continue;
248             }
249 
250             setIndicatorExpansionBehavior(parentIndicatorLayout, expandedLayout, expandView);
251             ViewCompat.replaceAccessibilityAction(
252                     parentIndicatorLayout,
253                     AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
254                     mContext.getString(R.string.safety_center_qs_expand_action),
255                     null);
256 
257             // Configure the indicator action buttons
258             configureIndicatorActionButtons(
259                     usage, parentIndicatorLayout, expandedLayout, expandView);
260         }
261     }
262 
configureIndicatorActionButtons( PermissionGroupUsage usage, ConstraintLayout parentIndicatorLayout, ConstraintLayout expandedLayout, ImageView expandView)263     private void configureIndicatorActionButtons(
264             PermissionGroupUsage usage,
265             ConstraintLayout parentIndicatorLayout,
266             ConstraintLayout expandedLayout,
267             ImageView expandView) {
268         configurePrimaryActionButton(usage, parentIndicatorLayout, expandedLayout, expandView);
269         configureSeeUsageButton(usage, expandedLayout);
270     }
271 
configurePrimaryActionButton( PermissionGroupUsage usage, ConstraintLayout parentIndicatorLayout, ConstraintLayout expandedLayout, ImageView expandView)272     private void configurePrimaryActionButton(
273             PermissionGroupUsage usage,
274             ConstraintLayout parentIndicatorLayout,
275             ConstraintLayout expandedLayout,
276             ImageView expandView) {
277         boolean shouldAllowRevoke = mViewModel.shouldAllowRevoke(usage);
278         Intent manageServiceIntent = null;
279 
280         if (isSubAttributionUsage(usage.getAttributionLabel())) {
281             manageServiceIntent = mViewModel.getStartViewPermissionUsageIntent(mContext, usage);
282         }
283 
284         int primaryActionButtonLabel =
285                 getPrimaryActionButtonLabel(
286                         manageServiceIntent != null,
287                         shouldAllowRevoke,
288                         usage.getPermissionGroupName());
289         MaterialButton primaryActionButton = expandedLayout.findViewById(R.id.primary_button);
290         primaryActionButton.setText(primaryActionButtonLabel);
291         primaryActionButton.setStrokeColorResource(
292                 Utils.getColorResId(mContext, android.R.attr.colorAccent));
293 
294         if (shouldAllowRevoke && manageServiceIntent == null) {
295             primaryActionButton.setOnClickListener(
296                     l -> {
297                         parentIndicatorLayout.callOnClick();
298                         disableIndicatorCardUi(parentIndicatorLayout, expandView);
299                         revokePermission(usage);
300                         mSafetyCenterViewModel
301                                 .getInteractionLogger()
302                                 .recordForSensor(
303                                         Action.SENSOR_PERMISSION_REVOKE_CLICKED,
304                                         Sensor.fromPermissionGroupUsage(usage));
305                     });
306         } else {
307             setPrimaryActionClickListener(primaryActionButton, usage, manageServiceIntent);
308         }
309     }
310 
configureSeeUsageButton( PermissionGroupUsage usage, ConstraintLayout expandedLayout)311     private void configureSeeUsageButton(
312             PermissionGroupUsage usage, ConstraintLayout expandedLayout) {
313         MaterialButton seeUsageButton = expandedLayout.findViewById(R.id.secondary_button);
314         seeUsageButton.setText(getSeeUsageText(usage.getPermissionGroupName()));
315 
316         seeUsageButton.setStrokeColorResource(
317                 Utils.getColorResId(mContext, android.R.attr.colorAccent));
318         seeUsageButton.setOnClickListener(
319                 l -> {
320                     mViewModel.navigateToSeeUsage(this, usage.getPermissionGroupName());
321                     mSafetyCenterViewModel
322                             .getInteractionLogger()
323                             .recordForSensor(
324                                     Action.SENSOR_PERMISSION_SEE_USAGES_CLICKED,
325                                     Sensor.fromPermissionGroupUsage(usage));
326                 });
327     }
328 
setPrimaryActionClickListener( Button primaryActionButton, PermissionGroupUsage usage, Intent manageServiceIntent)329     private void setPrimaryActionClickListener(
330             Button primaryActionButton, PermissionGroupUsage usage, Intent manageServiceIntent) {
331         if (manageServiceIntent != null) {
332             primaryActionButton.setOnClickListener(
333                     l -> {
334                         mViewModel.navigateToManageService(this, manageServiceIntent);
335                         mSafetyCenterViewModel
336                                 .getInteractionLogger()
337                                 .recordForSensor(
338                                         // Unfortunate name, but this is used for all primary
339                                         // CTAs on the permission usage cards.
340                                         Action.SENSOR_PERMISSION_REVOKE_CLICKED,
341                                         Sensor.fromPermissionGroupUsage(usage));
342                     });
343         } else {
344             primaryActionButton.setOnClickListener(
345                     l -> {
346                         mViewModel.navigateToManageAppPermissions(this, usage);
347                         mSafetyCenterViewModel
348                                 .getInteractionLogger()
349                                 .recordForSensor(
350                                         Action.SENSOR_PERMISSION_REVOKE_CLICKED,
351                                         Sensor.fromPermissionGroupUsage(usage));
352                     });
353         }
354     }
355 
getPrimaryActionButtonLabel( boolean canHandleIntent, boolean shouldAllowRevoke, String permissionGroupName)356     private int getPrimaryActionButtonLabel(
357             boolean canHandleIntent, boolean shouldAllowRevoke, String permissionGroupName) {
358         if (canHandleIntent) {
359             return R.string.manage_service_qs;
360         }
361         if (!shouldAllowRevoke) {
362             return R.string.manage_permissions_qs;
363         }
364         return getRemovePermissionText(permissionGroupName);
365     }
366 
isSubAttributionUsage(@ullable CharSequence attributionLabel)367     private boolean isSubAttributionUsage(@Nullable CharSequence attributionLabel) {
368         if (attributionLabel == null || attributionLabel.length() == 0) {
369             return false;
370         }
371         return true;
372     }
373 
revokePermission(PermissionGroupUsage usage)374     private void revokePermission(PermissionGroupUsage usage) {
375         mViewModel.revokePermission(usage);
376     }
377 
disableIndicatorCardUi( ConstraintLayout parentIndicatorLayout, ImageView expandView)378     private void disableIndicatorCardUi(
379             ConstraintLayout parentIndicatorLayout, ImageView expandView) {
380         // Disable the parent indicator and the expand view
381         parentIndicatorLayout.setEnabled(false);
382         expandView.setEnabled(false);
383         expandView.setVisibility(View.GONE);
384 
385         // Construct new icon for revoked permission
386         ImageView iconView = parentIndicatorLayout.findViewById(R.id.indicator_icon);
387         Drawable background = mContext.getDrawable(R.drawable.indicator_background_circle).mutate();
388         background.setTint(mContext.getColor(R.color.sc_surface_variant_dark));
389         Drawable icon = mContext.getDrawable(R.drawable.ic_check);
390         Utils.applyTint(mContext, icon, android.R.attr.textColorPrimary);
391         int bgSize = (int) getResources().getDimension(R.dimen.ongoing_appops_dialog_circle_size);
392         int iconSize = (int) getResources().getDimension(R.dimen.ongoing_appops_dialog_icon_size);
393         iconView.setImageDrawable(constructIcon(icon, background, bgSize, iconSize));
394 
395         // Set label to show on permission revoke
396         TextView labelView = parentIndicatorLayout.findViewById(R.id.indicator_label);
397         labelView.setText(R.string.permissions_removed_qs);
398         labelView.setContentDescription(mContext.getString(R.string.permissions_removed_qs));
399     }
400 
setIndicatorExpansionBehavior( ConstraintLayout parentIndicatorLayout, ConstraintLayout expandedLayout, ImageView expandView)401     private void setIndicatorExpansionBehavior(
402             ConstraintLayout parentIndicatorLayout,
403             ConstraintLayout expandedLayout,
404             ImageView expandView) {
405         View rootView = getView();
406         if (rootView == null) {
407             return;
408         }
409         parentIndicatorLayout.setOnClickListener(
410                 createExpansionListener(expandedLayout, expandView, rootView));
411     }
412 
createExpansionListener( ConstraintLayout expandedLayout, ImageView expandView, View rootView)413     private View.OnClickListener createExpansionListener(
414             ConstraintLayout expandedLayout, ImageView expandView, View rootView) {
415         AutoTransition transition = new AutoTransition();
416         // Get the entire fragment as a viewgroup in order to animate it nicely in case of
417         // expand/collapse
418         ViewGroup indicatorCardViewGroup = (ViewGroup) rootView;
419         return v -> {
420             if (expandedLayout.getVisibility() == View.VISIBLE) {
421                 // Enable -> Press -> Hide the expanded card for a continuous ripple effect
422                 expandedLayout.setEnabled(true);
423                 pressButton(expandedLayout);
424                 expandedLayout.setVisibility(View.GONE);
425                 TransitionManager.beginDelayedTransition(indicatorCardViewGroup, transition);
426                 expandView.setImageDrawable(
427                         mContext.getDrawable(R.drawable.ic_safety_group_expand));
428                 ViewCompat.replaceAccessibilityAction(
429                         v,
430                         AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
431                         mContext.getString(R.string.safety_center_qs_expand_action),
432                         null);
433             } else {
434                 // Show -> Press -> Disable the expanded card for a continuous ripple effect
435                 expandedLayout.setVisibility(View.VISIBLE);
436                 pressButton(expandedLayout);
437                 expandedLayout.setEnabled(false);
438                 TransitionManager.beginDelayedTransition(indicatorCardViewGroup, transition);
439                 expandView.setImageDrawable(
440                         mContext.getDrawable(R.drawable.ic_safety_group_collapse));
441                 ViewCompat.replaceAccessibilityAction(
442                         v,
443                         AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
444                         mContext.getString(R.string.safety_center_qs_collapse_action),
445                         null);
446             }
447         };
448     }
449 
450     /**
451      * To get the expanded card to ripple at the same time as the parent card we must simulate a
452      * user press on the expanded card
453      */
454     private void pressButton(View buttonToBePressed) {
455         buttonToBePressed.setPressed(true);
456         buttonToBePressed.setPressed(false);
457         buttonToBePressed.performClick();
458     }
459 
460     private String generateUsageLabel(PermissionGroupUsage usage) {
461         if (usage.isPhoneCall() && usage.isActive()) {
462             return mContext.getString(R.string.active_call_usage_qs);
463         } else if (usage.isPhoneCall()) {
464             return mContext.getString(R.string.recent_call_usage_qs);
465         }
466         return generateAttributionUsageLabel(usage);
467     }
468 
469     private String generateAttributionUsageLabel(PermissionGroupUsage usage) {
470         CharSequence appLabel = getAppLabel(usage);
471 
472         final int usageResId =
473                 usage.isActive() ? R.string.active_app_usage_qs : R.string.recent_app_usage_qs;
474         final int singleUsageResId =
475                 usage.isActive() ? R.string.active_app_usage_1_qs : R.string.recent_app_usage_1_qs;
476         final int doubleUsageResId =
477                 usage.isActive() ? R.string.active_app_usage_2_qs : R.string.recent_app_usage_2_qs;
478 
479         CharSequence attributionLabel = usage.getAttributionLabel();
480         CharSequence proxyLabel = usage.getProxyLabel();
481 
482         if (attributionLabel == null && proxyLabel == null) {
483             return mContext.getString(usageResId, appLabel);
484         } else if (attributionLabel != null && proxyLabel != null) {
485             return mContext.getString(doubleUsageResId, appLabel, attributionLabel, proxyLabel);
486         } else {
487             return mContext.getString(
488                     singleUsageResId,
489                     appLabel,
490                     attributionLabel == null ? proxyLabel : attributionLabel);
491         }
492     }
493 
494     private CharSequence getAppLabel(PermissionGroupUsage usage) {
495         return KotlinUtils.INSTANCE.getPackageLabel(
496                 getActivity().getApplication(),
497                 usage.getPackageName(),
498                 UserHandle.getUserHandleForUid(usage.getUid()));
499     }
500 
501     private void updateIndicatorParentUi(
502             ConstraintLayout indicatorParentLayout,
503             String permGroupName,
504             String usageText,
505             boolean isActiveUsage) {
506         CharSequence permGroupLabel = getPermGroupLabel(permGroupName);
507         ImageView iconView = indicatorParentLayout.findViewById(R.id.indicator_icon);
508 
509         Drawable background = mContext.getDrawable(R.drawable.indicator_background_circle);
510         int indicatorColor =
511                 Utils.getColorResId(
512                         mContext,
513                         isActiveUsage
514                                 ? android.R.attr.textColorPrimaryInverse
515                                 : android.R.attr.textColorPrimary);
516         Drawable indicatorIcon =
517                 KotlinUtils.INSTANCE.getPermGroupIcon(
518                         mContext, permGroupName, mContext.getColor(indicatorColor));
519         if (isActiveUsage) {
520             Utils.applyTint(mContext, background, android.R.attr.colorAccent);
521         } else {
522             background.setTint(mContext.getColor(R.color.sc_surface_variant_dark));
523         }
524         int bgSize = (int) getResources().getDimension(R.dimen.ongoing_appops_dialog_circle_size);
525         int iconSize = (int) getResources().getDimension(R.dimen.ongoing_appops_dialog_icon_size);
526         iconView.setImageDrawable(constructIcon(indicatorIcon, background, bgSize, iconSize));
527         iconView.setContentDescription(permGroupLabel);
528 
529         TextView titleText = indicatorParentLayout.findViewById(R.id.indicator_title);
530         titleText.setText(permGroupLabel);
531         titleText.setTextColor(
532                 mContext.getColor(Utils.getColorResId(mContext, android.R.attr.textColorPrimary)));
533         titleText.setContentDescription(permGroupLabel);
534 
535         TextView labelText = indicatorParentLayout.findViewById(R.id.indicator_label);
536         labelText.setText(usageText);
537         labelText.setContentDescription(usageText);
538 
539         ImageView expandView = indicatorParentLayout.findViewById(R.id.expand_view);
540         expandView.setImageDrawable(mContext.getDrawable(R.drawable.ic_safety_group_expand));
541     }
542 
543     private Drawable constructIcon(Drawable icon, Drawable background, int bgSize, int iconSize) {
544         LayerDrawable layered = new LayerDrawable(new Drawable[] {background, icon});
545         final int bgLayerIndex = 0;
546         final int iconLayerIndex = 1;
547         layered.setLayerSize(bgLayerIndex, bgSize, bgSize);
548         layered.setLayerSize(iconLayerIndex, iconSize, iconSize);
549         layered.setLayerGravity(iconLayerIndex, Gravity.CENTER);
550         return layered;
551     }
552 
553     private void setSensorToggleState(@Nullable Map<String, SensorState> sensorStates) {
554         if (!mAreSensorTogglesReady) {
555             mAreSensorTogglesReady = true;
556             maybeEnableView(getView());
557             setupSensorToggles(sensorStates, getView());
558         }
559         updateSensorToggleState(sensorStates, getView());
560     }
561 
562     private void setupSensorToggles(
563             @Nullable Map<String, SensorState> sensorStates, @Nullable View rootView) {
564         if (rootView == null) {
565             return;
566         }
567 
568         if (sensorStates == null) {
569             sensorStates = new ArrayMap<>();
570         }
571 
572         LinearLayout toggleContainer = rootView.findViewById(R.id.toggle_container);
573 
574         LinearLayout row = addRow(toggleContainer);
575 
576         for (String groupName : TOGGLE_BUTTONS) {
577             boolean sensorVisible =
578                     !sensorStates.containsKey(groupName)
579                             || sensorStates.get(groupName).getVisible();
580             if (!sensorVisible) {
581                 continue;
582             }
583 
584             addToggle(groupName, row);
585 
586             if (row.getChildCount() >= MAX_TOGGLES_PER_ROW) {
587                 row = addRow(toggleContainer);
588             }
589         }
590         addSettingsToggle(row);
591     }
592 
593     private LinearLayout addRow(ViewGroup parent) {
594         LinearLayout row =
595                 new LinearLayout(parent.getContext(), null, 0, R.style.SafetyCenterQsToggleRow);
596         parent.addView(row);
597         return row;
598     }
599 
600     private View addToggle(String tag, ViewGroup parent) {
601         View toggle =
602                 getLayoutInflater().inflate(R.layout.safety_center_toggle_button, parent, false);
603         toggle.setTag(tag);
604         parent.addView(toggle);
605         return toggle;
606     }
607 
608     private View addSettingsToggle(ViewGroup parent) {
609         View securitySettings = addToggle(SETTINGS_TOGGLE_TAG, parent);
610         securitySettings.setOnClickListener(
611                 (v) ->
612                         mSafetyCenterViewModel.navigateToSafetyCenter(
613                                 mContext, NavigationSource.QUICK_SETTINGS_TILE));
614         TextView securitySettingsText = securitySettings.findViewById(R.id.toggle_sensor_name);
615         securitySettingsText.setText(R.string.settings);
616         securitySettingsText.setSelected(true);
617         securitySettings.findViewById(R.id.toggle_sensor_status).setVisibility(View.GONE);
618         ImageView securitySettingsIcon = securitySettings.findViewById(R.id.toggle_sensor_icon);
619         securitySettingsIcon.setImageDrawable(
620                 Utils.applyTint(
621                         mContext,
622                         mContext.getDrawable(R.drawable.ic_safety_center_shield),
623                         android.R.attr.textColorPrimaryInverse));
624         securitySettings.findViewById(R.id.arrow_icon).setVisibility(View.VISIBLE);
625         ((ImageView) securitySettings.findViewById(R.id.arrow_icon))
626                 .setImageDrawable(
627                         Utils.applyTint(
628                                 mContext,
629                                 mContext.getDrawable(R.drawable.ic_chevron_right),
630                                 android.R.attr.textColorSecondaryInverse));
631         ViewCompat.replaceAccessibilityAction(
632                 securitySettings,
633                 AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
634                 mContext.getString(R.string.safety_center_qs_open_action),
635                 null);
636         return securitySettings;
637     }
638 
639     private void updateSensorToggleState(
640             @Nullable Map<String, SensorState> sensorStates, @Nullable View rootView) {
641         if (rootView == null) {
642             return;
643         }
644 
645         if (sensorStates == null) {
646             sensorStates = new ArrayMap<>();
647         }
648 
649         for (String groupName : TOGGLE_BUTTONS) {
650             View toggle = rootView.findViewWithTag(groupName);
651             if (toggle == null) {
652                 continue;
653             }
654             EnforcedAdmin admin =
655                     sensorStates.containsKey(groupName)
656                             ? sensorStates.get(groupName).getAdmin()
657                             : null;
658             boolean sensorBlockedByAdmin = admin != null;
659 
660             if (sensorBlockedByAdmin) {
661                 toggle.setOnClickListener(
662                         (v) ->
663                                 startActivity(
664                                         RestrictedLockUtils.getShowAdminSupportDetailsIntent(
665                                                 mContext, admin)));
666             } else {
667                 toggle.setOnClickListener(
668                         (v) -> {
669                             mViewModel.toggleSensor(groupName);
670                             mSafetyCenterViewModel
671                                     .getInteractionLogger()
672                                     .recordForSensor(
673                                             Action.PRIVACY_CONTROL_TOGGLE_CLICKED,
674                                             Sensor.fromPermissionGroupName(groupName));
675                         });
676             }
677 
678             TextView groupLabel = toggle.findViewById(R.id.toggle_sensor_name);
679             groupLabel.setText(getPermGroupLabel(groupName));
680             // Set the text as selected to get marquee to work
681             groupLabel.setSelected(true);
682             TextView blockedStatus = toggle.findViewById(R.id.toggle_sensor_status);
683             // Set the text as selected to get marquee to work
684             blockedStatus.setSelected(true);
685             ImageView iconView = toggle.findViewById(R.id.toggle_sensor_icon);
686             boolean sensorEnabled =
687                     !sensorStates.containsKey(groupName)
688                             || sensorStates.get(groupName).getEnabled();
689 
690             Drawable icon;
691             boolean useEnabledBackground = sensorEnabled && !sensorBlockedByAdmin;
692             int colorPrimary = getTextColor(true, useEnabledBackground, sensorBlockedByAdmin);
693             int colorSecondary = getTextColor(false, useEnabledBackground, sensorBlockedByAdmin);
694             if (useEnabledBackground) {
695                 toggle.setBackgroundResource(R.drawable.safety_center_sensor_toggle_enabled);
696             } else {
697                 toggle.setBackgroundResource(R.drawable.safety_center_sensor_toggle_disabled);
698             }
699             if (sensorEnabled) {
700                 icon = KotlinUtils.INSTANCE.getPermGroupIcon(mContext, groupName, colorPrimary);
701             } else {
702                 icon = mContext.getDrawable(getBlockedIconResId(groupName));
703                 icon.setTint(colorPrimary);
704             }
705             blockedStatus.setText(getSensorStatusTextResId(groupName, sensorEnabled));
706             blockedStatus.setTextColor(colorSecondary);
707             groupLabel.setTextColor(colorPrimary);
708             iconView.setImageDrawable(icon);
709 
710             int contentDescriptionResId = R.string.safety_center_qs_privacy_control;
711             toggle.setContentDescription(
712                     mContext.getString(
713                             contentDescriptionResId,
714                             groupLabel.getText(),
715                             blockedStatus.getText()));
716             ViewCompat.replaceAccessibilityAction(
717                     toggle,
718                     AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
719                     mContext.getString(R.string.safety_center_qs_toggle_action),
720                     null);
721         }
722     }
723 
724     @ColorInt
725     private int getTextColor(boolean primary, boolean inverse, boolean useLowerOpacity) {
726         int primaryAttribute =
727                 inverse ? android.R.attr.textColorPrimaryInverse : android.R.attr.textColorPrimary;
728         int secondaryAttribute =
729                 inverse
730                         ? android.R.attr.textColorSecondaryInverse
731                         : android.R.attr.textColorSecondary;
732         int attribute = primary ? primaryAttribute : secondaryAttribute;
733         TypedValue value = new TypedValue();
734         mContext.getTheme().resolveAttribute(attribute, value, true);
735         int colorRes = value.resourceId != 0 ? value.resourceId : value.data;
736         int color = mContext.getColor(colorRes);
737         if (useLowerOpacity) {
738             color = colorWithAdjustedAlpha(color, 0.5f);
739         }
740         return color;
741     }
742 
743     @ColorInt
744     private int colorWithAdjustedAlpha(@ColorInt int color, float factor) {
745         return Color.argb(
746                 Math.round(Color.alpha(color) * factor),
747                 Color.red(color),
748                 Color.green(color),
749                 Color.blue(color));
750     }
751 
752     private CharSequence getPermGroupLabel(String permissionGroup) {
753         switch (permissionGroup) {
754             case MICROPHONE:
755                 return mContext.getString(R.string.microphone_toggle_label_qs);
756             case CAMERA:
757                 return mContext.getString(R.string.camera_toggle_label_qs);
758         }
759         return KotlinUtils.INSTANCE.getPermGroupLabel(mContext, permissionGroup);
760     }
761 
762     private static int getRemovePermissionText(String permissionGroup) {
763         return CAMERA.equals(permissionGroup)
764                 ? R.string.remove_camera_qs
765                 : R.string.remove_microphone_qs;
766     }
767 
768     private static int getSeeUsageText(String permissionGroup) {
769         return CAMERA.equals(permissionGroup)
770                 ? R.string.camera_usage_qs
771                 : R.string.microphone_usage_qs;
772     }
773 
774     private static int getBlockedIconResId(String permissionGroup) {
775         switch (permissionGroup) {
776             case MICROPHONE:
777                 return R.drawable.ic_mic_blocked;
778             case CAMERA:
779                 return R.drawable.ic_camera_blocked;
780             case LOCATION:
781                 return R.drawable.ic_location_blocked;
782         }
783         return -1;
784     }
785 
786     private static int getSensorStatusTextResId(String permissionGroup, boolean enabled) {
787         switch (permissionGroup) {
788             case LOCATION:
789                 return enabled ? R.string.on : R.string.off;
790         }
791         return enabled ? R.string.available : R.string.blocked;
792     }
793 }
794