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