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 */
16package com.android.packageinstaller.permission.model;
17
18import android.content.Context;
19import android.content.pm.ApplicationInfo;
20import android.content.pm.PackageInfo;
21import android.content.pm.PackageItemInfo;
22import android.content.pm.PackageManager;
23import android.content.pm.PackageManager.NameNotFoundException;
24import android.content.pm.PermissionInfo;
25import android.graphics.drawable.Drawable;
26import android.os.AsyncTask;
27import android.os.UserHandle;
28import android.os.UserManager;
29import android.util.ArrayMap;
30import android.util.ArraySet;
31import android.util.Log;
32import android.util.SparseArray;
33
34import com.android.packageinstaller.R;
35import com.android.packageinstaller.permission.utils.Utils;
36
37import java.util.ArrayList;
38import java.util.Collections;
39import java.util.List;
40
41public class PermissionApps {
42    private static final String LOG_TAG = "PermissionApps";
43
44    private final Context mContext;
45    private final String mGroupName;
46    private final PackageManager mPm;
47    private final Callback mCallback;
48
49    private final PmCache mCache;
50
51    private CharSequence mLabel;
52    private Drawable mIcon;
53    private List<PermissionApp> mPermApps;
54    // Map (pkg|uid) -> AppPermission
55    private ArrayMap<String, PermissionApp> mAppLookup;
56
57    private boolean mSkipUi;
58    private boolean mRefreshing;
59
60    public PermissionApps(Context context, String groupName, Callback callback) {
61        this(context, groupName, callback, null);
62    }
63
64    public PermissionApps(Context context, String groupName, Callback callback, PmCache cache) {
65        mCache = cache;
66        mContext = context;
67        mPm = mContext.getPackageManager();
68        mGroupName = groupName;
69        mCallback = callback;
70        loadGroupInfo();
71    }
72
73    public String getGroupName() {
74        return mGroupName;
75    }
76
77    public void loadNowWithoutUi() {
78        mSkipUi = true;
79        createMap(loadPermissionApps());
80    }
81
82    /**
83     * Start an async refresh and call back the registered call back once done.
84     *
85     * @param getUiInfo If the UI info should be updated
86     */
87    public void refresh(boolean getUiInfo) {
88        if (mCallback == null) {
89            throw new IllegalStateException("callback needs to be set");
90        }
91
92        if (!mRefreshing) {
93            mRefreshing = true;
94            mSkipUi = !getUiInfo;
95            new PermissionAppsLoader().execute();
96        }
97    }
98
99    /**
100     * Refresh the state and do not return until it finishes. Should not be called while an {@link
101     * #refresh async referesh} is in progress.
102     */
103    public void refreshSync() {
104        mSkipUi = true;
105        createMap(loadPermissionApps());
106    }
107
108    public int getGrantedCount(ArraySet<String> launcherPkgs) {
109        int count = 0;
110        for (PermissionApp app : mPermApps) {
111            if (!Utils.shouldShowPermission(app)) {
112                continue;
113            }
114            if (Utils.isSystem(app, launcherPkgs)) {
115                // We default to not showing system apps, so hide them from count.
116                continue;
117            }
118            if (app.areRuntimePermissionsGranted()) {
119                count++;
120            }
121        }
122        return count;
123    }
124
125    public int getTotalCount(ArraySet<String> launcherPkgs) {
126        int count = 0;
127        for (PermissionApp app : mPermApps) {
128            if (!Utils.shouldShowPermission(app)) {
129                continue;
130            }
131            if (Utils.isSystem(app, launcherPkgs)) {
132                // We default to not showing system apps, so hide them from count.
133                continue;
134            }
135            count++;
136        }
137        return count;
138    }
139
140    public List<PermissionApp> getApps() {
141        return mPermApps;
142    }
143
144    public PermissionApp getApp(String key) {
145        return mAppLookup.get(key);
146    }
147
148    public CharSequence getLabel() {
149        return mLabel;
150    }
151
152    public Drawable getIcon() {
153        return mIcon;
154    }
155
156    private List<PermissionApp> loadPermissionApps() {
157        PackageItemInfo groupInfo = getGroupInfo(mGroupName);
158        if (groupInfo == null) {
159            return Collections.emptyList();
160        }
161
162        List<PermissionInfo> groupPermInfos = getGroupPermissionInfos(mGroupName);
163        if (groupPermInfos == null) {
164            return Collections.emptyList();
165        }
166
167        ArrayList<PermissionApp> permApps = new ArrayList<>();
168
169        UserManager userManager = mContext.getSystemService(UserManager.class);
170        for (UserHandle user : userManager.getUserProfiles()) {
171            List<PackageInfo> apps = mCache != null ? mCache.getPackages(user.getIdentifier())
172                    : mPm.getInstalledPackagesAsUser(PackageManager.GET_PERMISSIONS,
173                            user.getIdentifier());
174
175            final int N = apps.size();
176            for (int i = 0; i < N; i++) {
177                PackageInfo app = apps.get(i);
178                if (app.requestedPermissions == null) {
179                    continue;
180                }
181
182                for (int j = 0; j < app.requestedPermissions.length; j++) {
183                    String requestedPerm = app.requestedPermissions[j];
184
185                    PermissionInfo requestedPermissionInfo = null;
186
187                    for (PermissionInfo groupPermInfo : groupPermInfos) {
188                        if (requestedPerm.equals(groupPermInfo.name)) {
189                            requestedPermissionInfo = groupPermInfo;
190                            break;
191                        }
192                    }
193
194                    if (requestedPermissionInfo == null) {
195                        continue;
196                    }
197
198                    if ((requestedPermissionInfo.protectionLevel
199                                & PermissionInfo.PROTECTION_MASK_BASE)
200                                    != PermissionInfo.PROTECTION_DANGEROUS
201                            || (requestedPermissionInfo.flags
202                                & PermissionInfo.FLAG_INSTALLED) == 0
203                            || (requestedPermissionInfo.flags
204                                & PermissionInfo.FLAG_REMOVED) != 0) {
205                        continue;
206                    }
207
208                    AppPermissionGroup group = AppPermissionGroup.create(mContext,
209                            app, groupInfo, groupPermInfos, user);
210
211                    if (group == null) {
212                        continue;
213                    }
214
215                    String label = mSkipUi ? app.packageName
216                            : app.applicationInfo.loadLabel(mPm).toString();
217
218                    Drawable icon = null;
219                    if (!mSkipUi) {
220                        UserHandle userHandle = new UserHandle(
221                                UserHandle.getUserId(group.getApp().applicationInfo.uid));
222
223                        icon = mPm.getUserBadgedIcon(
224                                mPm.loadUnbadgedItemIcon(app.applicationInfo, app.applicationInfo),
225                                userHandle);
226                    }
227
228                    PermissionApp permApp = new PermissionApp(app.packageName, group, label, icon,
229                            app.applicationInfo);
230
231                    permApps.add(permApp);
232                    break; // move to the next app.
233                }
234            }
235        }
236
237        Collections.sort(permApps);
238
239        return permApps;
240    }
241
242    private void createMap(List<PermissionApp> result) {
243        mAppLookup = new ArrayMap<>();
244        for (PermissionApp app : result) {
245            mAppLookup.put(app.getKey(), app);
246        }
247        mPermApps = result;
248    }
249
250    private PackageItemInfo getGroupInfo(String groupName) {
251        try {
252            return mContext.getPackageManager().getPermissionGroupInfo(groupName, 0);
253        } catch (NameNotFoundException e) {
254            /* ignore */
255        }
256        try {
257            return mContext.getPackageManager().getPermissionInfo(groupName, 0);
258        } catch (NameNotFoundException e2) {
259            /* ignore */
260        }
261        return null;
262    }
263
264    private List<PermissionInfo> getGroupPermissionInfos(String groupName) {
265        try {
266            return mContext.getPackageManager().queryPermissionsByGroup(groupName, 0);
267        } catch (NameNotFoundException e) {
268            /* ignore */
269        }
270        try {
271            PermissionInfo permissionInfo = mContext.getPackageManager()
272                    .getPermissionInfo(groupName, 0);
273            List<PermissionInfo> permissions = new ArrayList<>();
274            permissions.add(permissionInfo);
275            return permissions;
276        } catch (NameNotFoundException e2) {
277            /* ignore */
278        }
279        return null;
280    }
281
282    private void loadGroupInfo() {
283        PackageItemInfo info;
284        try {
285            info = mPm.getPermissionGroupInfo(mGroupName, 0);
286        } catch (PackageManager.NameNotFoundException e) {
287            try {
288                PermissionInfo permInfo = mPm.getPermissionInfo(mGroupName, 0);
289                if ((permInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE)
290                        != PermissionInfo.PROTECTION_DANGEROUS) {
291                    Log.w(LOG_TAG, mGroupName + " is not a runtime permission");
292                    return;
293                }
294                info = permInfo;
295            } catch (NameNotFoundException reallyNotFound) {
296                Log.w(LOG_TAG, "Can't find permission: " + mGroupName, reallyNotFound);
297                return;
298            }
299        }
300        mLabel = info.loadLabel(mPm);
301        if (info.icon != 0) {
302            mIcon = info.loadUnbadgedIcon(mPm);
303        } else {
304            mIcon = mContext.getDrawable(R.drawable.ic_perm_device_info);
305        }
306        mIcon = Utils.applyTint(mContext, mIcon, android.R.attr.colorControlNormal);
307    }
308
309    public static class PermissionApp implements Comparable<PermissionApp> {
310        private final String mPackageName;
311        private final AppPermissionGroup mAppPermissionGroup;
312        private final String mLabel;
313        private final Drawable mIcon;
314        private final ApplicationInfo mInfo;
315
316        public PermissionApp(String packageName, AppPermissionGroup appPermissionGroup,
317                String label, Drawable icon, ApplicationInfo info) {
318            mPackageName = packageName;
319            mAppPermissionGroup = appPermissionGroup;
320            mLabel = label;
321            mIcon = icon;
322            mInfo = info;
323        }
324
325        public ApplicationInfo getAppInfo() {
326            return mInfo;
327        }
328
329        public String getKey() {
330            return mPackageName + getUid();
331        }
332
333        public String getLabel() {
334            return mLabel;
335        }
336
337        public Drawable getIcon() {
338            return mIcon;
339        }
340
341        public boolean areRuntimePermissionsGranted() {
342            return mAppPermissionGroup.areRuntimePermissionsGranted();
343        }
344
345        public boolean isReviewRequired() {
346            return mAppPermissionGroup.isReviewRequired();
347        }
348
349        public void grantRuntimePermissions() {
350            mAppPermissionGroup.grantRuntimePermissions(false);
351        }
352
353        public void revokeRuntimePermissions() {
354            mAppPermissionGroup.revokeRuntimePermissions(false);
355        }
356
357        public boolean isPolicyFixed() {
358            return mAppPermissionGroup.isPolicyFixed();
359        }
360
361        public boolean isSystemFixed() {
362            return mAppPermissionGroup.isSystemFixed();
363        }
364
365        public boolean hasGrantedByDefaultPermissions() {
366            return mAppPermissionGroup.hasGrantedByDefaultPermission();
367        }
368
369        public boolean doesSupportRuntimePermissions() {
370            return mAppPermissionGroup.doesSupportRuntimePermissions();
371        }
372
373        public int getUserId() {
374            return mAppPermissionGroup.getUserId();
375        }
376
377        public String getPackageName() {
378            return mPackageName;
379        }
380
381        public AppPermissionGroup getPermissionGroup() {
382            return mAppPermissionGroup;
383        }
384
385        @Override
386        public int compareTo(PermissionApp another) {
387            final int result = mLabel.compareTo(another.mLabel);
388            if (result == 0) {
389                // Unbadged before badged.
390                return getKey().compareTo(another.getKey());
391            }
392            return result;
393        }
394
395        public int getUid() {
396            return mAppPermissionGroup.getApp().applicationInfo.uid;
397        }
398    }
399
400    private class PermissionAppsLoader extends AsyncTask<Void, Void, List<PermissionApp>> {
401
402        @Override
403        protected List<PermissionApp> doInBackground(Void... args) {
404            return loadPermissionApps();
405        }
406
407        @Override
408        protected void onPostExecute(List<PermissionApp> result) {
409            mRefreshing = false;
410            createMap(result);
411            if (mCallback != null) {
412                mCallback.onPermissionsLoaded(PermissionApps.this);
413            }
414        }
415    }
416
417    /**
418     * Class used to reduce the number of calls to the package manager.
419     * This caches app information so it should only be used across parallel PermissionApps
420     * instances, and should not be retained across UI refresh.
421     */
422    public static class PmCache {
423        private final SparseArray<List<PackageInfo>> mPackageInfoCache = new SparseArray<>();
424        private final PackageManager mPm;
425
426        public PmCache(PackageManager pm) {
427            mPm = pm;
428        }
429
430        public synchronized List<PackageInfo> getPackages(int userId) {
431            List<PackageInfo> ret = mPackageInfoCache.get(userId);
432            if (ret == null) {
433                ret = mPm.getInstalledPackagesAsUser(PackageManager.GET_PERMISSIONS, userId);
434                mPackageInfoCache.put(userId, ret);
435            }
436            return ret;
437        }
438    }
439
440    public interface Callback {
441        void onPermissionsLoaded(PermissionApps permissionApps);
442    }
443}
444