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