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