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