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
17package com.android.tv.settings.users;
18
19import com.android.tv.settings.R;
20import com.android.tv.settings.dialog.DialogFragment;
21import com.android.tv.settings.dialog.DialogFragment.Action;
22
23import android.appwidget.AppWidgetManager;
24import android.content.Context;
25import android.content.Intent;
26import android.content.Intent.ShortcutIconResource;
27import android.content.pm.ActivityInfo;
28import android.content.pm.ApplicationInfo;
29import android.content.pm.IPackageManager;
30import android.content.pm.PackageInfo;
31import android.content.pm.PackageItemInfo;
32import android.content.pm.PackageManager;
33import android.content.pm.ResolveInfo;
34import android.content.pm.PackageManager.NameNotFoundException;
35import android.content.res.Resources;
36import android.graphics.drawable.Drawable;
37import android.net.Uri;
38import android.os.AsyncTask;
39import android.os.RemoteException;
40import android.text.TextUtils;
41import android.util.Log;
42import android.view.inputmethod.InputMethodInfo;
43import android.view.inputmethod.InputMethodManager;
44
45import com.android.tv.settings.util.UriUtils;
46
47import java.util.ArrayList;
48import java.util.Collections;
49import java.util.Comparator;
50import java.util.HashMap;
51import java.util.HashSet;
52import java.util.List;
53import java.util.Set;
54
55class 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