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