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.permission.ui.v34; 18 19 import static android.Manifest.permission_group.LOCATION; 20 import static android.content.Intent.EXTRA_PERMISSION_GROUP_NAME; 21 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 22 23 import static androidx.core.util.Preconditions.checkStringNotEmpty; 24 25 import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_ACCOUNT_MANAGEMENT; 26 import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_ADVERTISING; 27 import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_ANALYTICS; 28 import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_APP_FUNCTIONALITY; 29 import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_DEVELOPER_COMMUNICATIONS; 30 import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_FRAUD_PREVENTION_SECURITY; 31 import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_PERSONALIZATION; 32 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__CANCEL; 33 import static com.android.permissioncontroller.permission.ui.model.v34.PermissionRationaleViewModel.APP_PERMISSION_REQUEST_CODE; 34 import static com.android.permissioncontroller.permission.ui.v34.PermissionRationaleViewHandler.Result.CANCELLED; 35 36 import android.content.Intent; 37 import android.content.res.Resources; 38 import android.icu.lang.UCharacter; 39 import android.os.Build; 40 import android.os.Bundle; 41 import android.text.Annotation; 42 import android.text.Spannable; 43 import android.text.SpannableStringBuilder; 44 import android.text.TextUtils; 45 import android.text.style.BulletSpan; 46 import android.text.style.ClickableSpan; 47 import android.util.Log; 48 import android.util.Pair; 49 import android.view.MotionEvent; 50 import android.view.View; 51 import android.view.Window; 52 import android.view.WindowManager; 53 import android.view.inputmethod.InputMethodManager; 54 55 import androidx.annotation.NonNull; 56 import androidx.annotation.RequiresApi; 57 import androidx.annotation.StringRes; 58 import androidx.core.util.Preconditions; 59 60 import com.android.permission.safetylabel.DataPurposeConstants.Purpose; 61 import com.android.permissioncontroller.Constants; 62 import com.android.permissioncontroller.DeviceUtils; 63 import com.android.permissioncontroller.R; 64 import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity; 65 import com.android.permissioncontroller.permission.ui.SettingsActivity; 66 import com.android.permissioncontroller.permission.ui.handheld.v34.PermissionRationaleViewHandlerImpl; 67 import com.android.permissioncontroller.permission.ui.model.v34.PermissionRationaleViewModel; 68 import com.android.permissioncontroller.permission.ui.model.v34.PermissionRationaleViewModel.ActivityResultCallback; 69 import com.android.permissioncontroller.permission.ui.model.v34.PermissionRationaleViewModel.PermissionRationaleInfo; 70 import com.android.permissioncontroller.permission.ui.model.v34.PermissionRationaleViewModelFactory; 71 import com.android.permissioncontroller.permission.utils.KotlinUtils; 72 import com.android.permissioncontroller.permission.utils.Utils; 73 74 import org.jetbrains.annotations.Nullable; 75 76 import java.util.ArrayList; 77 import java.util.Arrays; 78 import java.util.Collections; 79 import java.util.Comparator; 80 import java.util.List; 81 import java.util.Random; 82 import java.util.stream.Collectors; 83 84 /** 85 * An activity which displays runtime permission rationale on behalf of an app. This activity is 86 * based on GrantPermissionActivity to keep view behavior and theming consistent. 87 */ 88 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 89 public class PermissionRationaleActivity extends SettingsActivity implements 90 PermissionRationaleViewHandler.ResultListener { 91 92 private static final String LOG_TAG = PermissionRationaleActivity.class.getSimpleName(); 93 94 private static final String KEY_SESSION_ID = PermissionRationaleActivity.class.getName() 95 + "_SESSION_ID"; 96 97 /** 98 * [Annotation] key for span annotations replacement within the permission rationale purposes 99 * string resource 100 */ 101 public static final String ANNOTATION_ID_KEY = "id"; 102 /** 103 * [Annotation] id value for span annotations replacement of link annotations within the 104 * permission rationale purposes string resource 105 */ 106 public static final String LINK_ANNOTATION_ID = "link"; 107 /** 108 * [Annotation] id value for span annotations replacement of install source annotations within 109 * the permission rationale purposes string resource 110 */ 111 public static final String INSTALL_SOURCE_ANNOTATION_ID = "install_source"; 112 /** 113 * [Annotation] id value for span annotations replacement of purpose list annotations within 114 * the permission rationale purposes string resource 115 */ 116 public static final String PURPOSE_LIST_ANNOTATION_ID = "purpose_list"; 117 /** 118 * [Annotation] id value for span annotations replacement of permission name annotations within 119 * the permission rationale purposes string resource 120 */ 121 public static final String PERMISSION_NAME_ANNOTATION_ID = "permission_name"; 122 123 /** 124 * key to the boolean if to show settings_section on the permission rationale dialog provide via 125 * intent extra 126 */ 127 public static final String EXTRA_SHOULD_SHOW_SETTINGS_SECTION = 128 "com.android.permissioncontroller.extra.SHOULD_SHOW_SETTINGS_SECTION"; 129 130 // Data class defines these values in a different natural order. Swap advertising and fraud 131 // prevention order for display in permission rationale dialog 132 private static final List<Integer> ORDERED_PURPOSES = Arrays.asList( 133 PURPOSE_APP_FUNCTIONALITY, 134 PURPOSE_ANALYTICS, 135 PURPOSE_DEVELOPER_COMMUNICATIONS, 136 PURPOSE_ADVERTISING, 137 PURPOSE_FRAUD_PREVENTION_SECURITY, 138 PURPOSE_PERSONALIZATION, 139 PURPOSE_ACCOUNT_MANAGEMENT 140 ); 141 142 /** Comparator used to update purpose order to expected display order */ 143 private static final Comparator<Integer> ORDERED_PURPOSE_COMPARATOR = 144 Comparator.comparingInt(purposeInt -> ORDERED_PURPOSES.indexOf(purposeInt)); 145 146 /** Unique Id of a request. Inherited from GrantPermissionDialog if provide via intent extra */ 147 private long mSessionId; 148 /** Package that shall have permissions granted */ 149 private String mTargetPackage; 150 /** The permission group that initiated the permission rationale details activity */ 151 private String mPermissionGroupName; 152 /** The permission rationale info resulting from the specified permission and group */ 153 private PermissionRationaleInfo mPermissionRationaleInfo; 154 155 private PermissionRationaleViewHandler mViewHandler; 156 private PermissionRationaleViewModel mViewModel; 157 158 private float mOriginalDimAmount; 159 private View mRootView; 160 161 162 @Override onCreate(Bundle icicle)163 protected void onCreate(Bundle icicle) { 164 super.onCreate(icicle); 165 166 if (!KotlinUtils.INSTANCE.isPermissionRationaleEnabled()) { 167 Log.e( 168 LOG_TAG, 169 "Permission rationale feature disabled"); 170 finishAfterTransition(); 171 return; 172 } 173 174 if (icicle == null) { 175 mSessionId = 176 getIntent().getLongExtra(Constants.EXTRA_SESSION_ID, new Random().nextLong()); 177 } else { 178 mSessionId = icicle.getLong(KEY_SESSION_ID); 179 } 180 181 getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 182 183 mPermissionGroupName = getIntent().getStringExtra(EXTRA_PERMISSION_GROUP_NAME); 184 if (mPermissionGroupName == null) { 185 Log.e( 186 LOG_TAG, 187 "null EXTRA_PERMISSION_GROUP_NAME. Must be set for permission rationale"); 188 finishAfterTransition(); 189 return; 190 } 191 192 mTargetPackage = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME); 193 if (mTargetPackage == null) { 194 Log.e(LOG_TAG, "null EXTRA_PACKAGE_NAME. Must be set for permission rationale"); 195 finishAfterTransition(); 196 return; 197 } 198 199 setFinishOnTouchOutside(false); 200 201 setTitle(getTitleResIdForPermissionGroup(mPermissionGroupName)); 202 203 if (DeviceUtils.isTelevision(this) 204 || DeviceUtils.isWear(this) 205 || DeviceUtils.isAuto(this)) { 206 finishAfterTransition(); 207 } else { 208 boolean shouldShowSettingsSection = 209 getIntent().getBooleanExtra(EXTRA_SHOULD_SHOW_SETTINGS_SECTION, true); 210 mViewHandler = new PermissionRationaleViewHandlerImpl(this, this, 211 shouldShowSettingsSection); 212 } 213 214 PermissionRationaleViewModelFactory factory = new PermissionRationaleViewModelFactory( 215 getApplication(), mTargetPackage, mPermissionGroupName, mSessionId, icicle); 216 mViewModel = factory.create(PermissionRationaleViewModel.class); 217 mViewModel.getPermissionRationaleInfoLiveData() 218 .observe(this, this::onPermissionRationaleInfoLoad); 219 220 mRootView = mViewHandler.createView(); 221 mRootView.setVisibility(View.GONE); 222 setContentView(mRootView); 223 Window window = getWindow(); 224 WindowManager.LayoutParams layoutParams = window.getAttributes(); 225 mOriginalDimAmount = layoutParams.dimAmount; 226 window.setAttributes(layoutParams); 227 228 if (getResources().getBoolean(R.bool.config_useWindowBlur)) { 229 java.util.function.Consumer<Boolean> blurEnabledListener = enabled -> { 230 mViewHandler.onBlurEnabledChanged(window, enabled); 231 }; 232 mRootView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 233 @Override 234 public void onViewAttachedToWindow(View v) { 235 window.getWindowManager().addCrossWindowBlurEnabledListener( 236 blurEnabledListener); 237 } 238 239 @Override 240 public void onViewDetachedFromWindow(View v) { 241 window.getWindowManager().removeCrossWindowBlurEnabledListener( 242 blurEnabledListener); 243 } 244 }); 245 } 246 // Restore UI state after lifecycle events. This has to be before we show the first request, 247 // as the UI behaves differently for updates and initial creations. 248 if (icicle != null) { 249 mViewHandler.loadInstanceState(icicle); 250 } 251 } 252 253 @Override onSaveInstanceState(@onNull Bundle outState)254 protected void onSaveInstanceState(@NonNull Bundle outState) { 255 super.onSaveInstanceState(outState); 256 257 if (mViewHandler == null) { 258 return; 259 } 260 261 mViewHandler.saveInstanceState(outState); 262 263 outState.putLong(KEY_SESSION_ID, mSessionId); 264 } 265 266 @Override onActivityResult(int requestCode, int resultCode, Intent data)267 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 268 super.onActivityResult(requestCode, resultCode, data); 269 ActivityResultCallback callback = mViewModel.getActivityResultCallback(); 270 if (callback == null || (requestCode != APP_PERMISSION_REQUEST_CODE)) { 271 return; 272 } 273 boolean shouldFinishActivity = callback.shouldFinishActivityForResult(data); 274 mViewModel.setActivityResultCallback(null); 275 276 if (shouldFinishActivity) { 277 setResultAndFinish(data); 278 } 279 } 280 setResultAndFinish(Intent result)281 private void setResultAndFinish(Intent result) { 282 setResult(RESULT_OK, result); 283 finishAfterTransition(); 284 } 285 286 @Override onBackPressed()287 public void onBackPressed() { 288 if (mViewHandler == null) { 289 return; 290 } 291 mViewHandler.onBackPressed(); 292 } 293 294 // LINT.IfChange(dispatchTouchEvent) 295 /** 296 * Used to dismiss dialog when tapping outside of dialog bounds 297 * Follows the same logic as GrantPermissionActivity 298 */ 299 @Override dispatchTouchEvent(MotionEvent ev)300 public boolean dispatchTouchEvent(MotionEvent ev) { 301 View rootView = getWindow().getDecorView(); 302 if (rootView.getTop() != 0) { 303 // We are animating the top view, need to compensate for that in motion events. 304 ev.setLocation(ev.getX(), ev.getY() - rootView.getTop()); 305 } 306 final int x = (int) ev.getX(); 307 final int y = (int) ev.getY(); 308 if ((x < 0) || (y < 0) || (x > (rootView.getWidth())) || (y > (rootView.getHeight()))) { 309 //TODO b/278783474: We should make this activity a fragment of the base GrantPermissions 310 // activity 311 GrantPermissionsActivity grantActivity = null; 312 synchronized (GrantPermissionsActivity.sCurrentGrantRequests) { 313 grantActivity = GrantPermissionsActivity.sCurrentGrantRequests 314 .get(new Pair<>(mTargetPackage, getTaskId())); 315 } 316 if (grantActivity != null 317 && getIntent().getBooleanExtra(EXTRA_SHOULD_SHOW_SETTINGS_SECTION, false)) { 318 grantActivity.finishAfterTransition(); 319 } 320 if (MotionEvent.ACTION_DOWN == ev.getAction()) { 321 mViewHandler.onCancelled(); 322 } 323 finishAfterTransition(); 324 } 325 return super.dispatchTouchEvent(ev); 326 } 327 // LINT.ThenChange(GrantPermissionsActivity.java:dispatchTouchEvent) 328 329 @Override onPermissionRationaleResult(@ullable String groupName, int result)330 public void onPermissionRationaleResult(@Nullable String groupName, int result) { 331 if (result == CANCELLED) { 332 mViewModel.logPermissionRationaleDialogActionReported( 333 PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__CANCEL); 334 finishAfterTransition(); 335 } 336 } 337 onPermissionRationaleInfoLoad(PermissionRationaleInfo permissionRationaleInfo)338 private void onPermissionRationaleInfoLoad(PermissionRationaleInfo permissionRationaleInfo) { 339 if (!mViewModel.getPermissionRationaleInfoLiveData().isInitialized()) { 340 return; 341 } 342 343 if (permissionRationaleInfo == null) { 344 finishAfterTransition(); 345 return; 346 } 347 348 mPermissionRationaleInfo = permissionRationaleInfo; 349 showPermissionRationale(); 350 } 351 showPermissionRationale()352 private void showPermissionRationale() { 353 @StringRes int titleResId = getTitleResIdForPermissionGroup(mPermissionGroupName); 354 setTitle(titleResId); 355 CharSequence title = getString(titleResId); 356 CharSequence dataSharingSourceMessage = getDataSharingSourceMessage(); 357 358 CharSequence purposeTitle = 359 getString(getPurposeTitleResIdForPermissionGroup(mPermissionGroupName)); 360 361 List<Integer> purposeList = new ArrayList<>(mPermissionRationaleInfo.getPurposeSet()); 362 Collections.sort(purposeList, ORDERED_PURPOSE_COMPARATOR); 363 List<String> purposeStringList = purposeList.stream() 364 .map(this::getStringForPurpose) 365 .collect(Collectors.toList()); 366 367 CharSequence purposeMessage = 368 createPurposeMessageWithBulletSpan( 369 getText(R.string.permission_rationale_purpose_message), 370 purposeStringList); 371 372 CharSequence learnMoreMessage; 373 if (mViewModel.canLinkToHelpCenter(this)) { 374 learnMoreMessage = setLink( 375 getText(R.string.permission_rationale_data_sharing_varies_message), 376 getLearnMoreLink() 377 ); 378 } else { 379 learnMoreMessage = 380 getText(R.string.permission_rationale_data_sharing_varies_message_without_link); 381 } 382 383 String groupName = mPermissionRationaleInfo.getGroupName(); 384 String permissionGroupLabel = 385 KotlinUtils.INSTANCE.getPermGroupLabel(this, groupName).toString(); 386 CharSequence settingsMessage = 387 createSettingsMessageWithSpans( 388 getText(getSettingsMessageResIdForPermissionGroup(groupName)), 389 UCharacter.toLowerCase(permissionGroupLabel), 390 getLinkToSettings() 391 ); 392 393 mViewHandler.updateUi( 394 groupName, 395 title, 396 dataSharingSourceMessage, 397 purposeTitle, 398 purposeMessage, 399 learnMoreMessage, 400 settingsMessage 401 ); 402 403 getWindow().setDimAmount(mOriginalDimAmount); 404 if (mRootView.getVisibility() == View.GONE) { 405 InputMethodManager manager = getSystemService(InputMethodManager.class); 406 manager.hideSoftInputFromWindow(mRootView.getWindowToken(), 0); 407 mRootView.setVisibility(View.VISIBLE); 408 } 409 } 410 getDataSharingSourceMessage()411 private CharSequence getDataSharingSourceMessage() { 412 if (mPermissionRationaleInfo.isPreloadedApp()) { 413 return getText(R.string.permission_rationale_data_sharing_device_manufacturer_message); 414 } 415 416 String installSourcePackageName = mPermissionRationaleInfo.getInstallSourcePackageName(); 417 CharSequence installSourceLabel = mPermissionRationaleInfo.getInstallSourceLabel(); 418 checkStringNotEmpty(installSourcePackageName, 419 "installSourcePackageName cannot be null or empty"); 420 checkStringNotEmpty(installSourceLabel, "installSourceLabel cannot be null or empty"); 421 return createDataSharingSourceMessageWithSpans( 422 getText(R.string.permission_rationale_data_sharing_source_message), 423 installSourceLabel, 424 getLinkToAppStore(installSourcePackageName)); 425 } 426 427 @StringRes getTitleResIdForPermissionGroup(String permissionGroupName)428 private int getTitleResIdForPermissionGroup(String permissionGroupName) { 429 if (LOCATION.equals(permissionGroupName)) { 430 return R.string.permission_rationale_location_title; 431 } 432 433 String exceptionString = 434 String.format("Permission Rationale does not support %s", permissionGroupName); 435 throw new IllegalArgumentException(exceptionString); 436 } 437 438 @StringRes getPurposeTitleResIdForPermissionGroup(String permissionGroupName)439 private int getPurposeTitleResIdForPermissionGroup(String permissionGroupName) { 440 if (LOCATION.equals(permissionGroupName)) { 441 return R.string.permission_rationale_location_purpose_title; 442 } 443 444 String exceptionString = 445 String.format("Permission Rationale does not support %s", permissionGroupName); 446 throw new IllegalArgumentException(exceptionString); 447 } 448 449 /** 450 * Returns permission settings message string resource id for the given permission group. 451 * 452 * <p> Supported permission groups: LOCATION 453 * 454 * @param permissionGroupName permission group for which to get a message string id 455 * @throws IllegalArgumentException if passing unsupported permission group 456 */ 457 @StringRes getSettingsMessageResIdForPermissionGroup(String permissionGroupName)458 private int getSettingsMessageResIdForPermissionGroup(String permissionGroupName) { 459 Preconditions.checkArgument(LOCATION.equals(permissionGroupName), 460 "Permission Rationale does not support %s", permissionGroupName); 461 462 return R.string.permission_rationale_permission_settings_message; 463 } 464 getStringForPurpose(@urpose int purpose)465 private String getStringForPurpose(@Purpose int purpose) { 466 switch (purpose) { 467 case PURPOSE_APP_FUNCTIONALITY: 468 return getString(R.string.permission_rationale_purpose_app_functionality); 469 case PURPOSE_ANALYTICS: 470 return getString(R.string.permission_rationale_purpose_analytics); 471 case PURPOSE_DEVELOPER_COMMUNICATIONS: 472 return getString(R.string.permission_rationale_purpose_developer_communications); 473 case PURPOSE_FRAUD_PREVENTION_SECURITY: 474 return getString(R.string.permission_rationale_purpose_fraud_prevention_security); 475 case PURPOSE_ADVERTISING: 476 return getString(R.string.permission_rationale_purpose_advertising); 477 case PURPOSE_PERSONALIZATION: 478 return getString(R.string.permission_rationale_purpose_personalization); 479 case PURPOSE_ACCOUNT_MANAGEMENT: 480 return getString(R.string.permission_rationale_purpose_account_management); 481 default: 482 throw new IllegalArgumentException("Invalid purpose: " + purpose); 483 } 484 } 485 createDataSharingSourceMessageWithSpans( CharSequence baseText, CharSequence installSourceLabel, ClickableSpan link)486 private CharSequence createDataSharingSourceMessageWithSpans( 487 CharSequence baseText, 488 CharSequence installSourceLabel, 489 ClickableSpan link) { 490 CharSequence updatedText = 491 replaceSpan(baseText, INSTALL_SOURCE_ANNOTATION_ID, installSourceLabel); 492 updatedText = setLink(updatedText, link); 493 return updatedText; 494 } 495 createPurposeMessageWithBulletSpan( CharSequence baseText, List<String> purposesList)496 private CharSequence createPurposeMessageWithBulletSpan( 497 CharSequence baseText, 498 List<String> purposesList) { 499 Resources res = getResources(); 500 final int bulletSize = 501 res.getDimensionPixelSize(R.dimen.permission_rationale_purpose_list_bullet_radius); 502 final int bulletIndent = 503 res.getDimensionPixelSize(R.dimen.permission_rationale_purpose_list_bullet_indent); 504 505 final int bulletColor = 506 getColor(Utils.getColorResId(this, android.R.attr.textColorSecondary)); 507 508 String purposesString = TextUtils.join("\n", purposesList); 509 SpannableStringBuilder purposesSpan = SpannableStringBuilder.valueOf(purposesString); 510 int spanStart = 0; 511 for (int i = 0; i < purposesList.size(); i++) { 512 final int length = purposesList.get(i).length(); 513 purposesSpan.setSpan(new BulletSpan(bulletIndent, bulletColor, bulletSize), 514 spanStart, spanStart + length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 515 spanStart += length + 1; 516 } 517 CharSequence updatedText = replaceSpan(baseText, PURPOSE_LIST_ANNOTATION_ID, purposesSpan); 518 return updatedText; 519 } 520 createSettingsMessageWithSpans( CharSequence baseText, CharSequence permissionName, ClickableSpan link)521 private CharSequence createSettingsMessageWithSpans( 522 CharSequence baseText, 523 CharSequence permissionName, 524 ClickableSpan link) { 525 CharSequence updatedText = 526 replaceSpan(baseText, PERMISSION_NAME_ANNOTATION_ID, permissionName); 527 updatedText = setLink(updatedText, link); 528 return updatedText; 529 } 530 replaceSpan( CharSequence baseText, String annotationId, CharSequence replacementText)531 private CharSequence replaceSpan( 532 CharSequence baseText, 533 String annotationId, 534 CharSequence replacementText) { 535 SpannableStringBuilder text = SpannableStringBuilder.valueOf(baseText); 536 Annotation[] annotations = text.getSpans(0, text.length(), Annotation.class); 537 538 for (android.text.Annotation annotation : annotations) { 539 if (!annotation.getKey().equals(ANNOTATION_ID_KEY) 540 || !annotation.getValue().equals(annotationId)) { 541 continue; 542 } 543 544 int spanStart = text.getSpanStart(annotation); 545 int spanEnd = text.getSpanEnd(annotation); 546 text.removeSpan(annotation); 547 text.replace(spanStart, spanEnd, replacementText); 548 break; 549 } 550 551 return text; 552 } 553 setLink(CharSequence baseText, ClickableSpan link)554 private CharSequence setLink(CharSequence baseText, ClickableSpan link) { 555 SpannableStringBuilder text = SpannableStringBuilder.valueOf(baseText); 556 Annotation[] annotations = text.getSpans(0, text.length(), Annotation.class); 557 558 for (android.text.Annotation annotation : annotations) { 559 if (!annotation.getKey().equals(ANNOTATION_ID_KEY) 560 || !annotation.getValue().equals(LINK_ANNOTATION_ID)) { 561 continue; 562 } 563 564 int spanStart = text.getSpanStart(annotation); 565 int spanEnd = text.getSpanEnd(annotation); 566 text.removeSpan(annotation); 567 text.setSpan(link, spanStart, spanEnd, 0); 568 break; 569 } 570 571 return text; 572 } 573 getLinkToAppStore(String installSourcePackageName)574 private ClickableSpan getLinkToAppStore(String installSourcePackageName) { 575 boolean canLinkToAppStore = mViewModel 576 .canLinkToAppStore(PermissionRationaleActivity.this, installSourcePackageName); 577 if (!canLinkToAppStore) { 578 return null; 579 } 580 return new ClickableSpan() { 581 @Override 582 public void onClick(@NonNull View widget) { 583 // TODO(b/259961958): metrics for click events 584 mViewModel.sendToAppStore(PermissionRationaleActivity.this, 585 installSourcePackageName); 586 } 587 }; 588 } 589 590 private ClickableSpan getLinkToSettings() { 591 return new ClickableSpan() { 592 @Override 593 public void onClick(@NonNull View widget) { 594 // TODO(b/259961958): metrics for click events 595 mViewModel.sendToSettingsForPermissionGroup(PermissionRationaleActivity.this, 596 mPermissionGroupName); 597 } 598 }; 599 } 600 601 private ClickableSpan getLearnMoreLink() { 602 return new ClickableSpan() { 603 @Override 604 public void onClick(@NonNull View widget) { 605 // TODO(b/259961958): metrics for click events 606 mViewModel.sendToLearnMore(PermissionRationaleActivity.this); 607 } 608 }; 609 } 610 } 611