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