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