1 /*
2  * Copyright (C) 2019 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.auto;
18 
19 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID;
20 
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.ApplicationInfo;
24 import android.content.pm.PackageInfo;
25 import android.content.pm.PackageItemInfo;
26 import android.content.pm.PackageManager;
27 import android.content.pm.PermissionGroupInfo;
28 import android.content.pm.PermissionInfo;
29 import android.graphics.drawable.Drawable;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.os.UserHandle;
33 import android.text.TextUtils;
34 import android.util.Log;
35 import android.view.View;
36 import android.widget.Switch;
37 
38 import androidx.annotation.NonNull;
39 import androidx.preference.Preference;
40 import androidx.preference.PreferenceCategory;
41 import androidx.preference.PreferenceGroup;
42 import androidx.preference.PreferenceViewHolder;
43 import androidx.preference.SwitchPreference;
44 
45 import com.android.car.ui.AlertDialogBuilder;
46 import com.android.permissioncontroller.R;
47 import com.android.permissioncontroller.auto.AutoSettingsFrameFragment;
48 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
49 import com.android.permissioncontroller.permission.model.Permission;
50 import com.android.permissioncontroller.permission.utils.ArrayUtils;
51 import com.android.permissioncontroller.permission.utils.PermissionMapping;
52 import com.android.permissioncontroller.permission.utils.Utils;
53 
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.List;
57 
58 /** Screen which shows all permissions for a particular app. */
59 public class AutoAllAppPermissionsFragment extends AutoSettingsFrameFragment {
60 
61     private static final String LOG_TAG = "AllAppPermsFrag";
62     private static final String KEY_OTHER = "other_perms";
63 
64     private List<AppPermissionGroup> mGroups;
65 
66     /** Creates an {@link AutoAllAppPermissionsFragment} with no filter. */
newInstance(@onNull String packageName, @NonNull UserHandle userHandle, long sessionId)67     public static AutoAllAppPermissionsFragment newInstance(@NonNull String packageName,
68             @NonNull UserHandle userHandle, long sessionId) {
69         return newInstance(packageName, /* filterGroup= */ null, userHandle, sessionId);
70     }
71 
72     /** Creates an {@link AutoAllAppPermissionsFragment} with a specific filter group. */
newInstance(@onNull String packageName, @NonNull String filterGroup, @NonNull UserHandle userHandle, long sessionId)73     public static AutoAllAppPermissionsFragment newInstance(@NonNull String packageName,
74             @NonNull String filterGroup, @NonNull UserHandle userHandle, long sessionId) {
75         AutoAllAppPermissionsFragment instance = new AutoAllAppPermissionsFragment();
76         Bundle arguments = new Bundle();
77         arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
78         arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, filterGroup);
79         arguments.putParcelable(Intent.EXTRA_USER, userHandle);
80         arguments.putLong(EXTRA_SESSION_ID, sessionId);
81         instance.setArguments(arguments);
82         return instance;
83     }
84 
85     @Override
onCreatePreferences(Bundle bundle, String s)86     public void onCreatePreferences(Bundle bundle, String s) {
87         setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext()));
88     }
89 
90     @Override
onStart()91     public void onStart() {
92         super.onStart();
93 
94         // If we target a group make this look like app permissions.
95         if (getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME) == null) {
96             setHeaderLabel(getContext().getString(R.string.all_permissions));
97         } else {
98             setHeaderLabel(getContext().getString(R.string.app_permissions));
99         }
100 
101         updateUi();
102     }
103 
104     @Override
onStop()105     public void onStop() {
106         super.onStop();
107         getPreferenceScreen().removeAll();
108     }
109 
updateUi()110     private void updateUi() {
111         PreferenceGroup otherGroup = new PreferenceCategory(getContext());
112         otherGroup.setKey(KEY_OTHER);
113         otherGroup.setTitle(R.string.other_permissions);
114         getPreferenceScreen().addPreference(otherGroup);
115         ArrayList<Preference> prefs = new ArrayList<>(); // Used for sorting.
116         prefs.add(otherGroup);
117         String pkg = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
118         String filterGroup = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
119         UserHandle userHandle = getArguments().getParcelable(Intent.EXTRA_USER);
120         otherGroup.removeAll();
121         PackageManager pm = getContext().getPackageManager();
122 
123         PackageInfo info = AutoPermissionsUtils.getPackageInfo(requireActivity(), pkg, userHandle);
124         if (info == null) {
125             return;
126         }
127 
128         ApplicationInfo appInfo = info.applicationInfo;
129         Preference header = AutoPermissionsUtils.createHeaderPreference(getContext(), appInfo);
130         header.setOrder(0);
131         getPreferenceScreen().addPreference(header);
132 
133         if (info.requestedPermissions != null) {
134             for (int i = 0; i < info.requestedPermissions.length; i++) {
135                 PermissionInfo perm;
136                 try {
137                     perm = pm.getPermissionInfo(info.requestedPermissions[i], /* flags= */ 0);
138                 } catch (PackageManager.NameNotFoundException e) {
139                     Log.e(LOG_TAG,
140                             "Can't get permission info for " + info.requestedPermissions[i], e);
141                     continue;
142                 }
143 
144                 if ((perm.flags & PermissionInfo.FLAG_INSTALLED) == 0
145                         || (perm.flags & PermissionInfo.FLAG_REMOVED) != 0) {
146                     continue;
147                 }
148 
149                 if (appInfo.isInstantApp()
150                         && (perm.protectionLevel & PermissionInfo.PROTECTION_FLAG_INSTANT)
151                         == 0) {
152                     continue;
153                 }
154                 if (appInfo.targetSdkVersion < Build.VERSION_CODES.M
155                         && (perm.protectionLevel & PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY)
156                         != 0) {
157                     continue;
158                 }
159 
160                 if ((perm.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE)
161                         == PermissionInfo.PROTECTION_DANGEROUS) {
162                     PackageItemInfo group =
163                             getGroup(PermissionMapping.getGroupOfPermission(perm), pm);
164                     if (group == null) {
165                         group = perm;
166                     }
167                     // If we show a targeted group, then ignore everything else.
168                     if (filterGroup != null && !group.name.equals(filterGroup)) {
169                         continue;
170                     }
171                     PreferenceGroup pref = findOrCreate(group, pm, prefs);
172                     pref.addPreference(getPreference(info, perm, group, pm));
173                 } else if (filterGroup == null) {
174                     if ((perm.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE)
175                             == PermissionInfo.PROTECTION_NORMAL) {
176                         PermissionGroupInfo group = getGroup(perm.group, pm);
177                         otherGroup.addPreference(getPreference(info,
178                                 perm, group, pm));
179                     }
180                 }
181 
182                 // If we show a targeted group, then don't show 'other' permissions.
183                 if (filterGroup != null) {
184                     getPreferenceScreen().removePreference(otherGroup);
185                 }
186             }
187         }
188 
189         // Sort an ArrayList of the groups and then set the order from the sorting.
190         Collections.sort(prefs, (lhs, rhs) -> {
191             String lKey = lhs.getKey();
192             String rKey = rhs.getKey();
193             if (lKey.equals(KEY_OTHER)) {
194                 return 1;
195             } else if (rKey.equals(KEY_OTHER)) {
196                 return -1;
197             } else if (PermissionMapping.isPlatformPermissionGroup(lKey)
198                     != PermissionMapping.isPlatformPermissionGroup(rKey)) {
199                 return PermissionMapping.isPlatformPermissionGroup(lKey) ? -1 : 1;
200             }
201             return lhs.getTitle().toString().compareTo(rhs.getTitle().toString());
202         });
203         for (int i = 0; i < prefs.size(); i++) {
204             prefs.get(i).setOrder(i + 1);
205         }
206     }
207 
getGroup(String group, PackageManager pm)208     private PermissionGroupInfo getGroup(String group, PackageManager pm) {
209         try {
210             return pm.getPermissionGroupInfo(group, /* flags= */ 0);
211         } catch (PackageManager.NameNotFoundException e) {
212             return null;
213         }
214     }
215 
findOrCreate(PackageItemInfo group, PackageManager pm, ArrayList<Preference> prefs)216     private PreferenceGroup findOrCreate(PackageItemInfo group, PackageManager pm,
217             ArrayList<Preference> prefs) {
218         PreferenceGroup pref = findPreference(group.name);
219         if (pref == null) {
220             pref = new PreferenceCategory(getPreferenceManager().getContext());
221             pref.setKey(group.name);
222             pref.setTitle(group.loadLabel(pm));
223             prefs.add(pref);
224             getPreferenceScreen().addPreference(pref);
225         }
226         return pref;
227     }
228 
getPreference(PackageInfo packageInfo, PermissionInfo perm, PackageItemInfo group, PackageManager pm)229     private Preference getPreference(PackageInfo packageInfo, PermissionInfo perm,
230             PackageItemInfo group, PackageManager pm) {
231         final Preference pref;
232         Context context = getPreferenceManager().getContext();
233 
234         // We allow individual permission control for some permissions if review enabled
235         final boolean mutable = Utils.isPermissionIndividuallyControlled(getContext(), perm.name);
236         if (mutable) {
237             pref = new MyMultiTargetSwitchPreference(context, perm.name,
238                     getPermissionForegroundGroup(packageInfo, perm.name));
239         } else {
240             pref = new Preference(context);
241         }
242 
243         Drawable icon;
244         if (perm.icon != 0) {
245             icon = perm.loadUnbadgedIcon(pm);
246         } else if (group != null && group.icon != 0) {
247             icon = group.loadUnbadgedIcon(pm);
248         } else {
249             icon = context.getDrawable(
250                     com.android.permissioncontroller.R.drawable.ic_perm_device_info);
251         }
252         pref.setIcon(Utils.applyTint(context, icon, android.R.attr.colorControlNormal));
253         pref.setTitle(
254                 perm.loadSafeLabel(pm, /* ellipsizeDip= */ 20000, TextUtils.SAFE_STRING_FLAG_TRIM));
255         pref.setSingleLineTitle(false);
256         final CharSequence desc = perm.loadDescription(pm);
257 
258         pref.setOnPreferenceClickListener((Preference preference) -> {
259             new AlertDialogBuilder(getContext())
260                     .setMessage(desc)
261                     .setPositiveButton(android.R.string.ok, /* listener= */ null)
262                     .show();
263             return mutable;
264         });
265 
266         return pref;
267     }
268 
269     /**
270      * Return the (foreground-) {@link AppPermissionGroup group} a permission belongs to.
271      *
272      * <p>For foreground or non background-foreground permissions this returns the group
273      * {@link AppPermissionGroup} the permission is in. For background permisisons this returns
274      * the group the matching foreground
275      *
276      * @param packageInfo Package information about the app
277      * @param permission  The permission that belongs to a group
278      * @return the group the permissions belongs to
279      */
getPermissionForegroundGroup(PackageInfo packageInfo, String permission)280     private AppPermissionGroup getPermissionForegroundGroup(PackageInfo packageInfo,
281             String permission) {
282         AppPermissionGroup appPermissionGroup = null;
283         if (mGroups != null) {
284             final int groupCount = mGroups.size();
285             for (int i = 0; i < groupCount; i++) {
286                 AppPermissionGroup currentPermissionGroup = mGroups.get(i);
287                 if (currentPermissionGroup.hasPermission(permission)) {
288                     appPermissionGroup = currentPermissionGroup;
289                     break;
290                 }
291                 if (currentPermissionGroup.getBackgroundPermissions() != null
292                         && currentPermissionGroup.getBackgroundPermissions().hasPermission(
293                         permission)) {
294                     appPermissionGroup = currentPermissionGroup.getBackgroundPermissions();
295                     break;
296                 }
297             }
298         }
299         if (appPermissionGroup == null) {
300             appPermissionGroup = AppPermissionGroup.create(
301                     getContext(), packageInfo, permission, /* delayChanges= */ false);
302             if (mGroups == null) {
303                 mGroups = new ArrayList<>();
304             }
305             mGroups.add(appPermissionGroup);
306         }
307         return appPermissionGroup;
308     }
309 
310 
311     private static final class MyMultiTargetSwitchPreference extends SwitchPreference {
312         private View.OnClickListener mSwitchOnClickLister;
313 
MyMultiTargetSwitchPreference(Context context, String permission, AppPermissionGroup appPermissionGroup)314         MyMultiTargetSwitchPreference(Context context, String permission,
315                 AppPermissionGroup appPermissionGroup) {
316             super(context);
317 
318             setChecked(appPermissionGroup.areRuntimePermissionsGranted(
319                     new String[]{permission}));
320 
321             setSwitchOnClickListener(v -> {
322                 Switch switchView = (Switch) v;
323                 if (switchView.isChecked()) {
324                     appPermissionGroup.grantRuntimePermissions(false, false,
325                             new String[]{permission});
326                     // We are granting a permission from a group but since this is an
327                     // individual permission control other permissions in the group may
328                     // be revoked, hence we need to mark them user fixed to prevent the
329                     // app from requesting a non-granted permission and it being granted
330                     // because another permission in the group is granted. This applies
331                     // only to apps that support runtime permissions.
332                     if (appPermissionGroup.doesSupportRuntimePermissions()) {
333                         int grantedCount = 0;
334                         String[] revokedPermissionsToFix = null;
335                         final int permissionCount = appPermissionGroup.getPermissions().size();
336                         for (int i = 0; i < permissionCount; i++) {
337                             Permission current = appPermissionGroup.getPermissions().get(i);
338                             if (!current.isGrantedIncludingAppOp()) {
339                                 if (!current.isUserFixed()) {
340                                     revokedPermissionsToFix = ArrayUtils.appendString(
341                                             revokedPermissionsToFix, current.getName());
342                                 }
343                             } else {
344                                 grantedCount++;
345                             }
346                         }
347                         if (revokedPermissionsToFix != null) {
348                             // If some permissions were not granted then they should be fixed.
349                             appPermissionGroup.revokeRuntimePermissions(/* fixedByTheUser= */ true,
350                                     revokedPermissionsToFix);
351                         } else if (appPermissionGroup.getPermissions().size() == grantedCount) {
352                             // If all permissions are granted then they should not be fixed.
353                             appPermissionGroup.grantRuntimePermissions(true,
354                                     /* fixedByTheUser= */ false);
355                         }
356                     }
357                 } else {
358                     appPermissionGroup.revokeRuntimePermissions(/* fixedByTheUser= */ true,
359                             new String[]{permission});
360                     // If we just revoked the last permission we need to clear
361                     // the user fixed state as now the app should be able to
362                     // request them at runtime if supported.
363                     if (appPermissionGroup.doesSupportRuntimePermissions()
364                             && !appPermissionGroup.areRuntimePermissionsGranted()) {
365                         appPermissionGroup.revokeRuntimePermissions(/* fixedByTheUser= */ false);
366                     }
367                 }
368             });
369         }
370 
371         @Override
setChecked(boolean checked)372         public void setChecked(boolean checked) {
373             // If double target behavior is enabled do nothing
374             if (mSwitchOnClickLister == null) {
375                 super.setChecked(checked);
376             }
377         }
378 
setSwitchOnClickListener(View.OnClickListener listener)379         void setSwitchOnClickListener(View.OnClickListener listener) {
380             mSwitchOnClickLister = listener;
381         }
382 
383         @Override
onBindViewHolder(PreferenceViewHolder holder)384         public void onBindViewHolder(PreferenceViewHolder holder) {
385             super.onBindViewHolder(holder);
386             Switch switchView = holder.itemView.findViewById(android.R.id.switch_widget);
387             if (switchView != null) {
388                 switchView.setOnClickListener(mSwitchOnClickLister);
389             }
390         }
391     }
392 }
393