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