1 /*
2  * Copyright (C) 2017 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.handheld;
18 
19 import static android.content.pm.PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED;
20 
21 import static com.android.permissioncontroller.PermissionControllerStatsLog.REVIEW_PERMISSIONS_FRAGMENT_RESULT_REPORTED;
22 
23 import android.app.Activity;
24 import android.content.Intent;
25 import android.content.IntentSender;
26 import android.content.pm.PackageInfo;
27 import android.content.pm.PackageManager;
28 import android.graphics.drawable.Drawable;
29 import android.os.Bundle;
30 import android.os.RemoteCallback;
31 import android.os.UserHandle;
32 import android.text.Html;
33 import android.text.Spanned;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.view.View;
37 import android.widget.Button;
38 import android.widget.ImageView;
39 import android.widget.TextView;
40 
41 import androidx.annotation.NonNull;
42 import androidx.preference.Preference;
43 import androidx.preference.PreferenceCategory;
44 import androidx.preference.PreferenceFragmentCompat;
45 import androidx.preference.PreferenceGroup;
46 import androidx.preference.PreferenceScreen;
47 
48 import com.android.permissioncontroller.PermissionControllerStatsLog;
49 import com.android.permissioncontroller.R;
50 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
51 import com.android.permissioncontroller.permission.model.AppPermissions;
52 import com.android.permissioncontroller.permission.model.Permission;
53 import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity;
54 import com.android.permissioncontroller.permission.utils.ArrayUtils;
55 import com.android.permissioncontroller.permission.utils.Utils;
56 
57 import java.util.ArrayList;
58 import java.util.List;
59 import java.util.Random;
60 
61 /**
62  * If an app does not support runtime permissions the user is prompted via this fragment to select
63  * which permissions to grant to the app before first use and if an update changed the permissions.
64  */
65 public final class ReviewPermissionsFragment extends PreferenceFragmentCompat
66         implements View.OnClickListener, PermissionPreference.PermissionPreferenceChangeListener,
67         PermissionPreference.PermissionPreferenceOwnerFragment {
68 
69     private static final String EXTRA_PACKAGE_INFO =
70             "com.android.permissioncontroller.permission.ui.extra.PACKAGE_INFO";
71     private static final String LOG_TAG = ReviewPermissionsFragment.class.getSimpleName();
72 
73     private AppPermissions mAppPermissions;
74 
75     private Button mContinueButton;
76     private Button mCancelButton;
77     private Button mMoreInfoButton;
78 
79     private PreferenceCategory mNewPermissionsCategory;
80     private PreferenceCategory mCurrentPermissionsCategory;
81 
82     private boolean mHasConfirmedRevoke;
83 
84     /**
85      * @return a new fragment
86      */
newInstance(PackageInfo packageInfo)87     public static ReviewPermissionsFragment newInstance(PackageInfo packageInfo) {
88         Bundle arguments = new Bundle();
89         arguments.putParcelable(EXTRA_PACKAGE_INFO, packageInfo);
90         ReviewPermissionsFragment instance = new ReviewPermissionsFragment();
91         instance.setArguments(arguments);
92         instance.setRetainInstance(true);
93         return instance;
94     }
95 
96     @Override
onCreate(Bundle savedInstanceState)97     public void onCreate(Bundle savedInstanceState) {
98         super.onCreate(savedInstanceState);
99 
100         Activity activity = getActivity();
101         if (activity == null) {
102             return;
103         }
104 
105         PackageInfo packageInfo = getArguments().getParcelable(EXTRA_PACKAGE_INFO);
106         if (packageInfo == null) {
107             activity.finishAfterTransition();
108             return;
109         }
110 
111         mAppPermissions = new AppPermissions(activity, packageInfo, false, true,
112                 () -> getActivity().finishAfterTransition());
113 
114         boolean reviewRequired = false;
115         for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) {
116             if (group.isReviewRequired() || (group.getBackgroundPermissions() != null
117                     && group.getBackgroundPermissions().isReviewRequired())) {
118                 reviewRequired = true;
119                 break;
120             }
121         }
122 
123         if (!reviewRequired) {
124             // If the system called for a review but no groups are found, this means that all groups
125             // are restricted. Hence there is nothing to review and instantly continue.
126             confirmPermissionsReview();
127             executeCallback(true);
128             activity.finishAfterTransition();
129         }
130     }
131 
132     @Override
onCreatePreferences(Bundle bundle, String s)133     public void onCreatePreferences(Bundle bundle, String s) {
134         // empty
135     }
136 
137     @Override
onViewCreated(@onNull View view, Bundle savedInstanceState)138     public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
139         super.onViewCreated(view, savedInstanceState);
140         bindUi();
141     }
142 
143     @Override
onResume()144     public void onResume() {
145         super.onResume();
146         mAppPermissions.refresh();
147         loadPreferences();
148     }
149 
150     @Override
onClick(View view)151     public void onClick(View view) {
152         Activity activity = getActivity();
153         if (activity == null) {
154             return;
155         }
156         if (view == mContinueButton) {
157             confirmPermissionsReview();
158             executeCallback(true);
159         } else if (view == mCancelButton) {
160             executeCallback(false);
161             activity.setResult(Activity.RESULT_CANCELED);
162         } else if (view == mMoreInfoButton) {
163             Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
164             intent.putExtra(Intent.EXTRA_PACKAGE_NAME,
165                     mAppPermissions.getPackageInfo().packageName);
166             intent.putExtra(Intent.EXTRA_USER, UserHandle.getUserHandleForUid(
167                     mAppPermissions.getPackageInfo().applicationInfo.uid));
168             intent.putExtra(ManagePermissionsActivity.EXTRA_ALL_PERMISSIONS, true);
169             getActivity().startActivity(intent);
170         }
171         activity.finishAfterTransition();
172     }
173 
grantReviewedPermission(AppPermissionGroup group)174     private void grantReviewedPermission(AppPermissionGroup group) {
175         String[] permissionsToGrant = null;
176         final int permissionCount = group.getPermissions().size();
177         for (int j = 0; j < permissionCount; j++) {
178             final Permission permission = group.getPermissions().get(j);
179             if (permission.isReviewRequired()) {
180                 permissionsToGrant = ArrayUtils.appendString(
181                         permissionsToGrant, permission.getName());
182             }
183         }
184         if (permissionsToGrant != null) {
185             group.grantRuntimePermissions(true, false, permissionsToGrant);
186         }
187     }
188 
confirmPermissionsReview()189     private void confirmPermissionsReview() {
190         final List<PreferenceGroup> preferenceGroups = new ArrayList<>();
191         if (mNewPermissionsCategory != null) {
192             preferenceGroups.add(mNewPermissionsCategory);
193             preferenceGroups.add(mCurrentPermissionsCategory);
194         } else {
195             PreferenceScreen preferenceScreen = getPreferenceScreen();
196             if (preferenceScreen != null) {
197                 preferenceGroups.add(preferenceScreen);
198             }
199         }
200 
201         final int preferenceGroupCount = preferenceGroups.size();
202         long changeIdForLogging = new Random().nextLong();
203 
204         for (int groupNum = 0; groupNum < preferenceGroupCount; groupNum++) {
205             final PreferenceGroup preferenceGroup = preferenceGroups.get(groupNum);
206 
207             final int preferenceCount = preferenceGroup.getPreferenceCount();
208             for (int prefNum = 0; prefNum < preferenceCount; prefNum++) {
209                 Preference preference = preferenceGroup.getPreference(prefNum);
210                 if (preference instanceof PermissionReviewPreference) {
211                     PermissionReviewPreference permPreference =
212                             (PermissionReviewPreference) preference;
213                     AppPermissionGroup group = permPreference.getGroup();
214 
215                     // If the preference wasn't toggled we show it as "granted"
216                     if (group.isReviewRequired() && !permPreference.wasChanged()) {
217                         grantReviewedPermission(group);
218                     }
219                     logReviewPermissionsFragmentResult(changeIdForLogging, group);
220 
221                     AppPermissionGroup backgroundGroup = group.getBackgroundPermissions();
222                     if (backgroundGroup != null) {
223                         // If the preference wasn't toggled we show it as "fully granted"
224                         if (backgroundGroup.isReviewRequired() && !permPreference.wasChanged()) {
225                             grantReviewedPermission(backgroundGroup);
226                         }
227                         logReviewPermissionsFragmentResult(changeIdForLogging, backgroundGroup);
228                     }
229                 }
230             }
231         }
232         mAppPermissions.persistChanges(true);
233 
234         // Some permission might be restricted and hence there is no AppPermissionGroup for it.
235         // Manually unset all review-required flags, regardless of restriction.
236         PackageManager pm = getContext().getPackageManager();
237         PackageInfo pkg = mAppPermissions.getPackageInfo();
238         UserHandle user = UserHandle.getUserHandleForUid(pkg.applicationInfo.uid);
239 
240         for (String perm : pkg.requestedPermissions) {
241             try {
242                 pm.updatePermissionFlags(perm, pkg.packageName, FLAG_PERMISSION_REVIEW_REQUIRED,
243                         0, user);
244             } catch (IllegalArgumentException e) {
245                 Log.e(LOG_TAG, "Cannot unmark " + perm + " requested by " + pkg.packageName
246                         + " as review required", e);
247             }
248         }
249     }
250 
logReviewPermissionsFragmentResult(long changeId, AppPermissionGroup group)251     private void logReviewPermissionsFragmentResult(long changeId, AppPermissionGroup group) {
252         ArrayList<Permission> permissions = group.getPermissions();
253 
254         int numPermissions = permissions.size();
255         for (int i = 0; i < numPermissions; i++) {
256             Permission permission = permissions.get(i);
257 
258             PermissionControllerStatsLog.write(REVIEW_PERMISSIONS_FRAGMENT_RESULT_REPORTED,
259                     changeId, group.getApp().applicationInfo.uid, group.getApp().packageName,
260                     permission.getName(), permission.isGrantedIncludingAppOp());
261             Log.v(LOG_TAG, "Permission grant via permission review changeId=" + changeId + " uid="
262                     + group.getApp().applicationInfo.uid + " packageName="
263                     + group.getApp().packageName + " permission="
264                     + permission.getName() + " granted=" + permission.isGrantedIncludingAppOp());
265         }
266     }
267 
bindUi()268     private void bindUi() {
269         Activity activity = getActivity();
270         if (activity == null) {
271             return;
272         }
273 
274         // Set icon
275         Drawable icon = mAppPermissions.getPackageInfo().applicationInfo.loadIcon(
276                 activity.getPackageManager());
277         ImageView iconView = activity.requireViewById(R.id.app_icon);
278         iconView.setImageDrawable(icon);
279 
280         // Set message
281         final int labelTemplateResId = isPackageUpdated()
282                 ? R.string.permission_review_title_template_update
283                 : R.string.permission_review_title_template_install;
284         Spanned message = Html.fromHtml(getString(labelTemplateResId,
285                 mAppPermissions.getAppLabel()), 0);
286 
287         // Set the permission message as the title so it can be announced.
288         activity.setTitle(message.toString());
289 
290         // Color the app name.
291         TextView permissionsMessageView = activity.requireViewById(
292                 R.id.permissions_message);
293         permissionsMessageView.setText(message);
294 
295         mContinueButton = getActivity().requireViewById(R.id.continue_button);
296         mContinueButton.setOnClickListener(this);
297 
298         mCancelButton = getActivity().requireViewById(R.id.cancel_button);
299         mCancelButton.setOnClickListener(this);
300 
301         if (activity.getPackageManager().arePermissionsIndividuallyControlled()) {
302             mMoreInfoButton = getActivity().requireViewById(
303                     R.id.permission_more_info_button);
304             mMoreInfoButton.setOnClickListener(this);
305             mMoreInfoButton.setVisibility(View.VISIBLE);
306         }
307     }
308 
getPreference(String key)309     private PermissionReviewPreference getPreference(String key) {
310         if (mNewPermissionsCategory != null) {
311             PermissionReviewPreference pref =
312                     (PermissionReviewPreference) mNewPermissionsCategory.findPreference(key);
313 
314             if (pref == null && mCurrentPermissionsCategory != null) {
315                 return (PermissionReviewPreference) mCurrentPermissionsCategory.findPreference(key);
316             } else {
317                 return pref;
318             }
319         } else {
320             return (PermissionReviewPreference) getPreferenceScreen().findPreference(key);
321         }
322     }
323 
loadPreferences()324     private void loadPreferences() {
325         Activity activity = getActivity();
326         if (activity == null) {
327             return;
328         }
329 
330         PreferenceScreen screen = getPreferenceScreen();
331         if (screen == null) {
332             screen = getPreferenceManager().createPreferenceScreen(getContext());
333             setPreferenceScreen(screen);
334         } else {
335             screen.removeAll();
336         }
337 
338         mCurrentPermissionsCategory = null;
339         mNewPermissionsCategory = null;
340 
341         final boolean isPackageUpdated = isPackageUpdated();
342 
343         for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) {
344             if (!Utils.shouldShowPermission(getContext(), group)
345                     || !Utils.OS_PKG.equals(group.getDeclaringPackage())) {
346                 continue;
347             }
348 
349             PermissionReviewPreference preference = getPreference(group.getName());
350             if (preference == null) {
351                 preference = new PermissionReviewPreference(this, group, this);
352 
353                 preference.setKey(group.getName());
354                 Drawable icon = Utils.loadDrawable(activity.getPackageManager(),
355                         group.getIconPkg(), group.getIconResId());
356                 preference.setIcon(Utils.applyTint(getContext(), icon,
357                         android.R.attr.colorControlNormal));
358                 preference.setTitle(group.getLabel());
359             } else {
360                 preference.updateUi();
361             }
362 
363             if (group.isReviewRequired() || (group.getBackgroundPermissions() != null
364                     && group.getBackgroundPermissions().isReviewRequired())) {
365                 if (!isPackageUpdated) {
366                     screen.addPreference(preference);
367                 } else {
368                     if (mNewPermissionsCategory == null) {
369                         mNewPermissionsCategory = new PreferenceCategory(activity);
370                         mNewPermissionsCategory.setTitle(R.string.new_permissions_category);
371                         mNewPermissionsCategory.setOrder(1);
372                         screen.addPreference(mNewPermissionsCategory);
373                     }
374                     mNewPermissionsCategory.addPreference(preference);
375                 }
376             } else {
377                 if (mCurrentPermissionsCategory == null) {
378                     mCurrentPermissionsCategory = new PreferenceCategory(activity);
379                     mCurrentPermissionsCategory.setTitle(R.string.current_permissions_category);
380                     mCurrentPermissionsCategory.setOrder(2);
381                     screen.addPreference(mCurrentPermissionsCategory);
382                 }
383                 mCurrentPermissionsCategory.addPreference(preference);
384             }
385         }
386     }
387 
isPackageUpdated()388     private boolean isPackageUpdated() {
389         List<AppPermissionGroup> groups = mAppPermissions.getPermissionGroups();
390         final int groupCount = groups.size();
391         for (int i = 0; i < groupCount; i++) {
392             AppPermissionGroup group = groups.get(i);
393             if (!(group.isReviewRequired() || (group.getBackgroundPermissions() != null
394                     && group.getBackgroundPermissions().isReviewRequired()))) {
395                 return true;
396             }
397         }
398         return false;
399     }
400 
executeCallback(boolean success)401     private void executeCallback(boolean success) {
402         Activity activity = getActivity();
403         if (activity == null) {
404             return;
405         }
406         if (success) {
407             IntentSender intent = activity.getIntent().getParcelableExtra(Intent.EXTRA_INTENT);
408             if (intent != null) {
409                 try {
410                     int flagMask = 0;
411                     int flagValues = 0;
412                     if (activity.getIntent().getBooleanExtra(
413                             Intent.EXTRA_RESULT_NEEDED, false)) {
414                         flagMask = Intent.FLAG_ACTIVITY_FORWARD_RESULT;
415                         flagValues = Intent.FLAG_ACTIVITY_FORWARD_RESULT;
416                     }
417                     activity.startIntentSenderForResult(intent, -1, null,
418                             flagMask, flagValues, 0);
419                 } catch (IntentSender.SendIntentException e) {
420                         /* ignore */
421                 }
422                 return;
423             }
424         }
425         RemoteCallback callback = activity.getIntent().getParcelableExtra(
426                 Intent.EXTRA_REMOTE_CALLBACK);
427         if (callback != null) {
428             Bundle result = new Bundle();
429             result.putBoolean(Intent.EXTRA_RETURN_RESULT, success);
430             callback.sendResult(result);
431         }
432     }
433 
434     @Override
shouldConfirmDefaultPermissionRevoke()435     public boolean shouldConfirmDefaultPermissionRevoke() {
436         return !mHasConfirmedRevoke;
437     }
438 
439     @Override
hasConfirmDefaultPermissionRevoke()440     public void hasConfirmDefaultPermissionRevoke() {
441         mHasConfirmedRevoke = true;
442     }
443 
444     @Override
onPreferenceChanged(String key)445     public void onPreferenceChanged(String key) {
446         getPreference(key).setChanged();
447     }
448 
449     @Override
onDenyAnyWay(String key, int changeTarget)450     public void onDenyAnyWay(String key, int changeTarget) {
451         getPreference(key).onDenyAnyWay(changeTarget);
452     }
453 
454     @Override
onBackgroundAccessChosen(String key, int chosenItem)455     public void onBackgroundAccessChosen(String key, int chosenItem) {
456         getPreference(key).onBackgroundAccessChosen(chosenItem);
457     }
458 
459     /**
460      * Extend the {@link PermissionPreference}:
461      * <ul>
462      *     <li>Show the description of the permission group</li>
463      *     <li>Show the permission group as granted if the user has not toggled it yet. This means
464      *     that if the user does not touch the preference, we will later grant the permission
465      *     in {@link #confirmPermissionsReview()}.</li>
466      * </ul>
467      */
468     private static class PermissionReviewPreference extends PermissionPreference {
469         private final AppPermissionGroup mGroup;
470         private boolean mWasChanged;
471 
PermissionReviewPreference(PreferenceFragmentCompat fragment, AppPermissionGroup group, PermissionPreferenceChangeListener callbacks)472         PermissionReviewPreference(PreferenceFragmentCompat fragment, AppPermissionGroup group,
473                 PermissionPreferenceChangeListener callbacks) {
474             super(fragment, group, callbacks);
475 
476             mGroup = group;
477             updateUi();
478         }
479 
getGroup()480         AppPermissionGroup getGroup() {
481             return mGroup;
482         }
483 
484         /**
485          * Mark the permission as changed by the user
486          */
setChanged()487         void setChanged() {
488             mWasChanged = true;
489             updateUi();
490         }
491 
492         /**
493          * @return {@code true} iff the permission was changed by the user
494          */
wasChanged()495         boolean wasChanged() {
496             return mWasChanged;
497         }
498 
499         @Override
updateUi()500         void updateUi() {
501             // updateUi might be called in super-constructor before group is initialized
502             if (mGroup == null) {
503                 return;
504             }
505 
506             super.updateUi();
507 
508             if (isEnabled()) {
509                 if (mGroup.isReviewRequired() && !mWasChanged) {
510                     setSummary(mGroup.getDescription());
511                     setCheckedOverride(true);
512                 } else if (TextUtils.isEmpty(getSummary())) {
513                     // Sometimes the summary is already used, e.g. when this for a
514                     // foreground/background group. In this case show leave the original summary.
515                     setSummary(mGroup.getDescription());
516                 }
517             }
518         }
519     }
520 }
521