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