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