1 /* 2 * Copyright (C) 2015 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.permission.ui; 18 19 import static android.Manifest.permission.ACCESS_COARSE_LOCATION; 20 import static android.Manifest.permission.ACCESS_FINE_LOCATION; 21 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 22 23 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.CANCELED; 24 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED; 25 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED_DO_NOT_ASK_AGAIN; 26 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_ALWAYS; 27 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_FOREGROUND_ONLY; 28 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_ONE_TIME; 29 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.LINKED_TO_SETTINGS; 30 import static com.android.permissioncontroller.permission.utils.Utils.getRequestMessage; 31 32 import android.app.KeyguardManager; 33 import android.content.Intent; 34 import android.content.pm.PackageManager; 35 import android.content.res.Resources; 36 import android.graphics.drawable.Icon; 37 import android.os.Bundle; 38 import android.os.Process; 39 import android.text.Annotation; 40 import android.text.SpannableString; 41 import android.text.Spanned; 42 import android.text.style.ClickableSpan; 43 import android.util.Log; 44 import android.view.MotionEvent; 45 import android.view.View; 46 import android.view.View.OnAttachStateChangeListener; 47 import android.view.Window; 48 import android.view.WindowManager; 49 50 import androidx.annotation.NonNull; 51 import androidx.core.util.Consumer; 52 53 import com.android.modules.utils.build.SdkLevel; 54 import com.android.permissioncontroller.DeviceUtils; 55 import com.android.permissioncontroller.R; 56 import com.android.permissioncontroller.permission.ui.auto.GrantPermissionsAutoViewHandler; 57 import com.android.permissioncontroller.permission.ui.model.GrantPermissionsViewModel; 58 import com.android.permissioncontroller.permission.ui.model.GrantPermissionsViewModel.RequestInfo; 59 import com.android.permissioncontroller.permission.ui.model.GrantPermissionsViewModelFactory; 60 import com.android.permissioncontroller.permission.ui.wear.GrantPermissionsWearViewHandler; 61 import com.android.permissioncontroller.permission.utils.KotlinUtils; 62 import com.android.permissioncontroller.permission.utils.Utils; 63 64 import java.util.ArrayList; 65 import java.util.HashMap; 66 import java.util.List; 67 import java.util.Map; 68 import java.util.Random; 69 70 /** 71 * An activity which displays runtime permission prompts on behalf of an app. 72 */ 73 public class GrantPermissionsActivity extends SettingsActivity 74 implements GrantPermissionsViewHandler.ResultListener { 75 76 private static final String LOG_TAG = "GrantPermissionsActivit"; 77 78 private static final String KEY_SESSION_ID = GrantPermissionsActivity.class.getName() 79 + "_REQUEST_ID"; 80 public static final String ANNOTATION_ID = "link"; 81 82 public static final int NEXT_BUTTON = 11; 83 public static final int ALLOW_BUTTON = 0; 84 public static final int ALLOW_ALWAYS_BUTTON = 1; // Used in auto 85 public static final int ALLOW_FOREGROUND_BUTTON = 2; 86 public static final int DENY_BUTTON = 3; 87 public static final int DENY_AND_DONT_ASK_AGAIN_BUTTON = 4; 88 public static final int ALLOW_ONE_TIME_BUTTON = 5; 89 public static final int NO_UPGRADE_BUTTON = 6; 90 public static final int NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON = 7; 91 public static final int NO_UPGRADE_OT_BUTTON = 8; // one-time 92 public static final int NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON = 9; // one-time 93 public static final int LINK_TO_SETTINGS = 10; 94 95 public static final int NEXT_LOCATION_DIALOG = 6; 96 public static final int LOCATION_ACCURACY_LAYOUT = 0; 97 public static final int FINE_RADIO_BUTTON = 1; 98 public static final int COARSE_RADIO_BUTTON = 2; 99 public static final int DIALOG_WITH_BOTH_LOCATIONS = 3; 100 public static final int DIALOG_WITH_FINE_LOCATION_ONLY = 4; 101 public static final int DIALOG_WITH_COARSE_LOCATION_ONLY = 5; 102 103 public static final Map<String, Integer> PERMISSION_TO_BIT_SHIFT = 104 new HashMap<String, Integer>() {{ 105 put(ACCESS_COARSE_LOCATION, 0); 106 put(ACCESS_FINE_LOCATION, 1); 107 }}; 108 109 private static final int APP_PERMISSION_REQUEST_CODE = 1; 110 111 /** Unique Id of a request */ 112 private long mSessionId; 113 114 private String[] mRequestedPermissions; 115 private boolean[] mButtonVisibilities; 116 private boolean[] mLocationVisibilities; 117 private List<RequestInfo> mRequestInfos = new ArrayList<>(); 118 private GrantPermissionsViewHandler mViewHandler; 119 private GrantPermissionsViewModel mViewModel; 120 private boolean mResultSet; 121 /** Package that requested the permission grant */ 122 private String mCallingPackage; 123 private int mTotalRequests = 0; 124 private int mCurrentRequestIdx = 0; 125 private float mOriginalDimAmount; 126 private View mRootView; 127 128 @Override onCreate(Bundle icicle)129 public void onCreate(Bundle icicle) { 130 super.onCreate(icicle); 131 132 if (icicle == null) { 133 mSessionId = new Random().nextLong(); 134 } else { 135 mSessionId = icicle.getLong(KEY_SESSION_ID); 136 } 137 138 getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 139 140 mRequestedPermissions = getIntent().getStringArrayExtra( 141 PackageManager.EXTRA_REQUEST_PERMISSIONS_NAMES); 142 if (mRequestedPermissions == null || mRequestedPermissions.length == 0) { 143 setResultAndFinish(); 144 return; 145 } 146 147 // Cache this as this can only read on onCreate, not later. 148 mCallingPackage = getCallingPackage(); 149 if (mCallingPackage == null) { 150 Log.e(LOG_TAG, "null callingPackageName. Please use \"RequestPermission\" to " 151 + "request permissions"); 152 setResultAndFinish(); 153 return; 154 } 155 156 setFinishOnTouchOutside(false); 157 158 setTitle(R.string.permission_request_title); 159 160 if (DeviceUtils.isTelevision(this)) { 161 mViewHandler = new com.android.permissioncontroller.permission.ui.television 162 .GrantPermissionsViewHandlerImpl(this, 163 mCallingPackage).setResultListener(this); 164 } else if (DeviceUtils.isWear(this)) { 165 mViewHandler = new GrantPermissionsWearViewHandler(this).setResultListener(this); 166 } else if (DeviceUtils.isAuto(this)) { 167 mViewHandler = new GrantPermissionsAutoViewHandler(this, mCallingPackage) 168 .setResultListener(this); 169 } else { 170 mViewHandler = new com.android.permissioncontroller.permission.ui.handheld 171 .GrantPermissionsViewHandlerImpl(this, mCallingPackage, 172 Process.myUserHandle()).setResultListener(this); 173 } 174 175 GrantPermissionsViewModelFactory factory = new GrantPermissionsViewModelFactory( 176 getApplication(), mCallingPackage, mRequestedPermissions, mSessionId, icicle); 177 mViewModel = factory.create(GrantPermissionsViewModel.class); 178 mViewModel.getRequestInfosLiveData().observe(this, this::onRequestInfoLoad); 179 180 mRootView = mViewHandler.createView(); 181 mRootView.setVisibility(View.GONE); 182 setContentView(mRootView); 183 Window window = getWindow(); 184 WindowManager.LayoutParams layoutParams = window.getAttributes(); 185 mOriginalDimAmount = layoutParams.dimAmount; 186 mViewHandler.updateWindowAttributes(layoutParams); 187 window.setAttributes(layoutParams); 188 189 if (SdkLevel.isAtLeastS() && getResources().getBoolean(R.bool.config_useWindowBlur)) { 190 java.util.function.Consumer<Boolean> blurEnabledListener = enabled -> { 191 mViewHandler.onBlurEnabledChanged(window, enabled); 192 }; 193 mRootView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { 194 @Override 195 public void onViewAttachedToWindow(View v) { 196 window.getWindowManager().addCrossWindowBlurEnabledListener( 197 blurEnabledListener); 198 } 199 200 @Override 201 public void onViewDetachedFromWindow(View v) { 202 window.getWindowManager().removeCrossWindowBlurEnabledListener( 203 blurEnabledListener); 204 } 205 }); 206 } 207 // Restore UI state after lifecycle events. This has to be before we show the first request, 208 // as the UI behaves differently for updates and initial creations. 209 if (icicle != null) { 210 mViewHandler.loadInstanceState(icicle); 211 } else { 212 // Do not show screen dim until data is loaded 213 window.setDimAmount(0f); 214 } 215 } 216 onRequestInfoLoad(List<RequestInfo> requests)217 private void onRequestInfoLoad(List<RequestInfo> requests) { 218 if (!mViewModel.getRequestInfosLiveData().isInitialized() || mResultSet) { 219 return; 220 } else if (requests == null) { 221 finishAfterTransition(); 222 return; 223 } else if (requests.isEmpty()) { 224 setResultAndFinish(); 225 return; 226 } 227 228 if (mRequestInfos == null) { 229 mTotalRequests = requests.size(); 230 } 231 mRequestInfos = requests; 232 233 showNextRequest(); 234 } 235 showNextRequest()236 private void showNextRequest() { 237 if (mRequestInfos == null || mRequestInfos.isEmpty()) { 238 return; 239 } 240 241 RequestInfo info = mRequestInfos.get(0); 242 243 if (info.getSendToSettingsImmediately()) { 244 mViewModel.sendDirectlyToSettings(this, info.getGroupName()); 245 return; 246 } 247 248 CharSequence appLabel = KotlinUtils.INSTANCE.getPackageLabel(getApplication(), 249 mCallingPackage, Process.myUserHandle()); 250 251 int messageId = 0; 252 switch(info.getMessage()) { 253 case FG_MESSAGE: 254 messageId = Utils.getRequest(info.getGroupName()); 255 break; 256 case FG_FINE_LOCATION_MESSAGE: 257 messageId = R.string.permgrouprequest_fineupgrade; 258 break; 259 case FG_COARSE_LOCATION_MESSAGE: 260 messageId = R.string.permgrouprequest_coarselocation; 261 break; 262 case BG_MESSAGE: 263 messageId = Utils.getBackgroundRequest(info.getGroupName()); 264 break; 265 case UPGRADE_MESSAGE: 266 messageId = Utils.getUpgradeRequest(info.getGroupName()); 267 } 268 269 CharSequence message = getRequestMessage(appLabel, mCallingPackage, 270 info.getGroupName(), this, messageId); 271 272 int detailMessageId = 0; 273 switch(info.getDetailMessage()) { 274 case FG_MESSAGE: 275 detailMessageId = Utils.getRequestDetail(info.getGroupName()); 276 break; 277 case BG_MESSAGE: 278 detailMessageId = Utils.getBackgroundRequestDetail(info.getGroupName()); 279 break; 280 case UPGRADE_MESSAGE: 281 detailMessageId = Utils.getUpgradeRequestDetail(info.getGroupName()); 282 } 283 284 Spanned detailMessage = null; 285 if (detailMessageId != 0) { 286 detailMessage = 287 new SpannableString(getText(detailMessageId)); 288 Annotation[] annotations = detailMessage.getSpans( 289 0, detailMessage.length(), Annotation.class); 290 int numAnnotations = annotations.length; 291 for (int i = 0; i < numAnnotations; i++) { 292 Annotation annotation = annotations[i]; 293 if (annotation.getValue().equals(ANNOTATION_ID)) { 294 int start = detailMessage.getSpanStart(annotation); 295 int end = detailMessage.getSpanEnd(annotation); 296 ClickableSpan clickableSpan = getLinkToAppPermissions(info); 297 SpannableString spannableString = 298 new SpannableString(detailMessage); 299 spannableString.setSpan(clickableSpan, start, end, 0); 300 detailMessage = spannableString; 301 break; 302 } 303 } 304 } 305 306 Icon icon = null; 307 try { 308 icon = Icon.createWithResource(info.getGroupInfo().getPackageName(), 309 info.getGroupInfo().getIcon()); 310 } catch (Resources.NotFoundException e) { 311 Log.e(LOG_TAG, "Cannot load icon for group" + info.getGroupName(), e); 312 } 313 314 boolean showingNewGroup = message == null || !message.equals(getTitle()); 315 316 // Set the permission message as the title so it can be announced. Skip on Wear 317 // because the dialog title is already announced, as is the default selection which 318 // is a text view containing the title. 319 if (!DeviceUtils.isWear(this)) { 320 setTitle(message); 321 } 322 323 ArrayList<Integer> idxs = new ArrayList<>(); 324 mButtonVisibilities = new boolean[info.getButtonVisibilities().size()]; 325 for (int i = 0; i < info.getButtonVisibilities().size(); i++) { 326 mButtonVisibilities[i] = info.getButtonVisibilities().get(i); 327 if (mButtonVisibilities[i]) { 328 idxs.add(i); 329 } 330 } 331 332 mLocationVisibilities = new boolean[info.getLocationVisibilities().size()]; 333 for (int i = 0; i < info.getLocationVisibilities().size(); i++) { 334 mLocationVisibilities[i] = info.getLocationVisibilities().get(i); 335 } 336 337 mViewHandler.updateUi(info.getGroupName(), mTotalRequests, mCurrentRequestIdx, icon, 338 message, detailMessage, mButtonVisibilities, mLocationVisibilities); 339 if (showingNewGroup) { 340 mCurrentRequestIdx++; 341 } 342 343 getWindow().setDimAmount(mOriginalDimAmount); 344 mRootView.setVisibility(View.VISIBLE); 345 } 346 347 @Override dispatchTouchEvent(MotionEvent ev)348 public boolean dispatchTouchEvent(MotionEvent ev) { 349 View rootView = getWindow().getDecorView(); 350 if (rootView.getTop() != 0) { 351 // We are animating the top view, need to compensate for that in motion events. 352 ev.setLocation(ev.getX(), ev.getY() - rootView.getTop()); 353 } 354 return super.dispatchTouchEvent(ev); 355 } 356 357 @Override onSaveInstanceState(@onNull Bundle outState)358 protected void onSaveInstanceState(@NonNull Bundle outState) { 359 super.onSaveInstanceState(outState); 360 361 mViewHandler.saveInstanceState(outState); 362 mViewModel.saveInstanceState(outState); 363 364 outState.putLong(KEY_SESSION_ID, mSessionId); 365 } 366 getLinkToAppPermissions(RequestInfo info)367 private ClickableSpan getLinkToAppPermissions(RequestInfo info) { 368 return new ClickableSpan() { 369 @Override 370 public void onClick(View widget) { 371 logGrantPermissionActivityButtons(info.getGroupName(), null, LINKED_TO_SETTINGS); 372 mViewModel.sendToSettingsFromLink(GrantPermissionsActivity.this, 373 info.getGroupName()); 374 } 375 }; 376 } 377 378 379 @Override 380 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 381 super.onActivityResult(requestCode, resultCode, data); 382 Consumer<Intent> callback = mViewModel.getActivityResultCallback(); 383 384 if (requestCode == APP_PERMISSION_REQUEST_CODE && callback != null) { 385 callback.accept(data); 386 mViewModel.setActivityResultCallback(null); 387 } 388 } 389 390 @Override 391 public void onPermissionGrantResult(String name, 392 @GrantPermissionsViewHandler.Result int result) { 393 if (checkKgm(name, null, result)) { 394 return; 395 } 396 397 logGrantPermissionActivityButtons(name, null, result); 398 mViewModel.onPermissionGrantResult(name, null, result); 399 showNextRequest(); 400 if (result == CANCELED) { 401 setResultAndFinish(); 402 } 403 } 404 405 @Override 406 public void onPermissionGrantResult(String name, List<String> affectedForegroundPermissions, 407 @GrantPermissionsViewHandler.Result int result) { 408 if (checkKgm(name, affectedForegroundPermissions, result)) { 409 return; 410 } 411 412 logGrantPermissionActivityButtons(name, affectedForegroundPermissions, result); 413 mViewModel.onPermissionGrantResult(name, affectedForegroundPermissions, result); 414 showNextRequest(); 415 if (result == CANCELED) { 416 setResultAndFinish(); 417 } 418 } 419 420 @Override 421 public void onBackPressed() { 422 mViewHandler.onBackPressed(); 423 } 424 425 @Override 426 public void finishAfterTransition() { 427 setResultIfNeeded(RESULT_CANCELED); 428 if (mViewModel != null) { 429 mViewModel.autoGrantNotify(); 430 } 431 super.finishAfterTransition(); 432 } 433 434 private boolean checkKgm(String name, List<String> affectedForegroundPermissions, 435 @GrantPermissionsViewHandler.Result int result) { 436 if (result == GRANTED_ALWAYS || result == GRANTED_FOREGROUND_ONLY 437 || result == DENIED_DO_NOT_ASK_AGAIN) { 438 KeyguardManager kgm = getSystemService(KeyguardManager.class); 439 440 if (kgm != null && kgm.isDeviceLocked()) { 441 kgm.requestDismissKeyguard(this, new KeyguardManager.KeyguardDismissCallback() { 442 @Override 443 public void onDismissError() { 444 Log.e(LOG_TAG, "Cannot dismiss keyguard perm=" + name 445 + " result=" + result); 446 } 447 448 @Override 449 public void onDismissCancelled() { 450 // do nothing (i.e. stay at the current permission group) 451 } 452 453 @Override 454 public void onDismissSucceeded() { 455 // Now the keyguard is dismissed, hence the device is not locked 456 // anymore 457 onPermissionGrantResult(name, affectedForegroundPermissions, result); 458 } 459 }); 460 return true; 461 } 462 } 463 return false; 464 } 465 466 private void setResultIfNeeded(int resultCode) { 467 if (!mResultSet) { 468 mResultSet = true; 469 if (mViewModel != null) { 470 mViewModel.logRequestedPermissionGroups(); 471 } 472 Intent result = new Intent(PackageManager.ACTION_REQUEST_PERMISSIONS); 473 String[] resultPermissions = mRequestedPermissions != null 474 ? mRequestedPermissions : new String[0]; 475 int[] grantResults = new int[resultPermissions.length]; 476 477 if (mViewModel != null && mViewModel.shouldReturnPermissionState() 478 && mCallingPackage != null) { 479 PackageManager pm = getPackageManager(); 480 for (int i = 0; i < resultPermissions.length; i++) { 481 grantResults[i] = pm.checkPermission(resultPermissions[i], mCallingPackage); 482 } 483 } else { 484 grantResults = new int[0]; 485 resultPermissions = new String[0]; 486 } 487 result.putExtra(PackageManager.EXTRA_REQUEST_PERMISSIONS_NAMES, resultPermissions); 488 result.putExtra(PackageManager.EXTRA_REQUEST_PERMISSIONS_RESULTS, grantResults); 489 setResult(resultCode, result); 490 } 491 } 492 493 private void setResultAndFinish() { 494 setResultIfNeeded(RESULT_OK); 495 finishAfterTransition(); 496 } 497 498 private void logGrantPermissionActivityButtons(String permissionGroupName, 499 List<String> affectedForegroundPermissions, int grantResult) { 500 int clickedButton = 0; 501 int presentedButtons = getButtonState(); 502 switch (grantResult) { 503 case GRANTED_ALWAYS: 504 clickedButton = 1 << ALLOW_BUTTON; 505 break; 506 case GRANTED_FOREGROUND_ONLY: 507 clickedButton = 1 << ALLOW_FOREGROUND_BUTTON; 508 break; 509 case DENIED: 510 if (mButtonVisibilities != null) { 511 if (mButtonVisibilities[NO_UPGRADE_BUTTON]) { 512 clickedButton = 1 << NO_UPGRADE_BUTTON; 513 } else if (mButtonVisibilities[NO_UPGRADE_OT_BUTTON]) { 514 clickedButton = 1 << NO_UPGRADE_OT_BUTTON; 515 } else if (mButtonVisibilities[DENY_BUTTON]) { 516 clickedButton = 1 << DENY_BUTTON; 517 } 518 } 519 break; 520 case DENIED_DO_NOT_ASK_AGAIN: 521 if (mButtonVisibilities != null) { 522 if (mButtonVisibilities[NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON]) { 523 clickedButton = 1 << NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON; 524 } else if (mButtonVisibilities[NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON]) { 525 clickedButton = 1 << NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON; 526 } else if (mButtonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON]) { 527 clickedButton = 1 << DENY_AND_DONT_ASK_AGAIN_BUTTON; 528 } 529 } 530 break; 531 case GRANTED_ONE_TIME: 532 clickedButton = 1 << ALLOW_ONE_TIME_BUTTON; 533 break; 534 case LINKED_TO_SETTINGS: 535 clickedButton = 1 << LINK_TO_SETTINGS; 536 case CANCELED: 537 // fall through 538 default: 539 break; 540 } 541 542 int selectedPrecision = 0; 543 if (affectedForegroundPermissions != null) { 544 for (Map.Entry<String, Integer> entry : PERMISSION_TO_BIT_SHIFT.entrySet()) { 545 if (affectedForegroundPermissions.contains(entry.getKey())) { 546 selectedPrecision |= 1 << entry.getValue(); 547 } 548 } 549 } 550 551 mViewModel.logClickedButtons(permissionGroupName, selectedPrecision, clickedButton, 552 presentedButtons); 553 } 554 555 private int getButtonState() { 556 if (mButtonVisibilities == null) { 557 return 0; 558 } 559 int buttonState = 0; 560 for (int i = NEXT_BUTTON - 1; i >= 0; i--) { 561 buttonState *= 2; 562 if (mButtonVisibilities[i]) { 563 buttonState++; 564 } 565 } 566 return buttonState; 567 } 568 } 569