1/*
2 * Copyright (C) 2016 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
17package com.android.settingslib.users;
18
19import android.app.AppGlobals;
20import android.appwidget.AppWidgetManager;
21import android.content.Context;
22import android.content.Intent;
23import android.content.pm.ApplicationInfo;
24import android.content.pm.IPackageManager;
25import android.content.pm.PackageInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.ParceledListSlice;
28import android.content.pm.ResolveInfo;
29import android.content.res.Resources;
30import android.graphics.drawable.Drawable;
31import android.os.RemoteException;
32import android.os.UserHandle;
33import android.os.UserManager;
34import android.text.TextUtils;
35import android.util.Log;
36import android.view.inputmethod.InputMethodInfo;
37import android.view.inputmethod.InputMethodManager;
38
39import java.util.ArrayList;
40import java.util.Collections;
41import java.util.Comparator;
42import java.util.HashMap;
43import java.util.HashSet;
44import java.util.List;
45import java.util.Map;
46import java.util.Set;
47
48public class AppRestrictionsHelper {
49    private static final boolean DEBUG = false;
50    private static final String TAG = "AppRestrictionsHelper";
51
52    private final Context mContext;
53    private final PackageManager mPackageManager;
54    private final IPackageManager mIPm;
55    private final UserManager mUserManager;
56    private final UserHandle mUser;
57    private final boolean mRestrictedProfile;
58    private boolean mLeanback;
59
60    HashMap<String,Boolean> mSelectedPackages = new HashMap<>();
61    private List<SelectableAppInfo> mVisibleApps;
62
63    public AppRestrictionsHelper(Context context, UserHandle user) {
64        mContext = context;
65        mPackageManager = context.getPackageManager();
66        mIPm = AppGlobals.getPackageManager();
67        mUser = user;
68        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
69        mRestrictedProfile = mUserManager.getUserInfo(mUser.getIdentifier()).isRestricted();
70    }
71
72    public void setPackageSelected(String packageName, boolean selected) {
73        mSelectedPackages.put(packageName, selected);
74    }
75
76    public boolean isPackageSelected(String packageName) {
77        return mSelectedPackages.get(packageName);
78    }
79
80    public void setLeanback(boolean isLeanback) {
81        mLeanback = isLeanback;
82    }
83
84    public List<SelectableAppInfo> getVisibleApps() {
85        return mVisibleApps;
86    }
87
88    public void applyUserAppsStates(OnDisableUiForPackageListener listener) {
89        final int userId = mUser.getIdentifier();
90        if (!mUserManager.getUserInfo(userId).isRestricted() && userId != UserHandle.myUserId()) {
91            Log.e(TAG, "Cannot apply application restrictions on another user!");
92            return;
93        }
94        for (Map.Entry<String,Boolean> entry : mSelectedPackages.entrySet()) {
95            String packageName = entry.getKey();
96            boolean enabled = entry.getValue();
97            applyUserAppState(packageName, enabled, listener);
98        }
99    }
100
101    public void applyUserAppState(String packageName, boolean enabled,
102            OnDisableUiForPackageListener listener) {
103        final int userId = mUser.getIdentifier();
104        if (enabled) {
105            // Enable selected apps
106            try {
107                ApplicationInfo info = mIPm.getApplicationInfo(packageName,
108                        PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
109                if (info == null || !info.enabled
110                        || (info.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
111                    mIPm.installExistingPackageAsUser(packageName, mUser.getIdentifier());
112                    if (DEBUG) {
113                        Log.d(TAG, "Installing " + packageName);
114                    }
115                }
116                if (info != null && (info.privateFlags&ApplicationInfo.PRIVATE_FLAG_HIDDEN) != 0
117                        && (info.flags&ApplicationInfo.FLAG_INSTALLED) != 0) {
118                    listener.onDisableUiForPackage(packageName);
119                    mIPm.setApplicationHiddenSettingAsUser(packageName, false, userId);
120                    if (DEBUG) {
121                        Log.d(TAG, "Unhiding " + packageName);
122                    }
123                }
124            } catch (RemoteException re) {
125                // Ignore
126            }
127        } else {
128            // Blacklist all other apps, system or downloaded
129            try {
130                ApplicationInfo info = mIPm.getApplicationInfo(packageName, 0, userId);
131                if (info != null) {
132                    if (mRestrictedProfile) {
133                        mIPm.deletePackageAsUser(packageName, null, mUser.getIdentifier(),
134                                PackageManager.DELETE_SYSTEM_APP);
135                        if (DEBUG) {
136                            Log.d(TAG, "Uninstalling " + packageName);
137                        }
138                    } else {
139                        listener.onDisableUiForPackage(packageName);
140                        mIPm.setApplicationHiddenSettingAsUser(packageName, true, userId);
141                        if (DEBUG) {
142                            Log.d(TAG, "Hiding " + packageName);
143                        }
144                    }
145                }
146            } catch (RemoteException re) {
147                // Ignore
148            }
149        }
150    }
151
152    public void fetchAndMergeApps() {
153        mVisibleApps = new ArrayList<>();
154        final PackageManager pm = mPackageManager;
155        final IPackageManager ipm = mIPm;
156
157        final HashSet<String> excludePackages = new HashSet<>();
158        addSystemImes(excludePackages);
159
160        // Add launchers
161        Intent launcherIntent = new Intent(Intent.ACTION_MAIN);
162        if (mLeanback) {
163            launcherIntent.addCategory(Intent.CATEGORY_LEANBACK_LAUNCHER);
164        } else {
165            launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
166        }
167        addSystemApps(mVisibleApps, launcherIntent, excludePackages);
168
169        // Add widgets
170        Intent widgetIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
171        addSystemApps(mVisibleApps, widgetIntent, excludePackages);
172
173        List<ApplicationInfo> installedApps = pm.getInstalledApplications(
174                PackageManager.MATCH_UNINSTALLED_PACKAGES);
175        for (ApplicationInfo app : installedApps) {
176            // If it's not installed, skip
177            if ((app.flags & ApplicationInfo.FLAG_INSTALLED) == 0) continue;
178
179            if ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0
180                    && (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0) {
181                // Downloaded app
182                SelectableAppInfo info = new SelectableAppInfo();
183                info.packageName = app.packageName;
184                info.appName = app.loadLabel(pm);
185                info.activityName = info.appName;
186                info.icon = app.loadIcon(pm);
187                mVisibleApps.add(info);
188            } else {
189                try {
190                    PackageInfo pi = pm.getPackageInfo(app.packageName, 0);
191                    // If it's a system app that requires an account and doesn't see restricted
192                    // accounts, mark for removal. It might get shown in the UI if it has an icon
193                    // but will still be marked as false and immutable.
194                    if (mRestrictedProfile
195                            && pi.requiredAccountType != null && pi.restrictedAccountType == null) {
196                        mSelectedPackages.put(app.packageName, false);
197                    }
198                } catch (PackageManager.NameNotFoundException re) {
199                    // Skip
200                }
201            }
202        }
203
204        // Get the list of apps already installed for the user
205        List<ApplicationInfo> userApps = null;
206        try {
207            ParceledListSlice<ApplicationInfo> listSlice = ipm.getInstalledApplications(
208                    PackageManager.MATCH_UNINSTALLED_PACKAGES, mUser.getIdentifier());
209            if (listSlice != null) {
210                userApps = listSlice.getList();
211            }
212        } catch (RemoteException re) {
213            // Ignore
214        }
215
216        if (userApps != null) {
217            for (ApplicationInfo app : userApps) {
218                if ((app.flags & ApplicationInfo.FLAG_INSTALLED) == 0) continue;
219
220                if ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0
221                        && (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0) {
222                    // Downloaded app
223                    SelectableAppInfo info = new SelectableAppInfo();
224                    info.packageName = app.packageName;
225                    info.appName = app.loadLabel(pm);
226                    info.activityName = info.appName;
227                    info.icon = app.loadIcon(pm);
228                    mVisibleApps.add(info);
229                }
230            }
231        }
232
233        // Sort the list of visible apps
234        Collections.sort(mVisibleApps, new AppLabelComparator());
235
236        // Remove dupes
237        Set<String> dedupPackageSet = new HashSet<String>();
238        for (int i = mVisibleApps.size() - 1; i >= 0; i--) {
239            SelectableAppInfo info = mVisibleApps.get(i);
240            if (DEBUG) Log.i(TAG, info.toString());
241            String both = info.packageName + "+" + info.activityName;
242            if (!TextUtils.isEmpty(info.packageName)
243                    && !TextUtils.isEmpty(info.activityName)
244                    && dedupPackageSet.contains(both)) {
245                mVisibleApps.remove(i);
246            } else {
247                dedupPackageSet.add(both);
248            }
249        }
250
251        // Establish master/slave relationship for entries that share a package name
252        HashMap<String,SelectableAppInfo> packageMap = new HashMap<String,SelectableAppInfo>();
253        for (SelectableAppInfo info : mVisibleApps) {
254            if (packageMap.containsKey(info.packageName)) {
255                info.masterEntry = packageMap.get(info.packageName);
256            } else {
257                packageMap.put(info.packageName, info);
258            }
259        }
260    }
261
262    /**
263     * Find all pre-installed input methods that are marked as default
264     * and add them to an exclusion list so that they aren't
265     * presented to the user for toggling.
266     * Don't add non-default ones, as they may include other stuff that we
267     * don't need to auto-include.
268     * @param excludePackages the set of package names to append to
269     */
270    private void addSystemImes(Set<String> excludePackages) {
271        InputMethodManager imm = (InputMethodManager)
272                mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
273        List<InputMethodInfo> imis = imm.getInputMethodList();
274        for (InputMethodInfo imi : imis) {
275            try {
276                if (imi.isDefault(mContext) && isSystemPackage(imi.getPackageName())) {
277                    excludePackages.add(imi.getPackageName());
278                }
279            } catch (Resources.NotFoundException rnfe) {
280                // Not default
281            }
282        }
283    }
284
285    /**
286     * Add system apps that match an intent to the list, excluding any packages in the exclude list.
287     * @param visibleApps list of apps to append the new list to
288     * @param intent the intent to match
289     * @param excludePackages the set of package names to be excluded, since they're required
290     */
291    private void addSystemApps(List<SelectableAppInfo> visibleApps, Intent intent,
292            Set<String> excludePackages) {
293        final PackageManager pm = mPackageManager;
294        List<ResolveInfo> launchableApps = pm.queryIntentActivities(intent,
295                PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_UNINSTALLED_PACKAGES);
296        for (ResolveInfo app : launchableApps) {
297            if (app.activityInfo != null && app.activityInfo.applicationInfo != null) {
298                final String packageName = app.activityInfo.packageName;
299                int flags = app.activityInfo.applicationInfo.flags;
300                if ((flags & ApplicationInfo.FLAG_SYSTEM) != 0
301                        || (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
302                    // System app
303                    // Skip excluded packages
304                    if (excludePackages.contains(packageName)) continue;
305                    int enabled = pm.getApplicationEnabledSetting(packageName);
306                    if (enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
307                            || enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
308                        // Check if the app is already enabled for the target user
309                        ApplicationInfo targetUserAppInfo = getAppInfoForUser(packageName,
310                                0, mUser);
311                        if (targetUserAppInfo == null
312                                || (targetUserAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
313                            continue;
314                        }
315                    }
316                    SelectableAppInfo info = new SelectableAppInfo();
317                    info.packageName = app.activityInfo.packageName;
318                    info.appName = app.activityInfo.applicationInfo.loadLabel(pm);
319                    info.icon = app.activityInfo.loadIcon(pm);
320                    info.activityName = app.activityInfo.loadLabel(pm);
321                    if (info.activityName == null) info.activityName = info.appName;
322
323                    visibleApps.add(info);
324                }
325            }
326        }
327    }
328
329    private boolean isSystemPackage(String packageName) {
330        try {
331            final PackageInfo pi = mPackageManager.getPackageInfo(packageName, 0);
332            if (pi.applicationInfo == null) return false;
333            final int flags = pi.applicationInfo.flags;
334            if ((flags & ApplicationInfo.FLAG_SYSTEM) != 0
335                    || (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
336                return true;
337            }
338        } catch (PackageManager.NameNotFoundException nnfe) {
339            // Missing package?
340        }
341        return false;
342    }
343
344    private ApplicationInfo getAppInfoForUser(String packageName, int flags, UserHandle user) {
345        try {
346            return mIPm.getApplicationInfo(packageName, flags, user.getIdentifier());
347        } catch (RemoteException re) {
348            return null;
349        }
350    }
351
352    public interface OnDisableUiForPackageListener {
353        void onDisableUiForPackage(String packageName);
354    }
355
356    public static class SelectableAppInfo {
357        public String packageName;
358        public CharSequence appName;
359        public CharSequence activityName;
360        public Drawable icon;
361        public SelectableAppInfo masterEntry;
362
363        @Override
364        public String toString() {
365            return packageName + ": appName=" + appName + "; activityName=" + activityName
366                    + "; icon=" + icon + "; masterEntry=" + masterEntry;
367        }
368    }
369
370    private static class AppLabelComparator implements Comparator<SelectableAppInfo> {
371
372        @Override
373        public int compare(SelectableAppInfo lhs, SelectableAppInfo rhs) {
374            String lhsLabel = lhs.activityName.toString();
375            String rhsLabel = rhs.activityName.toString();
376            return lhsLabel.toLowerCase().compareTo(rhsLabel.toLowerCase());
377        }
378    }
379}
380