Home | History | Annotate | Download | only in users
      1 /*
      2  * Copyright (C) 2014 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.tv.settings.users;
     18 
     19 import com.android.tv.settings.R;
     20 import com.android.tv.settings.dialog.DialogFragment;
     21 import com.android.tv.settings.dialog.DialogFragment.Action;
     22 
     23 import android.appwidget.AppWidgetManager;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.Intent.ShortcutIconResource;
     27 import android.content.pm.ActivityInfo;
     28 import android.content.pm.ApplicationInfo;
     29 import android.content.pm.IPackageManager;
     30 import android.content.pm.PackageInfo;
     31 import android.content.pm.PackageItemInfo;
     32 import android.content.pm.PackageManager;
     33 import android.content.pm.ResolveInfo;
     34 import android.content.pm.PackageManager.NameNotFoundException;
     35 import android.content.res.Resources;
     36 import android.graphics.drawable.Drawable;
     37 import android.net.Uri;
     38 import android.os.AsyncTask;
     39 import android.os.RemoteException;
     40 import android.text.TextUtils;
     41 import android.util.Log;
     42 import android.view.inputmethod.InputMethodInfo;
     43 import android.view.inputmethod.InputMethodManager;
     44 
     45 import com.android.tv.settings.util.UriUtils;
     46 
     47 import java.util.ArrayList;
     48 import java.util.Collections;
     49 import java.util.Comparator;
     50 import java.util.HashMap;
     51 import java.util.HashSet;
     52 import java.util.List;
     53 import java.util.Set;
     54 
     55 class AppLoadingTask extends AsyncTask<Void, Void, List<AppLoadingTask.SelectableAppInfo>> {
     56 
     57     interface Listener {
     58         void onPackageEnableChanged(String packageName, boolean enabled);
     59 
     60         void onActionsLoaded(ArrayList<Action> actions);
     61     }
     62 
     63     private static final boolean DEBUG = false;
     64     private static final String TAG = "RestrictedProfile";
     65 
     66     private final Context mContext;
     67     private final int mUserId;
     68     private final boolean mNewUser;
     69     private final PackageManager mPackageManager;
     70     private final IPackageManager mIPackageManager;
     71     private final Listener mListener;
     72     private final PackageInfo mSysPackageInfo;
     73     private final HashMap<String, Boolean> mSelectedPackages = new HashMap<String, Boolean>();
     74     private boolean mFirstTime = true;
     75 
     76     /**
     77      * Loads the list of activities that the user can enable or disable in a restricted profile.
     78      *
     79      * @param context context for querying the list of activities.
     80      * @param userId the user ID of the user whose apps should be listed.
     81      * @param newUser true if this is a newly create user.
     82      * @param iPackageManager used to get application info.
     83      * @param listener listener for package enable state changes.
     84      */
     85     AppLoadingTask(Context context, int userId, boolean newUser, IPackageManager iPackageManager,
     86             Listener listener) {
     87         mContext = context;
     88         mUserId = userId;
     89         mNewUser = newUser;
     90         mPackageManager = context.getPackageManager();
     91         mIPackageManager = iPackageManager;
     92         mListener = listener;
     93         PackageInfo sysPackageInfo = null;
     94         try {
     95             sysPackageInfo = mPackageManager.getPackageInfo("android",
     96                     PackageManager.GET_SIGNATURES);
     97         } catch (NameNotFoundException nnfe) {
     98             Log.wtf(TAG, "Failed to get package signatures!");
     99         }
    100         mSysPackageInfo = sysPackageInfo;
    101     }
    102 
    103     @Override
    104     protected List<SelectableAppInfo> doInBackground(Void... params) {
    105         return fetchAndMergeApps();
    106     }
    107 
    108     @Override
    109     protected void onPostExecute(List<SelectableAppInfo> visibleApps) {
    110         populateApps(visibleApps);
    111     }
    112 
    113     private void populateApps(List<SelectableAppInfo> visibleApps) {
    114         ArrayList<Action> actions = new ArrayList<Action>();
    115         Intent restrictionsIntent = new Intent(Intent.ACTION_GET_RESTRICTION_ENTRIES);
    116         List<ResolveInfo> receivers = mPackageManager.queryBroadcastReceivers(restrictionsIntent,
    117                 0);
    118         for (SelectableAppInfo app : visibleApps) {
    119             String packageName = app.packageName;
    120             if (packageName == null) {
    121                 if (DEBUG) {
    122                     Log.d(TAG, "App has no package name: " + app.appName);
    123                 }
    124                 continue;
    125             }
    126             final boolean isSettingsApp = packageName.equals(mContext.getPackageName());
    127             final boolean hasSettings = resolveInfoListHasPackage(receivers, packageName);
    128             boolean isAllowed = false;
    129             String controllingActivity = null;
    130             if (app.masterEntry != null) {
    131                 controllingActivity = app.masterEntry.activityName.toString();
    132             }
    133             boolean hasCustomizableRestrictions = ((hasSettings || isSettingsApp)
    134                     && app.masterEntry == null);
    135             PackageInfo pi = null;
    136             try {
    137                 pi = mIPackageManager.getPackageInfo(packageName,
    138                         PackageManager.GET_UNINSTALLED_PACKAGES
    139                         | PackageManager.GET_SIGNATURES, mUserId);
    140             } catch (RemoteException e) {
    141             }
    142             boolean canBeEnabledDisabled = true;
    143             if (pi != null && (pi.requiredForAllUsers || isPlatformSigned(pi))) {
    144                 isAllowed = true;
    145                 canBeEnabledDisabled = false;
    146                 // If the app is required and has no restrictions, skip showing it
    147                 if (!hasSettings && !isSettingsApp) {
    148                     if (DEBUG) {
    149                         Log.d(TAG, "App is required and has no settings: " + app.appName);
    150                     }
    151                     continue;
    152                 }
    153                 // Get and populate the defaults, since the user is not going to be
    154                 // able to toggle this app ON (it's ON by default and immutable).
    155                 // Only do this for restricted profiles, not single-user restrictions
    156                 // Also don't do this for slave icons
    157             } else if (!mNewUser && isAppEnabledForUser(pi)) {
    158                 isAllowed = true;
    159             }
    160             boolean availableForRestrictedProfile = true;
    161             if (pi.requiredAccountType != null && pi.restrictedAccountType == null) {
    162                 availableForRestrictedProfile = false;
    163                 isAllowed = false;
    164                 canBeEnabledDisabled = false;
    165             }
    166             boolean canSeeRestrictedAccounts = pi.restrictedAccountType != null;
    167             if (app.masterEntry != null) {
    168                 canBeEnabledDisabled = false;
    169                 isAllowed = mSelectedPackages.get(packageName);
    170             }
    171             onPackageEnableChanged(packageName, isAllowed);
    172             if (DEBUG) {
    173                 Log.d(TAG, "Adding action for: " + app.appName + " has restrictions: "
    174                         + hasCustomizableRestrictions);
    175             }
    176             actions.add(UserAppRestrictionsDialogFragment.createAction(mContext, packageName,
    177                     app.activityName.toString(), getAppIconUri(mContext, app.info, app.iconRes),
    178                     canBeEnabledDisabled, isAllowed, hasCustomizableRestrictions,
    179                     canSeeRestrictedAccounts, availableForRestrictedProfile, controllingActivity));
    180         }
    181         mListener.onActionsLoaded(actions);
    182         // If this is the first time for a new profile, install/uninstall default apps for
    183         // profile
    184         // to avoid taking the hit in onPause(), which can cause race conditions on user switch.
    185         if (mNewUser && mFirstTime) {
    186             mFirstTime = false;
    187             UserAppRestrictionsDialogFragment.applyUserAppsStates(mSelectedPackages, actions,
    188                     mIPackageManager, mUserId);
    189         }
    190     }
    191 
    192     private void onPackageEnableChanged(String packageName, boolean enabled) {
    193         mListener.onPackageEnableChanged(packageName, enabled);
    194         mSelectedPackages.put(packageName, enabled);
    195     }
    196 
    197     private boolean resolveInfoListHasPackage(List<ResolveInfo> receivers, String packageName) {
    198         for (ResolveInfo info : receivers) {
    199             if (info.activityInfo.packageName.equals(packageName)) {
    200                 return true;
    201             }
    202         }
    203         return false;
    204     }
    205 
    206     private List<SelectableAppInfo> fetchAndMergeApps() {
    207         List<SelectableAppInfo> visibleApps = new ArrayList<SelectableAppInfo>();
    208 
    209         // Find all pre-installed input methods that are marked as default and add them to an
    210         // exclusion list so that they aren't presented to the user for toggling. Don't add
    211         // non-default ones, as they may include other stuff that we don't need to auto-include.
    212         final HashSet<String> defaultSystemImes = getDefaultSystemImes();
    213 
    214         // Add Settings
    215         try {
    216             visibleApps.add(new SelectableAppInfo(mPackageManager,
    217                     mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)));
    218         } catch (NameNotFoundException nnfe) {
    219             Log.e(TAG, "Couldn't add settings item to list!", nnfe);
    220         }
    221 
    222         // Add leanback launchers
    223         Intent leanbackLauncherIntent = new Intent(Intent.ACTION_MAIN);
    224         leanbackLauncherIntent.addCategory(Intent.CATEGORY_LEANBACK_LAUNCHER);
    225         addSystemApps(visibleApps, leanbackLauncherIntent, defaultSystemImes, mUserId);
    226 
    227         // Add widgets
    228         Intent widgetIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
    229         addSystemApps(visibleApps, widgetIntent, defaultSystemImes, mUserId);
    230 
    231         List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications(
    232                 PackageManager.GET_UNINSTALLED_PACKAGES);
    233         addNonSystemApps(installedApps, true, visibleApps);
    234 
    235         // Get the list of apps already installed for the user
    236         try {
    237             List<ApplicationInfo> userApps = mIPackageManager.getInstalledApplications(
    238                     PackageManager.GET_UNINSTALLED_PACKAGES, mUserId).getList();
    239             addNonSystemApps(userApps, false, visibleApps);
    240         } catch (RemoteException re) {
    241         }
    242 
    243         // Sort the list of visible apps
    244         Collections.sort(visibleApps, new AppLabelComparator());
    245 
    246         // Remove dupes
    247         Set<String> dedupPackageSet = new HashSet<String>();
    248         for (int i = visibleApps.size() - 1; i >= 0; i--) {
    249             SelectableAppInfo info = visibleApps.get(i);
    250             if (DEBUG) {
    251                 Log.i(TAG, info.toString());
    252             }
    253             String both = info.packageName + "+" + info.activityName;
    254             if (!TextUtils.isEmpty(info.packageName)
    255                     && !TextUtils.isEmpty(info.activityName)
    256                     && dedupPackageSet.contains(both)) {
    257                 if (DEBUG) {
    258                     Log.d(TAG, "Removing app: " + info.appName);
    259                 }
    260                 visibleApps.remove(i);
    261             } else {
    262                 dedupPackageSet.add(both);
    263             }
    264         }
    265 
    266         // Establish master/slave relationship for entries that share a package name
    267         HashMap<String, SelectableAppInfo> packageMap = new HashMap<String,
    268                 SelectableAppInfo>();
    269         for (SelectableAppInfo info : visibleApps) {
    270             if (packageMap.containsKey(info.packageName)) {
    271                 info.masterEntry = packageMap.get(info.packageName);
    272             } else {
    273                 packageMap.put(info.packageName, info);
    274             }
    275         }
    276         return visibleApps;
    277     }
    278 
    279     private void addNonSystemApps(List<ApplicationInfo> apps, boolean disableSystemApps,
    280             List<SelectableAppInfo> visibleApps) {
    281         if (apps == null) {
    282             return;
    283         }
    284 
    285         for (ApplicationInfo app : apps) {
    286             // If it's not installed, skip
    287             if ((app.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
    288                 continue;
    289             }
    290 
    291             if ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0
    292                     && (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0) {
    293                 // Downloaded app
    294                 visibleApps.add(new SelectableAppInfo(mPackageManager, app));
    295             } else if (disableSystemApps) {
    296                 try {
    297                     PackageInfo pi = mPackageManager.getPackageInfo(app.packageName, 0);
    298                     // If it's a system app that requires an account and doesn't see restricted
    299                     // accounts, mark for removal. It might get shown in the UI if it has an
    300                     // icon but will still be marked as false and immutable.
    301                     if (pi.requiredAccountType != null && pi.restrictedAccountType == null) {
    302                         onPackageEnableChanged(app.packageName, false);
    303                     }
    304                 } catch (NameNotFoundException re) {
    305                 }
    306             }
    307         }
    308     }
    309 
    310     static class SelectableAppInfo {
    311         private final String packageName;
    312         private final CharSequence appName;
    313         private final CharSequence activityName;
    314         private final ApplicationInfo info;
    315         private final int iconRes;
    316         private SelectableAppInfo masterEntry;
    317 
    318         SelectableAppInfo(PackageManager packageManager, ResolveInfo resolveInfo) {
    319             packageName = resolveInfo.activityInfo.packageName;
    320             appName = resolveInfo.activityInfo.applicationInfo.loadLabel(packageManager);
    321             CharSequence label = resolveInfo.activityInfo.loadLabel(packageManager);
    322             activityName = (label != null) ? label : appName;
    323             int activityIconRes = getIconResource(resolveInfo.activityInfo);
    324             info = resolveInfo.activityInfo.applicationInfo;
    325             iconRes = activityIconRes != 0 ? activityIconRes
    326                     : getIconResource(resolveInfo.activityInfo.applicationInfo);
    327         }
    328 
    329         SelectableAppInfo(PackageManager packageManager, ApplicationInfo applicationInfo) {
    330             packageName = applicationInfo.packageName;
    331             appName = applicationInfo.loadLabel(packageManager);
    332             activityName = appName;
    333             info = applicationInfo;
    334             iconRes = getIconResource(applicationInfo);
    335         }
    336 
    337         @Override
    338         public String toString() {
    339             return packageName + ": appName=" + appName + "; activityName=" + activityName
    340                     + "; masterEntry=" + masterEntry;
    341         }
    342 
    343         private int getIconResource(PackageItemInfo packageItemInfo) {
    344             if (packageItemInfo.banner != 0) {
    345                 return packageItemInfo.banner;
    346             }
    347             if (packageItemInfo.logo != 0) {
    348                 return packageItemInfo.logo;
    349             }
    350             return packageItemInfo.icon;
    351         }
    352     }
    353 
    354     private static class AppLabelComparator implements Comparator<SelectableAppInfo> {
    355 
    356         @Override
    357         public int compare(SelectableAppInfo lhs, SelectableAppInfo rhs) {
    358             String lhsLabel = lhs.activityName.toString();
    359             String rhsLabel = rhs.activityName.toString();
    360             return lhsLabel.toLowerCase().compareTo(rhsLabel.toLowerCase());
    361         }
    362     }
    363 
    364     /**
    365      * Find all pre-installed input methods that are marked as default and add them to an exclusion
    366      * list so that they aren't presented to the user for toggling. Don't add non-default ones, as
    367      * they may include other stuff that we don't need to auto-include.
    368      *
    369      * @return the set of default system imes
    370      */
    371     private HashSet<String> getDefaultSystemImes() {
    372         HashSet<String> defaultSystemImes = new HashSet<String>();
    373         InputMethodManager imm = (InputMethodManager)
    374                 mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
    375         List<InputMethodInfo> imis = imm.getInputMethodList();
    376         for (InputMethodInfo imi : imis) {
    377             try {
    378                 if (imi.isDefault(mContext) && isSystemPackage(imi.getPackageName())) {
    379                     defaultSystemImes.add(imi.getPackageName());
    380                 }
    381             } catch (Resources.NotFoundException rnfe) {
    382                 // Not default
    383             }
    384         }
    385         return defaultSystemImes;
    386     }
    387 
    388     private boolean isSystemPackage(String packageName) {
    389         try {
    390             final PackageInfo pi = mPackageManager.getPackageInfo(packageName, 0);
    391             if (pi.applicationInfo == null)
    392                 return false;
    393             final int flags = pi.applicationInfo.flags;
    394             if ((flags & ApplicationInfo.FLAG_SYSTEM) != 0
    395                     || (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
    396                 return true;
    397             }
    398         } catch (NameNotFoundException nnfe) {
    399             // Missing package?
    400         }
    401         return false;
    402     }
    403 
    404     /**
    405      * Add system apps that match an intent to the list, excluding any packages in the exclude list.
    406      *
    407      * @param visibleApps list of apps to append the new list to
    408      * @param intent the intent to match
    409      * @param excludePackages the set of package names to be excluded, since they're required
    410      */
    411     private void addSystemApps(List<SelectableAppInfo> visibleApps, Intent intent,
    412             Set<String> excludePackages, int userId) {
    413         final PackageManager pm = mPackageManager;
    414         List<ResolveInfo> launchableApps = pm.queryIntentActivities(intent,
    415                 PackageManager.GET_DISABLED_COMPONENTS
    416                 | PackageManager.GET_UNINSTALLED_PACKAGES);
    417         for (ResolveInfo app : launchableApps) {
    418             if (app.activityInfo != null && app.activityInfo.applicationInfo != null) {
    419                 final String packageName = app.activityInfo.packageName;
    420                 int flags = app.activityInfo.applicationInfo.flags;
    421                 if ((flags & ApplicationInfo.FLAG_SYSTEM) != 0
    422                         || (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
    423                     if (DEBUG) {
    424                         Log.d(TAG, "Found system app: "
    425                                 + app.activityInfo.applicationInfo.loadLabel(pm));
    426                     }
    427                     // System app
    428                     // Skip excluded packages
    429                     if (excludePackages.contains(packageName)) {
    430                         if (DEBUG) {
    431                             Log.d(TAG, "App is an excluded ime, not adding: "
    432                                     + app.activityInfo.applicationInfo.loadLabel(pm));
    433                         }
    434                         continue;
    435                     }
    436                     int enabled = pm.getApplicationEnabledSetting(packageName);
    437                     if (enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
    438                             || enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
    439                         // Check if the app is already enabled for the target user
    440                         ApplicationInfo targetUserAppInfo = getAppInfoForUser(packageName,
    441                                 0, userId);
    442                         if (targetUserAppInfo == null
    443                                 || (targetUserAppInfo.flags & ApplicationInfo.FLAG_INSTALLED)
    444                                         == 0) {
    445                             if (DEBUG) {
    446                                 Log.d(TAG, "App is already something, not adding: "
    447                                         + app.activityInfo.applicationInfo.loadLabel(pm));
    448                             }
    449                             continue;
    450                         }
    451                     }
    452 
    453                     if (DEBUG) {
    454                         Log.d(TAG, "Adding system app: "
    455                                 + app.activityInfo.applicationInfo.loadLabel(pm));
    456                     }
    457                     visibleApps.add(new SelectableAppInfo(pm, app));
    458                 }
    459             }
    460         }
    461     }
    462 
    463     private ApplicationInfo getAppInfoForUser(String packageName, int flags, int userId) {
    464         try {
    465             ApplicationInfo targetUserAppInfo = mIPackageManager.getApplicationInfo(packageName,
    466                     flags,
    467                     userId);
    468             return targetUserAppInfo;
    469         } catch (RemoteException re) {
    470             return null;
    471         }
    472     }
    473 
    474     private boolean isPlatformSigned(PackageInfo pi) {
    475         return (pi != null && pi.signatures != null &&
    476                 mSysPackageInfo.signatures[0].equals(pi.signatures[0]));
    477     }
    478 
    479     private boolean isAppEnabledForUser(PackageInfo pi) {
    480         if (pi == null)
    481             return false;
    482         final int flags = pi.applicationInfo.flags;
    483         // Return true if it is installed and not hidden
    484         return ((flags & ApplicationInfo.FLAG_INSTALLED) != 0
    485                 && (flags & ApplicationInfo.FLAG_HIDDEN) == 0);
    486     }
    487 
    488     private static Uri getAppIconUri(Context context, ApplicationInfo info, int iconRes) {
    489         String iconUri = null;
    490         if (iconRes != 0) {
    491             try {
    492                 Resources resources = context.getPackageManager()
    493                         .getResourcesForApplication(info);
    494                 ShortcutIconResource iconResource = new ShortcutIconResource();
    495                 iconResource.packageName = info.packageName;
    496                 iconResource.resourceName = resources.getResourceName(iconRes);
    497                 iconUri = UriUtils.getShortcutIconResourceUri(iconResource).toString();
    498             } catch (Exception e1) {
    499                 Log.w("AppsBrowseInfo", e1.toString());
    500             }
    501         } else {
    502             iconUri = UriUtils.getAndroidResourceUri(Resources.getSystem(),
    503                     com.android.internal.R.drawable.sym_def_app_icon);
    504         }
    505 
    506         if (iconUri == null) {
    507             iconUri = UriUtils.getAndroidResourceUri(context.getResources(),
    508                     com.android.internal.R.drawable.sym_app_on_sd_unavailable_icon);
    509         }
    510         return Uri.parse(iconUri);
    511     }
    512 }
    513