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