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