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