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