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 android.app.Activity;
20 import android.content.Intent;
21 import android.content.IntentSender;
22 import android.content.pm.PackageInfo;
23 import android.graphics.drawable.Drawable;
24 import android.os.Bundle;
25 import android.os.RemoteCallback;
26 import android.text.Html;
27 import android.text.SpannableString;
28 import android.text.style.ForegroundColorSpan;
29 import android.util.Log;
30 import android.util.TypedValue;
31 
32 import androidx.preference.Preference;
33 import androidx.preference.PreferenceCategory;
34 import androidx.preference.PreferenceFragmentCompat;
35 import androidx.preference.PreferenceGroup;
36 import androidx.preference.PreferenceScreen;
37 import androidx.preference.SwitchPreference;
38 import androidx.preference.TwoStatePreference;
39 import androidx.wear.ble.view.WearableDialogHelper;
40 
41 import com.android.permissioncontroller.R;
42 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
43 import com.android.permissioncontroller.permission.model.AppPermissions;
44 import com.android.permissioncontroller.permission.utils.Utils;
45 
46 import java.util.List;
47 
48 public class ReviewPermissionsWearFragment extends PreferenceFragmentCompat
49         implements Preference.OnPreferenceChangeListener {
50     private static final String TAG = "ReviewPermWear";
51 
52     private static final int ORDER_NEW_PERMS = 1;
53     private static final int ORDER_CURRENT_PERMS = 2;
54     // Category for showing actions should be displayed last.
55     private static final int ORDER_ACTION = 100000;
56     private static final int ORDER_PERM_OFFSET_START = 100;
57 
58     private static final String EXTRA_PACKAGE_INFO =
59         "com.android.permissioncontroller.permission.ui.extra.PACKAGE_INFO";
60 
newInstance(PackageInfo packageInfo)61     public static ReviewPermissionsWearFragment newInstance(PackageInfo packageInfo) {
62         Bundle arguments = new Bundle();
63         arguments.putParcelable(EXTRA_PACKAGE_INFO, packageInfo);
64         ReviewPermissionsWearFragment instance = new ReviewPermissionsWearFragment();
65         instance.setArguments(arguments);
66         instance.setRetainInstance(true);
67         return instance;
68     }
69 
70     private AppPermissions mAppPermissions;
71 
72     private PreferenceCategory mNewPermissionsCategory;
73 
74     private boolean mHasConfirmedRevoke;
75 
76     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)77     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
78         Activity activity = getActivity();
79         if (activity == null) {
80             return;
81         }
82 
83         PackageInfo packageInfo = getArguments().getParcelable(EXTRA_PACKAGE_INFO);
84         if (packageInfo == null) {
85             activity.finish();
86             return;
87         }
88 
89         mAppPermissions = new AppPermissions(activity, packageInfo, false,
90                 () -> getActivity().finish());
91 
92         if (mAppPermissions.getPermissionGroups().isEmpty()) {
93             activity.finish();
94             return;
95         }
96 
97         boolean reviewRequired = false;
98         for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) {
99             if (group.isReviewRequired()) {
100                 reviewRequired = true;
101                 break;
102             }
103         }
104 
105         if (!reviewRequired) {
106             activity.finish();
107         }
108     }
109 
110     @Override
onResume()111     public void onResume() {
112         super.onResume();
113         mAppPermissions.refresh();
114         loadPreferences();
115     }
116 
loadPreferences()117     private void loadPreferences() {
118         Activity activity = getActivity();
119         if (activity == null) {
120             return;
121         }
122 
123         PreferenceScreen screen = getPreferenceScreen();
124         if (screen == null) {
125             screen = getPreferenceManager().createPreferenceScreen(getActivity());
126             setPreferenceScreen(screen);
127         } else {
128             screen.removeAll();
129         }
130 
131         PreferenceGroup currentPermissionsCategory = null;
132         PreferenceGroup oldNewPermissionsCategory = mNewPermissionsCategory;
133         mNewPermissionsCategory = null;
134 
135         final boolean isPackageUpdated = isPackageUpdated();
136         int permOrder = ORDER_PERM_OFFSET_START;
137 
138         for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) {
139             if (!Utils.shouldShowPermission(getContext(), group)
140                     || !Utils.OS_PKG.equals(group.getDeclaringPackage())) {
141                 continue;
142             }
143 
144             final SwitchPreference preference;
145             Preference cachedPreference = oldNewPermissionsCategory != null
146                     ? oldNewPermissionsCategory.findPreference(group.getName()) : null;
147             if (cachedPreference instanceof SwitchPreference) {
148                 preference = (SwitchPreference) cachedPreference;
149             } else {
150                 preference = new SwitchPreference(getActivity());
151 
152                 preference.setKey(group.getName());
153                 preference.setTitle(group.getLabel());
154                 preference.setPersistent(false);
155                 preference.setOrder(permOrder++);
156 
157                 preference.setOnPreferenceChangeListener(this);
158             }
159 
160             preference.setChecked(group.areRuntimePermissionsGranted());
161 
162             // Mutable state
163             if (group.isSystemFixed() || group.isPolicyFixed()) {
164                 preference.setEnabled(false);
165             } else {
166                 preference.setEnabled(true);
167             }
168 
169             if (group.isReviewRequired()) {
170                 if (!isPackageUpdated) {
171                     // An app just being installed, which means all groups requiring reviews.
172                     screen.addPreference(preference);
173                 } else {
174                     if (mNewPermissionsCategory == null) {
175                         mNewPermissionsCategory = new PreferenceCategory(activity);
176                         mNewPermissionsCategory.setTitle(R.string.new_permissions_category);
177                         mNewPermissionsCategory.setOrder(ORDER_NEW_PERMS);
178                         screen.addPreference(mNewPermissionsCategory);
179                     }
180                     mNewPermissionsCategory.addPreference(preference);
181                 }
182             } else {
183                 if (currentPermissionsCategory == null) {
184                     currentPermissionsCategory = new PreferenceCategory(activity);
185                     currentPermissionsCategory.setTitle(R.string.current_permissions_category);
186                     currentPermissionsCategory.setOrder(ORDER_CURRENT_PERMS);
187                     screen.addPreference(currentPermissionsCategory);
188                 }
189                 currentPermissionsCategory.addPreference(preference);
190             }
191         }
192 
193         addTitlePreferenceToScreen(screen);
194         addActionPreferencesToScreen(screen);
195     }
196 
isPackageUpdated()197     private boolean isPackageUpdated() {
198         List<AppPermissionGroup> groups = mAppPermissions.getPermissionGroups();
199         final int groupCount = groups.size();
200         for (int i = 0; i < groupCount; i++) {
201             AppPermissionGroup group = groups.get(i);
202             if (!group.isReviewRequired()) {
203                 return true;
204             }
205         }
206         return false;
207     }
208 
209     @Override
onPreferenceChange(Preference preference, Object newValue)210     public boolean onPreferenceChange(Preference preference, Object newValue) {
211       Log.d(TAG, "onPreferenceChange " + preference.getTitle());
212         if (mHasConfirmedRevoke) {
213             return true;
214         }
215         if (preference instanceof SwitchPreference) {
216             SwitchPreference switchPreference = (SwitchPreference) preference;
217             if (switchPreference.isChecked()) {
218                 showWarnRevokeDialog(switchPreference);
219             } else {
220                 return true;
221             }
222         }
223         return false;
224     }
225 
showWarnRevokeDialog(final SwitchPreference preference)226     private void showWarnRevokeDialog(final SwitchPreference preference) {
227         // When revoking, we set "confirm" as the negative icon to be shown at the bottom.
228         new WearableDialogHelper.DialogBuilder(getContext())
229                 .setPositiveIcon(R.drawable.cancel_button)
230                 .setNegativeIcon(R.drawable.confirm_button)
231                 .setPositiveButton(R.string.cancel, null)
232                 .setNegativeButton(R.string.grant_dialog_button_deny_anyway,
233                         (dialog, which) -> {
234                             preference.setChecked(false);
235                             mHasConfirmedRevoke = true;
236                         })
237                 .setMessage(R.string.old_sdk_deny_warning)
238                 .show();
239     }
240 
confirmPermissionsReview()241     private void confirmPermissionsReview() {
242         PreferenceGroup preferenceGroup = mNewPermissionsCategory != null
243                 ? mNewPermissionsCategory : getPreferenceScreen();
244 
245         final int preferenceCount = preferenceGroup.getPreferenceCount();
246         for (int i = 0; i < preferenceCount; i++) {
247             Preference preference = preferenceGroup.getPreference(i);
248             if (preference instanceof TwoStatePreference) {
249                 TwoStatePreference twoStatePreference = (TwoStatePreference) preference;
250                 String groupName = preference.getKey();
251                 AppPermissionGroup group = mAppPermissions.getPermissionGroup(groupName);
252                 if (twoStatePreference.isChecked()) {
253                     group.grantRuntimePermissions(true, false);
254                 } else {
255                     group.revokeRuntimePermissions(false);
256                 }
257                 group.unsetReviewRequired();
258             }
259         }
260     }
261 
addTitlePreferenceToScreen(PreferenceScreen screen)262     private void addTitlePreferenceToScreen(PreferenceScreen screen) {
263         Activity activity = getActivity();
264         Preference titlePref = new Preference(activity);
265         screen.addPreference(titlePref);
266 
267         // Set icon
268         Drawable icon = mAppPermissions.getPackageInfo().applicationInfo.loadIcon(
269               activity.getPackageManager());
270         titlePref.setIcon(icon);
271 
272         // Set message
273         String appLabel = mAppPermissions.getAppLabel().toString();
274         final int labelTemplateResId = isPackageUpdated()
275                 ?  R.string.permission_review_title_template_update
276                 :  R.string.permission_review_title_template_install;
277         SpannableString message =
278             new SpannableString(Html.fromHtml(getString(labelTemplateResId, appLabel)));
279 
280         // Color the app name.
281         final int appLabelStart = message.toString().indexOf(appLabel, 0);
282         final int appLabelLength = appLabel.length();
283 
284         TypedValue typedValue = new TypedValue();
285         activity.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
286         final int color = activity.getColor(typedValue.resourceId);
287 
288         if (appLabelStart >= 0) {
289             message.setSpan(new ForegroundColorSpan(color), appLabelStart,
290                     appLabelStart + appLabelLength, 0);
291         }
292 
293         titlePref.setTitle(message);
294 
295         titlePref.setSelectable(false);
296         titlePref.setLayoutResource(R.layout.wear_review_permission_title_pref);
297     }
298 
addActionPreferencesToScreen(PreferenceScreen screen)299     private void addActionPreferencesToScreen(PreferenceScreen screen) {
300         final Activity activity = getActivity();
301 
302         Preference cancelPref = new Preference(activity);
303         cancelPref.setTitle(R.string.review_button_cancel);
304         cancelPref.setOrder(ORDER_ACTION);
305         cancelPref.setEnabled(true);
306         cancelPref.setLayoutResource(R.layout.wear_review_permission_action_pref);
307         cancelPref.setOnPreferenceClickListener(p -> {
308             executeCallback(false);
309             activity.setResult(Activity.RESULT_CANCELED);
310             activity.finish();
311             return true;
312         });
313         screen.addPreference(cancelPref);
314 
315         Preference continuePref = new Preference(activity);
316         continuePref.setTitle(R.string.review_button_continue);
317         continuePref.setOrder(ORDER_ACTION + 1);
318         continuePref.setEnabled(true);
319         continuePref.setLayoutResource(R.layout.wear_review_permission_action_pref);
320         continuePref.setOnPreferenceClickListener(p -> {
321             confirmPermissionsReview();
322             executeCallback(true);
323             getActivity().finish();
324             return true;
325         });
326         screen.addPreference(continuePref);
327     }
328 
executeCallback(boolean success)329     private void executeCallback(boolean success) {
330         Activity activity = getActivity();
331         if (activity == null) {
332             return;
333         }
334         if (success) {
335             IntentSender intent = activity.getIntent().getParcelableExtra(Intent.EXTRA_INTENT);
336             if (intent != null) {
337                 try {
338                     int flagMask = 0;
339                     int flagValues = 0;
340                     if (activity.getIntent().getBooleanExtra(
341                             Intent.EXTRA_RESULT_NEEDED, false)) {
342                         flagMask = Intent.FLAG_ACTIVITY_FORWARD_RESULT;
343                         flagValues = Intent.FLAG_ACTIVITY_FORWARD_RESULT;
344                     }
345                     activity.startIntentSenderForResult(intent, -1, null,
346                             flagMask, flagValues, 0);
347                 } catch (IntentSender.SendIntentException e) {
348                     /* ignore */
349                 }
350                 return;
351             }
352         }
353         RemoteCallback callback = activity.getIntent().getParcelableExtra(
354                 Intent.EXTRA_REMOTE_CALLBACK);
355         if (callback != null) {
356             Bundle result = new Bundle();
357             result.putBoolean(Intent.EXTRA_RETURN_RESULT, success);
358             callback.sendResult(result);
359         }
360     }
361 }
362