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.wear;
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 android.app.Activity;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentSender;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager;
29 import android.graphics.drawable.Drawable;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.os.RemoteCallback;
33 import android.os.UserHandle;
34 import android.text.Html;
35 import android.text.SpannableString;
36 import android.text.style.ForegroundColorSpan;
37 import android.util.Log;
38 import android.util.TypedValue;
39 
40 import androidx.preference.Preference;
41 import androidx.preference.PreferenceCategory;
42 import androidx.preference.PreferenceFragmentCompat;
43 import androidx.preference.PreferenceGroup;
44 import androidx.preference.PreferenceScreen;
45 import androidx.preference.SwitchPreference;
46 import androidx.preference.TwoStatePreference;
47 import androidx.wear.ble.view.WearableDialogHelper;
48 
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.utils.Utils;
53 
54 import java.util.ArrayList;
55 import java.util.List;
56 
57 public class ReviewPermissionsWearFragment extends PreferenceFragmentCompat
58         implements Preference.OnPreferenceChangeListener {
59     private static final String TAG = "ReviewPermWear";
60 
61     private static final int ORDER_NEW_PERMS = 1;
62     private static final int ORDER_CURRENT_PERMS = 2;
63     // Category for showing actions should be displayed last.
64     private static final int ORDER_ACTION = 100000;
65     private static final int ORDER_PERM_OFFSET_START = 100;
66 
67     private static final String EXTRA_PACKAGE_INFO =
68         "com.android.permissioncontroller.permission.ui.extra.PACKAGE_INFO";
69 
newInstance(PackageInfo packageInfo)70     public static ReviewPermissionsWearFragment newInstance(PackageInfo packageInfo) {
71         Bundle arguments = new Bundle();
72         arguments.putParcelable(EXTRA_PACKAGE_INFO, packageInfo);
73         ReviewPermissionsWearFragment instance = new ReviewPermissionsWearFragment();
74         instance.setArguments(arguments);
75         instance.setRetainInstance(true);
76         return instance;
77     }
78 
79     private AppPermissions mAppPermissions;
80 
81     private PreferenceCategory mNewPermissionsCategory;
82     private PreferenceCategory mCurrentPermissionsCategory;
83 
84     private boolean mHasConfirmedRevoke;
85 
86     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)87     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
88         Activity activity = getActivity();
89         if (activity == null) {
90             return;
91         }
92 
93         PackageInfo packageInfo = getArguments().getParcelable(EXTRA_PACKAGE_INFO);
94         if (packageInfo == null) {
95             activity.finish();
96             return;
97         }
98 
99         mAppPermissions = new AppPermissions(activity, packageInfo, false,
100                 () -> getActivity().finish());
101 
102         boolean reviewRequired = false;
103         for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) {
104             if (group.isReviewRequired()) {
105                 reviewRequired = true;
106                 break;
107             }
108         }
109 
110         if (!reviewRequired) {
111             confirmPermissionsReview();
112             activity.finish();
113         }
114     }
115 
116     @Override
onResume()117     public void onResume() {
118         super.onResume();
119         mAppPermissions.refresh();
120         loadPreferences();
121     }
122 
loadPreferences()123     private void loadPreferences() {
124         Activity activity = getActivity();
125         if (activity == null) {
126             return;
127         }
128 
129         PreferenceScreen screen = getPreferenceScreen();
130         if (screen == null) {
131             screen = getPreferenceManager().createPreferenceScreen(getActivity());
132             setPreferenceScreen(screen);
133         } else {
134             screen.removeAll();
135         }
136 
137         mCurrentPermissionsCategory = null;
138         PreferenceGroup oldNewPermissionsCategory = mNewPermissionsCategory;
139         mNewPermissionsCategory = null;
140 
141         final boolean isPackageUpdated = isPackageUpdated();
142         int permOrder = ORDER_PERM_OFFSET_START;
143 
144         PackageInfo pkg = mAppPermissions.getPackageInfo();
145         ApplicationInfo appInfo = pkg.applicationInfo;
146 
147         for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) {
148             if (!Utils.shouldShowPermission(getContext(), group)
149                     || !Utils.OS_PKG.equals(group.getDeclaringPackage())) {
150                 continue;
151             }
152 
153             final PermissionSwitchPreference preference;
154             Preference cachedPreference = oldNewPermissionsCategory != null
155                     ? oldNewPermissionsCategory.findPreference(group.getName()) : null;
156             if (cachedPreference instanceof PermissionSwitchPreference) {
157                 preference = (PermissionSwitchPreference) cachedPreference;
158             } else {
159                 preference = new PermissionSwitchPreference(getActivity());
160 
161                 preference.setKey(group.getName());
162                 preference.setTitle(group.getLabel());
163                 preference.setPersistent(false);
164                 preference.setOrder(permOrder++);
165 
166                 preference.setOnPreferenceChangeListener(this);
167             }
168 
169             if (appInfo.targetSdkVersion < Build.VERSION_CODES.M &&
170                     group.isReviewRequired() ) {
171                 preference.setChecked(true);
172             } else {
173                 preference.setChecked(group.areRuntimePermissionsGranted());
174             }
175 
176             // Mutable state
177             if (group.isSystemFixed() || group.isPolicyFixed()) {
178                 preference.setEnabled(false);
179             } else {
180                 preference.setEnabled(true);
181             }
182             if (preference.getParent() != null) {
183                 // Remove first if already added.
184                 preference.getParent().removePreference(preference);
185             }
186             if (group.isReviewRequired()) {
187                 if (!isPackageUpdated) {
188                     // An app just being installed, which means all groups requiring reviews.
189                     screen.addPreference(preference);
190                 } else {
191                     if (mNewPermissionsCategory == null) {
192                         mNewPermissionsCategory = new PreferenceCategory(activity);
193                         mNewPermissionsCategory.setTitle(R.string.new_permissions_category);
194                         mNewPermissionsCategory.setOrder(ORDER_NEW_PERMS);
195                         screen.addPreference(mNewPermissionsCategory);
196                     }
197                     mNewPermissionsCategory.addPreference(preference);
198                 }
199             } else {
200                 if (mCurrentPermissionsCategory == null) {
201                     mCurrentPermissionsCategory = new PreferenceCategory(activity);
202                     mCurrentPermissionsCategory.setTitle(R.string.current_permissions_category);
203                     mCurrentPermissionsCategory.setOrder(ORDER_CURRENT_PERMS);
204                     screen.addPreference(mCurrentPermissionsCategory);
205                 }
206                 mCurrentPermissionsCategory.addPreference(preference);
207             }
208         }
209         addTitlePreferenceToScreen(screen);
210         addActionPreferencesToScreen(screen);
211     }
212 
isPackageUpdated()213     private boolean isPackageUpdated() {
214         List<AppPermissionGroup> groups = mAppPermissions.getPermissionGroups();
215         final int groupCount = groups.size();
216         for (int i = 0; i < groupCount; i++) {
217             AppPermissionGroup group = groups.get(i);
218             if (!group.isReviewRequired()) {
219                 return true;
220             }
221         }
222         return false;
223     }
224 
225     @Override
onPreferenceChange(Preference preference, Object newValue)226     public boolean onPreferenceChange(Preference preference, Object newValue) {
227         Log.d(TAG, "onPreferenceChange " + preference.getTitle());
228         if (mHasConfirmedRevoke) {
229             return true;
230         }
231         if (preference instanceof PermissionSwitchPreference) {
232             PermissionSwitchPreference permPreference = (PermissionSwitchPreference)
233                     preference;
234             permPreference.setChanged();
235             if (permPreference.isChecked()) {
236                 showWarnRevokeDialog(permPreference);
237             } else {
238                 return true;
239             }
240         }
241         return false;
242     }
243 
showWarnRevokeDialog(final SwitchPreference preference)244     private void showWarnRevokeDialog(final SwitchPreference preference) {
245         // When revoking, we set "confirm" as the negative icon to be shown at the bottom.
246         new WearableDialogHelper.DialogBuilder(getContext())
247                 .setPositiveIcon(R.drawable.cancel_button)
248                 .setNegativeIcon(R.drawable.confirm_button)
249                 .setPositiveButton(R.string.grant_dialog_button_deny_anyway, (dialog, which) -> {
250                     preference.setChecked(false);
251                     mHasConfirmedRevoke = true;
252                 })
253                 .setNegativeButton(R.string.cancel, null)
254                 .setMessage(R.string.old_sdk_deny_warning)
255                 .show();
256     }
257 
confirmPermissionsReview()258     private void confirmPermissionsReview() {
259         final List<PreferenceGroup> preferenceGroups = new ArrayList<>();
260         if (mNewPermissionsCategory != null) {
261             preferenceGroups.add(mNewPermissionsCategory);
262         }
263         if (mCurrentPermissionsCategory != null) {
264             preferenceGroups.add(mCurrentPermissionsCategory);
265         }
266         if (preferenceGroups.isEmpty()){
267             PreferenceScreen preferenceScreen = getPreferenceScreen();
268             if (preferenceScreen != null) {
269                 preferenceGroups.add(preferenceScreen);
270             }
271         }
272 
273         for (PreferenceGroup preferenceGroup: preferenceGroups) {
274             final int preferenceCount = preferenceGroup.getPreferenceCount();
275             for (int i = 0; i < preferenceCount; i++) {
276                 Preference preference = preferenceGroup.getPreference(i);
277                 if (preference instanceof PermissionSwitchPreference) {
278                     PermissionSwitchPreference permPreference = (PermissionSwitchPreference)
279                             preference;
280                     String groupName = preference.getKey();
281                     AppPermissionGroup group = mAppPermissions.getPermissionGroup(groupName);
282                     if (group.isReviewRequired() || permPreference.wasChanged()) {
283                         if (permPreference.isChecked()) {
284                             Log.i(TAG, groupName + " permPreference.isChecked()");
285                             group.grantRuntimePermissions(true, false);
286                         } else {
287                             Log.i(TAG, groupName + " !permPreference.isChecked()");
288                             group.revokeRuntimePermissions(false);
289                         }
290                     }
291 
292                     AppPermissionGroup backgroundGroup = group.getBackgroundPermissions();
293                     if (backgroundGroup != null) {
294                         // If the preference wasn't toggled we show it as "fully granted"
295                         if (backgroundGroup.isReviewRequired() && !permPreference.wasChanged()) {
296                             backgroundGroup.grantRuntimePermissions(true, false);
297                         }
298                         backgroundGroup.unsetReviewRequired();
299                     }
300                 }
301             }
302         }
303 
304         // Some permission might be restricted and hence there is no AppPermissionGroup for it.
305         // Manually unset all review-required flags, regardless of restriction.
306         PackageManager pm = getContext().getPackageManager();
307         PackageInfo pkg = mAppPermissions.getPackageInfo();
308         UserHandle user = UserHandle.getUserHandleForUid(pkg.applicationInfo.uid);
309 
310         if (pkg.requestedPermissions == null) {
311             // No flag updating to do
312             return;
313         }
314 
315         for (String perm : pkg.requestedPermissions) {
316             try {
317                 pm.updatePermissionFlags(perm, pkg.packageName,
318                         FLAG_PERMISSION_REVIEW_REQUIRED | FLAG_PERMISSION_USER_SET,
319                         FLAG_PERMISSION_USER_SET, user);
320             } catch (IllegalArgumentException e) {
321                 Log.e(TAG, "Cannot unmark " + perm + " requested by " + pkg.packageName
322                         + " as review required", e);
323             }
324         }
325     }
326 
addTitlePreferenceToScreen(PreferenceScreen screen)327     private void addTitlePreferenceToScreen(PreferenceScreen screen) {
328         Activity activity = getActivity();
329         Preference titlePref = new Preference(activity);
330         screen.addPreference(titlePref);
331 
332         // Set icon
333         Drawable icon = mAppPermissions.getPackageInfo().applicationInfo.loadIcon(
334               activity.getPackageManager());
335         titlePref.setIcon(icon);
336 
337         // Set message
338         String appLabel = Html.escapeHtml(mAppPermissions.getAppLabel().toString());
339         final int labelTemplateResId = isPackageUpdated()
340                 ?  R.string.permission_review_title_template_update
341                 :  R.string.permission_review_title_template_install;
342         SpannableString message =
343             new SpannableString(Html.fromHtml(getString(labelTemplateResId, appLabel)));
344 
345         // Color the app name.
346         final int appLabelStart = message.toString().indexOf(appLabel, 0);
347         final int appLabelLength = appLabel.length();
348 
349         TypedValue typedValue = new TypedValue();
350         activity.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
351         final int color = activity.getColor(typedValue.resourceId);
352 
353         if (appLabelStart >= 0) {
354             message.setSpan(new ForegroundColorSpan(color), appLabelStart,
355                     appLabelStart + appLabelLength, 0);
356         }
357 
358         titlePref.setTitle(message);
359 
360         titlePref.setSelectable(false);
361         titlePref.setLayoutResource(R.layout.wear_review_permission_title_pref);
362     }
363 
addActionPreferencesToScreen(PreferenceScreen screen)364     private void addActionPreferencesToScreen(PreferenceScreen screen) {
365         final Activity activity = getActivity();
366 
367         Preference cancelPref = new Preference(activity);
368         cancelPref.setTitle(R.string.review_button_cancel);
369         cancelPref.setOrder(ORDER_ACTION);
370         cancelPref.setEnabled(true);
371         cancelPref.setLayoutResource(R.layout.wear_review_permission_action_pref);
372         cancelPref.setOnPreferenceClickListener(p -> {
373             executeCallback(false);
374             activity.setResult(Activity.RESULT_CANCELED);
375             activity.finish();
376             return true;
377         });
378         screen.addPreference(cancelPref);
379 
380         Preference continuePref = new Preference(activity);
381         continuePref.setTitle(R.string.review_button_continue);
382         continuePref.setOrder(ORDER_ACTION + 1);
383         continuePref.setEnabled(true);
384         continuePref.setLayoutResource(R.layout.wear_review_permission_action_pref);
385         continuePref.setOnPreferenceClickListener(p -> {
386             confirmPermissionsReview();
387             executeCallback(true);
388             activity.setResult(Activity.RESULT_OK);
389             getActivity().finish();
390             return true;
391         });
392         screen.addPreference(continuePref);
393     }
394 
executeCallback(boolean success)395     private void executeCallback(boolean success) {
396         Activity activity = getActivity();
397         if (activity == null) {
398             return;
399         }
400         if (success) {
401             IntentSender intent = activity.getIntent().getParcelableExtra(Intent.EXTRA_INTENT);
402             if (intent != null) {
403                 try {
404                     int flagMask = 0;
405                     int flagValues = 0;
406                     if (activity.getIntent().getBooleanExtra(
407                             Intent.EXTRA_RESULT_NEEDED, false)) {
408                         flagMask = Intent.FLAG_ACTIVITY_FORWARD_RESULT;
409                         flagValues = Intent.FLAG_ACTIVITY_FORWARD_RESULT;
410                     }
411                     activity.startIntentSenderForResult(intent, -1, null,
412                             flagMask, flagValues, 0);
413                 } catch (IntentSender.SendIntentException e) {
414                     /* ignore */
415                 }
416                 return;
417             }
418         }
419         RemoteCallback callback = activity.getIntent().getParcelableExtra(
420                 Intent.EXTRA_REMOTE_CALLBACK);
421         if (callback != null) {
422             Bundle result = new Bundle();
423             result.putBoolean(Intent.EXTRA_RETURN_RESULT, success);
424             callback.sendResult(result);
425         }
426     }
427 
428     /**
429      * Extend the {@link SwitchPreference}:
430      * <ul>
431      *     <li>Monitor the changed state</li>
432      * </ul>
433      */
434     private static class PermissionSwitchPreference extends SwitchPreference {
435         private boolean mWasChanged = false;
436 
PermissionSwitchPreference(Context context)437         PermissionSwitchPreference(Context context) {
438             super(context);
439         }
440 
441         /**
442          * Mark the permission as changed by the user
443          */
setChanged()444         void setChanged() {
445             mWasChanged = true;
446         }
447 
448         /**
449          * @return {@code true} iff the permission was changed by the user
450          */
wasChanged()451         boolean wasChanged() {
452             return mWasChanged;
453         }
454     }
455 }
456