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