1 /*
2  * Copyright (C) 2015 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 package com.android.packageinstaller.permission.model;
17 
18 import android.content.Context;
19 import android.content.pm.ApplicationInfo;
20 import android.content.pm.PackageInfo;
21 import android.content.pm.PackageItemInfo;
22 import android.content.pm.PackageManager;
23 import android.content.pm.PackageManager.NameNotFoundException;
24 import android.content.pm.PermissionGroupInfo;
25 import android.content.pm.PermissionInfo;
26 import android.graphics.drawable.Drawable;
27 import android.os.AsyncTask;
28 import android.os.UserHandle;
29 import android.os.UserManager;
30 import android.text.TextUtils;
31 import android.util.ArrayMap;
32 import android.util.Log;
33 import android.util.Pair;
34 import android.util.SparseArray;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 
39 import com.android.packageinstaller.permission.utils.Utils;
40 import com.android.permissioncontroller.R;
41 
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.List;
45 
46 public class PermissionApps {
47     private static final String LOG_TAG = "PermissionApps";
48 
49     private final Context mContext;
50     private final String mGroupName;
51     private final String mPackageName;
52     private final PackageManager mPm;
53     private final Callback mCallback;
54 
55     private final @Nullable PmCache mPmCache;
56     private final @Nullable AppDataCache mAppDataCache;
57 
58     private CharSequence mLabel;
59     private CharSequence mFullLabel;
60     private Drawable mIcon;
61     private @Nullable CharSequence mDescription;
62     private List<PermissionApp> mPermApps;
63     // Map (pkg|uid) -> AppPermission
64     private ArrayMap<String, PermissionApp> mAppLookup;
65 
66     private boolean mSkipUi;
67     private boolean mRefreshing;
68 
PermissionApps(Context context, String groupName, String packageName)69     public PermissionApps(Context context, String groupName, String packageName) {
70         this(context, groupName, packageName, null, null, null);
71     }
72 
PermissionApps(Context context, String groupName, Callback callback)73     public PermissionApps(Context context, String groupName, Callback callback) {
74         this(context, groupName, null, callback, null, null);
75     }
76 
PermissionApps(Context context, String groupName, String packageName, Callback callback, @Nullable PmCache pmCache, @Nullable AppDataCache appDataCache)77     public PermissionApps(Context context, String groupName, String packageName,
78             Callback callback, @Nullable PmCache pmCache, @Nullable AppDataCache appDataCache) {
79         mPmCache = pmCache;
80         mAppDataCache = appDataCache;
81         mContext = context;
82         mPm = mContext.getPackageManager();
83         mGroupName = groupName;
84         mCallback = callback;
85         mPackageName = packageName;
86         loadGroupInfo();
87     }
88 
getGroupName()89     public String getGroupName() {
90         return mGroupName;
91     }
92 
loadNowWithoutUi()93     public void loadNowWithoutUi() {
94         mSkipUi = true;
95         createMap(loadPermissionApps());
96     }
97 
98     /**
99      * Start an async refresh and call back the registered call back once done.
100      *
101      * @param getUiInfo If the UI info should be updated
102      */
refresh(boolean getUiInfo)103     public void refresh(boolean getUiInfo) {
104         if (!mRefreshing) {
105             mRefreshing = true;
106             mSkipUi = !getUiInfo;
107             new PermissionAppsLoader().execute();
108         }
109     }
110 
111     /**
112      * Refresh the state and do not return until it finishes. Should not be called while an {@link
113      * #refresh async referesh} is in progress.
114      */
refreshSync(boolean getUiInfo)115     public void refreshSync(boolean getUiInfo) {
116         mSkipUi = !getUiInfo;
117         createMap(loadPermissionApps());
118     }
119 
getGrantedCount()120     public int getGrantedCount() {
121         int count = 0;
122         for (PermissionApp app : mPermApps) {
123             if (!Utils.shouldShowPermission(mContext, app.getPermissionGroup())) {
124                 continue;
125             }
126             if (!Utils.isGroupOrBgGroupUserSensitive(app.mAppPermissionGroup)) {
127                 // We default to not showing system apps, so hide them from count.
128                 continue;
129             }
130             if (app.areRuntimePermissionsGranted()) {
131                 count++;
132             }
133         }
134         return count;
135     }
136 
getTotalCount()137     public int getTotalCount() {
138         int count = 0;
139         for (PermissionApp app : mPermApps) {
140             if (!Utils.shouldShowPermission(mContext, app.getPermissionGroup())) {
141                 continue;
142             }
143             if (!Utils.isGroupOrBgGroupUserSensitive(app.mAppPermissionGroup)) {
144                 // We default to not showing system apps, so hide them from count.
145                 continue;
146             }
147             count++;
148         }
149         return count;
150     }
151 
getApps()152     public List<PermissionApp> getApps() {
153         return mPermApps;
154     }
155 
getApp(String key)156     public PermissionApp getApp(String key) {
157         return mAppLookup.get(key);
158     }
159 
getLabel()160     public CharSequence getLabel() {
161         return mLabel;
162     }
163 
getFullLabel()164     public CharSequence getFullLabel() {
165         return mFullLabel;
166     }
167 
getIcon()168     public Drawable getIcon() {
169         return mIcon;
170     }
171 
getDescription()172     public CharSequence getDescription() {
173         return mDescription;
174     }
175 
getPackageInfos(@onNull UserHandle user)176     private @NonNull List<PackageInfo> getPackageInfos(@NonNull UserHandle user) {
177         List<PackageInfo> apps = (mPmCache != null) ? mPmCache.getPackages(
178                 user.getIdentifier()) : null;
179         if (apps != null) {
180             if (mPackageName != null) {
181                 final int appCount = apps.size();
182                 for (int i = 0; i < appCount; i++) {
183                     final PackageInfo app = apps.get(i);
184                     if (mPackageName.equals(app.packageName)) {
185                         apps = new ArrayList<>(1);
186                         apps.add(app);
187                         return apps;
188                     }
189                 }
190             }
191             return apps;
192         }
193         if (mPackageName == null) {
194             return mPm.getInstalledPackagesAsUser(PackageManager.GET_PERMISSIONS,
195                     user.getIdentifier());
196         } else {
197             try {
198                 final PackageInfo packageInfo = mPm.getPackageInfo(mPackageName,
199                         PackageManager.GET_PERMISSIONS);
200                 apps = new ArrayList<>(1);
201                 apps.add(packageInfo);
202                 return apps;
203             } catch (NameNotFoundException e) {
204                 return Collections.emptyList();
205             }
206         }
207     }
208 
loadPermissionApps()209     private List<PermissionApp> loadPermissionApps() {
210         PackageItemInfo groupInfo = Utils.getGroupInfo(mGroupName, mContext);
211         if (groupInfo == null) {
212             return Collections.emptyList();
213         }
214 
215         List<PermissionInfo> groupPermInfos = Utils.getGroupPermissionInfos(mGroupName, mContext);
216         if (groupPermInfos == null) {
217             return Collections.emptyList();
218         }
219         List<PermissionInfo> targetPermInfos = new ArrayList<PermissionInfo>(groupPermInfos.size());
220         for (int i = 0; i < groupPermInfos.size(); i++) {
221             PermissionInfo permInfo = groupPermInfos.get(i);
222             if ((permInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE)
223                     == PermissionInfo.PROTECTION_DANGEROUS
224                     && (permInfo.flags & PermissionInfo.FLAG_INSTALLED) != 0
225                     && (permInfo.flags & PermissionInfo.FLAG_REMOVED) == 0) {
226                 targetPermInfos.add(permInfo);
227             }
228         }
229 
230         PackageManager packageManager = mContext.getPackageManager();
231         CharSequence groupLabel = groupInfo.loadLabel(packageManager);
232         CharSequence fullGroupLabel = groupInfo.loadSafeLabel(packageManager, 0,
233                 TextUtils.SAFE_STRING_FLAG_TRIM | TextUtils.SAFE_STRING_FLAG_FIRST_LINE);
234 
235         ArrayList<PermissionApp> permApps = new ArrayList<>();
236 
237         UserManager userManager = mContext.getSystemService(UserManager.class);
238         for (UserHandle user : userManager.getUserProfiles()) {
239             List<PackageInfo> apps = getPackageInfos(user);
240             final int N = apps.size();
241             for (int i = 0; i < N; i++) {
242                 PackageInfo app = apps.get(i);
243                 if (app.requestedPermissions == null) {
244                     continue;
245                 }
246 
247                 for (int j = 0; j < app.requestedPermissions.length; j++) {
248                     String requestedPerm = app.requestedPermissions[j];
249 
250                     PermissionInfo requestedPermissionInfo = null;
251 
252                     for (PermissionInfo groupPermInfo : targetPermInfos) {
253                         if (requestedPerm.equals(groupPermInfo.name)) {
254                             requestedPermissionInfo = groupPermInfo;
255                             break;
256                         }
257                     }
258 
259                     if (requestedPermissionInfo == null) {
260                         continue;
261                     }
262 
263                     AppPermissionGroup group = AppPermissionGroup.create(mContext,
264                             app, groupInfo, groupPermInfos, groupLabel, fullGroupLabel, false);
265 
266                     if (group == null) {
267                         continue;
268                     }
269 
270                     Pair<String, Drawable> appData = null;
271                     if (mAppDataCache != null && !mSkipUi) {
272                         appData = mAppDataCache.getAppData(user.getIdentifier(),
273                                 app.applicationInfo);
274                     }
275 
276                     String label;
277                     if (mSkipUi) {
278                         label = app.packageName;
279                     } else if (appData != null) {
280                         label = appData.first;
281                     } else {
282                         label = app.applicationInfo.loadLabel(mPm).toString();
283                     }
284 
285                     Drawable icon = null;
286                     if (!mSkipUi) {
287                         if (appData != null) {
288                             icon = appData.second;
289                         } else {
290                             icon = Utils.getBadgedIcon(mContext, app.applicationInfo);
291                         }
292                     }
293 
294                     PermissionApp permApp = new PermissionApp(app.packageName, group, label, icon,
295                             app.applicationInfo);
296 
297                     permApps.add(permApp);
298                     break; // move to the next app.
299                 }
300             }
301         }
302 
303         Collections.sort(permApps);
304 
305         return permApps;
306     }
307 
createMap(List<PermissionApp> result)308     private void createMap(List<PermissionApp> result) {
309         mAppLookup = new ArrayMap<>();
310         for (PermissionApp app : result) {
311             mAppLookup.put(app.getKey(), app);
312         }
313         mPermApps = result;
314     }
315 
loadGroupInfo()316     private void loadGroupInfo() {
317         PackageItemInfo info;
318         try {
319             info = mPm.getPermissionGroupInfo(mGroupName, 0);
320         } catch (PackageManager.NameNotFoundException e) {
321             try {
322                 PermissionInfo permInfo = mPm.getPermissionInfo(mGroupName, 0);
323                 if ((permInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE)
324                         != PermissionInfo.PROTECTION_DANGEROUS) {
325                     Log.w(LOG_TAG, mGroupName + " is not a runtime permission");
326                     return;
327                 }
328                 info = permInfo;
329             } catch (NameNotFoundException reallyNotFound) {
330                 Log.w(LOG_TAG, "Can't find permission: " + mGroupName, reallyNotFound);
331                 return;
332             }
333         }
334         mLabel = info.loadLabel(mPm);
335         mFullLabel = info.loadSafeLabel(mPm, 0,
336                 TextUtils.SAFE_STRING_FLAG_TRIM | TextUtils.SAFE_STRING_FLAG_FIRST_LINE);
337         if (info.icon != 0) {
338             mIcon = info.loadUnbadgedIcon(mPm);
339         } else {
340             mIcon = mContext.getDrawable(R.drawable.ic_perm_device_info);
341         }
342         mIcon = Utils.applyTint(mContext, mIcon, android.R.attr.colorControlNormal);
343         if (info instanceof PermissionGroupInfo) {
344             mDescription = ((PermissionGroupInfo) info).loadDescription(mPm);
345         } else if (info instanceof PermissionInfo) {
346             mDescription = ((PermissionInfo) info).loadDescription(mPm);
347         }
348     }
349 
350     public static class PermissionApp implements Comparable<PermissionApp> {
351         private final String mPackageName;
352         private final AppPermissionGroup mAppPermissionGroup;
353         private String mLabel;
354         private Drawable mIcon;
355         private final ApplicationInfo mInfo;
356 
PermissionApp(String packageName, AppPermissionGroup appPermissionGroup, String label, Drawable icon, ApplicationInfo info)357         public PermissionApp(String packageName, AppPermissionGroup appPermissionGroup,
358                 String label, Drawable icon, ApplicationInfo info) {
359             mPackageName = packageName;
360             mAppPermissionGroup = appPermissionGroup;
361             mLabel = label;
362             mIcon = icon;
363             mInfo = info;
364         }
365 
getAppInfo()366         public ApplicationInfo getAppInfo() {
367             return mInfo;
368         }
369 
getKey()370         public String getKey() {
371             return mPackageName + getUid();
372         }
373 
getLabel()374         public String getLabel() {
375             return mLabel;
376         }
377 
getIcon()378         public Drawable getIcon() {
379             return mIcon;
380         }
381 
areRuntimePermissionsGranted()382         public boolean areRuntimePermissionsGranted() {
383             return mAppPermissionGroup.areRuntimePermissionsGranted();
384         }
385 
isReviewRequired()386         public boolean isReviewRequired() {
387             return mAppPermissionGroup.isReviewRequired();
388         }
389 
grantRuntimePermissions()390         public void grantRuntimePermissions() {
391             mAppPermissionGroup.grantRuntimePermissions(false);
392         }
393 
revokeRuntimePermissions()394         public void revokeRuntimePermissions() {
395             mAppPermissionGroup.revokeRuntimePermissions(false);
396         }
397 
isPolicyFixed()398         public boolean isPolicyFixed() {
399             return mAppPermissionGroup.isPolicyFixed();
400         }
401 
isSystemFixed()402         public boolean isSystemFixed() {
403             return mAppPermissionGroup.isSystemFixed();
404         }
405 
hasGrantedByDefaultPermissions()406         public boolean hasGrantedByDefaultPermissions() {
407             return mAppPermissionGroup.hasGrantedByDefaultPermission();
408         }
409 
doesSupportRuntimePermissions()410         public boolean doesSupportRuntimePermissions() {
411             return mAppPermissionGroup.doesSupportRuntimePermissions();
412         }
413 
getPackageName()414         public String getPackageName() {
415             return mPackageName;
416         }
417 
getPermissionGroup()418         public AppPermissionGroup getPermissionGroup() {
419             return mAppPermissionGroup;
420         }
421 
422         /**
423          * Load this app's label and icon if they were not previously loaded.
424          *
425          * @param appDataCache the cache of already-loaded labels and icons.
426          */
loadLabelAndIcon(@onNull AppDataCache appDataCache)427         public void loadLabelAndIcon(@NonNull AppDataCache appDataCache) {
428             if (mInfo.packageName.equals(mLabel) || mIcon == null) {
429                 Pair<String, Drawable> appData = appDataCache.getAppData(getUid(), mInfo);
430                 mLabel = appData.first;
431                 mIcon = appData.second;
432             }
433         }
434 
435         @Override
compareTo(PermissionApp another)436         public int compareTo(PermissionApp another) {
437             final int result = mLabel.compareTo(another.mLabel);
438             if (result == 0) {
439                 // Unbadged before badged.
440                 return getKey().compareTo(another.getKey());
441             }
442             return result;
443         }
444 
getUid()445         public int getUid() {
446             return mAppPermissionGroup.getApp().applicationInfo.uid;
447         }
448     }
449 
450     private class PermissionAppsLoader extends AsyncTask<Void, Void, List<PermissionApp>> {
451 
452         @Override
doInBackground(Void... args)453         protected List<PermissionApp> doInBackground(Void... args) {
454             return loadPermissionApps();
455         }
456 
457         @Override
onPostExecute(List<PermissionApp> result)458         protected void onPostExecute(List<PermissionApp> result) {
459             mRefreshing = false;
460             createMap(result);
461             if (mCallback != null) {
462                 mCallback.onPermissionsLoaded(PermissionApps.this);
463             }
464         }
465     }
466 
467     /**
468      * Class used to reduce the number of calls to the package manager.
469      * This caches app information so it should only be used across parallel PermissionApps
470      * instances, and should not be retained across UI refresh.
471      */
472     public static class PmCache {
473         private final SparseArray<List<PackageInfo>> mPackageInfoCache = new SparseArray<>();
474         private final PackageManager mPm;
475 
PmCache(PackageManager pm)476         public PmCache(PackageManager pm) {
477             mPm = pm;
478         }
479 
getPackages(int userId)480         public synchronized List<PackageInfo> getPackages(int userId) {
481             List<PackageInfo> ret = mPackageInfoCache.get(userId);
482             if (ret == null) {
483                 ret = mPm.getInstalledPackagesAsUser(PackageManager.GET_PERMISSIONS, userId);
484                 mPackageInfoCache.put(userId, ret);
485             }
486             return ret;
487         }
488     }
489 
490     /**
491      * Class used to reduce the number of calls to loading labels and icons.
492      * This caches app information so it should only be used across parallel PermissionApps
493      * instances, and should not be retained across UI refresh.
494      */
495     public static class AppDataCache {
496         private final @NonNull SparseArray<ArrayMap<String, Pair<String, Drawable>>> mCache =
497                 new SparseArray<>();
498         private final @NonNull PackageManager mPm;
499         private final @NonNull Context mContext;
500 
AppDataCache(@onNull PackageManager pm, @NonNull Context context)501         public AppDataCache(@NonNull PackageManager pm, @NonNull Context context) {
502             mPm = pm;
503             mContext = context;
504         }
505 
506         /**
507          * Get the label and icon for the given app.
508          *
509          * @param userId the user id.
510          * @param app The app
511          *
512          * @return a pair of the label and icon.
513          */
getAppData(int userId, @NonNull ApplicationInfo app)514         public @NonNull Pair<String, Drawable> getAppData(int userId,
515                 @NonNull ApplicationInfo app) {
516             ArrayMap<String, Pair<String, Drawable>> dataForUser = mCache.get(userId);
517             if (dataForUser == null) {
518                 dataForUser = new ArrayMap<>();
519                 mCache.put(userId, dataForUser);
520             }
521             Pair<String, Drawable> data = dataForUser.get(app.packageName);
522             if (data == null) {
523                 data = Pair.create(app.loadLabel(mPm).toString(),
524                         Utils.getBadgedIcon(mContext, app));
525                 dataForUser.put(app.packageName, data);
526             }
527             return data;
528         }
529     }
530 
531     public interface Callback {
onPermissionsLoaded(PermissionApps permissionApps)532         void onPermissionsLoaded(PermissionApps permissionApps);
533     }
534 
535     /**
536      * Class used to asyncronously load apps' labels and icons.
537      */
538     public static class AppDataLoader extends AsyncTask<PermissionApp, Void, Void> {
539 
540         private final Context mContext;
541         private final Runnable mCallback;
542 
AppDataLoader(Context context, Runnable callback)543         public AppDataLoader(Context context, Runnable callback) {
544             mContext = context;
545             mCallback = callback;
546         }
547 
548         @Override
doInBackground(PermissionApp... args)549         protected Void doInBackground(PermissionApp... args) {
550             AppDataCache appDataCache = new AppDataCache(mContext.getPackageManager(), mContext);
551             int numArgs = args.length;
552             for (int i = 0; i < numArgs; i++) {
553                 args[i].loadLabelAndIcon(appDataCache);
554             }
555             return null;
556         }
557 
558         @Override
onPostExecute(Void result)559         protected void onPostExecute(Void result) {
560             mCallback.run();
561         }
562     }
563 }
564