1 /* 2 * Copyright (C) 2018 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.car.carlauncher; 18 19 import static java.lang.annotation.RetentionPolicy.SOURCE; 20 21 import android.app.Activity; 22 import android.app.ActivityOptions; 23 import android.car.Car; 24 import android.car.CarNotConnectedException; 25 import android.car.content.pm.CarPackageManager; 26 import android.car.media.CarMediaManager; 27 import android.content.ComponentName; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.ApplicationInfo; 31 import android.content.pm.LauncherActivityInfo; 32 import android.content.pm.LauncherApps; 33 import android.content.pm.PackageManager; 34 import android.content.pm.ResolveInfo; 35 import android.content.res.Resources; 36 import android.content.res.XmlResourceParser; 37 import android.os.Process; 38 import android.service.media.MediaBrowserService; 39 import android.text.TextUtils; 40 import android.util.ArraySet; 41 import android.util.Log; 42 43 import androidx.annotation.IntDef; 44 import androidx.annotation.NonNull; 45 import androidx.annotation.Nullable; 46 import androidx.annotation.VisibleForTesting; 47 48 import org.xmlpull.v1.XmlPullParser; 49 import org.xmlpull.v1.XmlPullParserException; 50 51 import java.io.IOException; 52 import java.lang.annotation.Retention; 53 import java.util.ArrayDeque; 54 import java.util.ArrayList; 55 import java.util.Collections; 56 import java.util.Comparator; 57 import java.util.HashMap; 58 import java.util.List; 59 import java.util.Map; 60 import java.util.Set; 61 import java.util.function.Predicate; 62 63 /** 64 * Util class that contains helper method used by app launcher classes. 65 */ 66 public class AppLauncherUtils { 67 private static final String TAG = "AppLauncherUtils"; 68 69 @Retention(SOURCE) 70 @IntDef({APP_TYPE_LAUNCHABLES, APP_TYPE_MEDIA_SERVICES}) 71 @interface AppTypes {} 72 static final int APP_TYPE_LAUNCHABLES = 1; 73 static final int APP_TYPE_MEDIA_SERVICES = 2; 74 75 private static final String TAG_AUTOMOTIVE_APP = "automotiveApp"; 76 private static final String TAG_USES = "uses"; 77 private static final String ATTRIBUTE_NAME = "name"; 78 private static final String TYPE_VIDEO = "video"; 79 80 // Max no. of uses tags in automotiveApp XML. This is an arbitrary limit to be defensive 81 // to bad input. 82 private static final int MAX_APP_TYPES = 64; 83 AppLauncherUtils()84 private AppLauncherUtils() { 85 } 86 87 /** 88 * Comparator for {@link AppMetaData} that sorts the list 89 * by the "displayName" property in ascending order. 90 */ 91 static final Comparator<AppMetaData> ALPHABETICAL_COMPARATOR = Comparator 92 .comparing(AppMetaData::getDisplayName, String::compareToIgnoreCase); 93 94 /** 95 * Helper method that launches the app given the app's AppMetaData. 96 * 97 * @param app the requesting app's AppMetaData 98 */ launchApp(Context context, Intent intent)99 static void launchApp(Context context, Intent intent) { 100 ActivityOptions options = ActivityOptions.makeBasic(); 101 options.setLaunchDisplayId(context.getDisplayId()); 102 context.startActivity(intent, options.toBundle()); 103 } 104 105 /** Bundles application and services info. */ 106 static class LauncherAppsInfo { 107 /* 108 * Map of all car launcher components' (including launcher activities and media services) 109 * metadata keyed by ComponentName. 110 */ 111 private final Map<ComponentName, AppMetaData> mLaunchables; 112 113 /** Map of all the media services keyed by ComponentName. */ 114 private final Map<ComponentName, ResolveInfo> mMediaServices; 115 LauncherAppsInfo(@onNull Map<ComponentName, AppMetaData> launchablesMap, @NonNull Map<ComponentName, ResolveInfo> mediaServices)116 LauncherAppsInfo(@NonNull Map<ComponentName, AppMetaData> launchablesMap, 117 @NonNull Map<ComponentName, ResolveInfo> mediaServices) { 118 mLaunchables = launchablesMap; 119 mMediaServices = mediaServices; 120 } 121 122 /** Returns true if all maps are empty. */ isEmpty()123 boolean isEmpty() { 124 return mLaunchables.isEmpty() && mMediaServices.isEmpty(); 125 } 126 127 /** 128 * Returns whether the given componentName is a media service. 129 */ isMediaService(ComponentName componentName)130 boolean isMediaService(ComponentName componentName) { 131 return mMediaServices.containsKey(componentName); 132 } 133 134 /** Returns the {@link AppMetaData} for the given componentName. */ 135 @Nullable getAppMetaData(ComponentName componentName)136 AppMetaData getAppMetaData(ComponentName componentName) { 137 return mLaunchables.get(componentName); 138 } 139 140 /** Returns a new list of all launchable components' {@link AppMetaData}. */ 141 @NonNull getLaunchableComponentsList()142 List<AppMetaData> getLaunchableComponentsList() { 143 return new ArrayList<>(mLaunchables.values()); 144 } 145 } 146 147 private final static LauncherAppsInfo EMPTY_APPS_INFO = new LauncherAppsInfo( 148 Collections.emptyMap(), Collections.emptyMap()); 149 150 /* 151 * Gets the media source in a given package. If there are multiple sources in the package, 152 * returns the first one. 153 */ getMediaSource(@onNull PackageManager packageManager, @NonNull String packageName)154 static ComponentName getMediaSource(@NonNull PackageManager packageManager, 155 @NonNull String packageName) { 156 Intent mediaIntent = new Intent(); 157 mediaIntent.setPackage(packageName); 158 mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE); 159 160 List<ResolveInfo> mediaServices = packageManager.queryIntentServices(mediaIntent, 161 PackageManager.GET_RESOLVED_FILTER); 162 163 if (mediaServices == null || mediaServices.isEmpty()) { 164 return null; 165 } 166 String defaultService = mediaServices.get(0).serviceInfo.name; 167 if (!TextUtils.isEmpty(defaultService)) { 168 return new ComponentName(packageName, defaultService); 169 } 170 return null; 171 } 172 173 /** 174 * Gets all the components that we want to see in the launcher in unsorted order, including 175 * launcher activities and media services. 176 * 177 * @param appsToHide A (possibly empty) list of apps (package names) to hide 178 * @param customMediaComponents A (possibly empty) list of media components (component names) 179 * that shouldn't be shown in Launcher because their applications' 180 * launcher activities will be shown 181 * @param appTypes Types of apps to show (e.g.: all, or media sources only) 182 * @param openMediaCenter Whether launcher should navigate to media center when the 183 * user selects a media source. 184 * @param launcherApps The {@link LauncherApps} system service 185 * @param carPackageManager The {@link CarPackageManager} system service 186 * @param packageManager The {@link PackageManager} system service 187 * @param videoAppPredicate Predicate that checks if a given {@link ResolveInfo} resolves 188 * to a video app. See {@link #VideoAppPredicate}. Media-services 189 * of such apps are always excluded. 190 * @param carMediaManager The {@link CarMediaManager} system service 191 * @return a new {@link LauncherAppsInfo} 192 */ 193 @NonNull getLauncherApps( @onNull Set<String> appsToHide, @NonNull Set<String> customMediaComponents, @AppTypes int appTypes, boolean openMediaCenter, LauncherApps launcherApps, CarPackageManager carPackageManager, PackageManager packageManager, @NonNull Predicate<ResolveInfo> videoAppPredicate, CarMediaManager carMediaManager)194 static LauncherAppsInfo getLauncherApps( 195 @NonNull Set<String> appsToHide, 196 @NonNull Set<String> customMediaComponents, 197 @AppTypes int appTypes, 198 boolean openMediaCenter, 199 LauncherApps launcherApps, 200 CarPackageManager carPackageManager, 201 PackageManager packageManager, 202 @NonNull Predicate<ResolveInfo> videoAppPredicate, 203 CarMediaManager carMediaManager) { 204 205 if (launcherApps == null || carPackageManager == null || packageManager == null 206 || carMediaManager == null) { 207 return EMPTY_APPS_INFO; 208 } 209 210 // Useing new list since we require a mutable list to do removeIf. 211 List<ResolveInfo> mediaServices = new ArrayList<>(); 212 mediaServices.addAll( 213 packageManager.queryIntentServices( 214 new Intent(MediaBrowserService.SERVICE_INTERFACE), 215 PackageManager.GET_RESOLVED_FILTER)); 216 // Exclude Media Services from Video apps from being considered. These apps should offer a 217 // normal Launcher Activity as an entry point. 218 mediaServices.removeIf(videoAppPredicate); 219 220 List<LauncherActivityInfo> availableActivities = 221 launcherApps.getActivityList(null, Process.myUserHandle()); 222 223 int launchablesSize = mediaServices.size() + availableActivities.size(); 224 Map<ComponentName, AppMetaData> launchablesMap = new HashMap<>(launchablesSize); 225 Map<ComponentName, ResolveInfo> mediaServicesMap = new HashMap<>(mediaServices.size()); 226 Set<String> mEnabledPackages = new ArraySet<>(launchablesSize); 227 228 // Process media services 229 if ((appTypes & APP_TYPE_MEDIA_SERVICES) != 0) { 230 for (ResolveInfo info : mediaServices) { 231 String packageName = info.serviceInfo.packageName; 232 String className = info.serviceInfo.name; 233 ComponentName componentName = new ComponentName(packageName, className); 234 mediaServicesMap.put(componentName, info); 235 mEnabledPackages.add(packageName); 236 if (shouldAddToLaunchables(componentName, appsToHide, customMediaComponents, 237 appTypes, APP_TYPE_MEDIA_SERVICES)) { 238 final boolean isDistractionOptimized = true; 239 240 Intent intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE); 241 intent.putExtra(Car.CAR_EXTRA_MEDIA_COMPONENT, componentName.flattenToString()); 242 243 AppMetaData appMetaData = new AppMetaData( 244 info.serviceInfo.loadLabel(packageManager), 245 componentName, 246 info.serviceInfo.loadIcon(packageManager), 247 isDistractionOptimized, 248 context -> { 249 if (openMediaCenter) { 250 AppLauncherUtils.launchApp(context, intent); 251 } else { 252 selectMediaSourceAndFinish(context, componentName, carMediaManager); 253 } 254 }, 255 context -> { 256 // getLaunchIntentForPackage looks for a main activity in the category 257 // Intent.CATEGORY_INFO, then Intent.CATEGORY_LAUNCHER, and returns null 258 // if neither are found 259 Intent packageLaunchIntent = 260 packageManager.getLaunchIntentForPackage(packageName); 261 AppLauncherUtils.launchApp(context, 262 packageLaunchIntent != null ? packageLaunchIntent : intent); 263 }); 264 launchablesMap.put(componentName, appMetaData); 265 } 266 } 267 } 268 269 // Process activities 270 if ((appTypes & APP_TYPE_LAUNCHABLES) != 0) { 271 for (LauncherActivityInfo info : availableActivities) { 272 ComponentName componentName = info.getComponentName(); 273 String packageName = componentName.getPackageName(); 274 mEnabledPackages.add(packageName); 275 if (shouldAddToLaunchables(componentName, appsToHide, customMediaComponents, 276 appTypes, APP_TYPE_LAUNCHABLES)) { 277 boolean isDistractionOptimized = 278 isActivityDistractionOptimized(carPackageManager, packageName, 279 info.getName()); 280 281 Intent intent = new Intent(Intent.ACTION_MAIN) 282 .setComponent(componentName) 283 .addCategory(Intent.CATEGORY_LAUNCHER) 284 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 285 286 AppMetaData appMetaData = new AppMetaData( 287 info.getLabel(), 288 componentName, 289 info.getBadgedIcon(0), 290 isDistractionOptimized, 291 context -> AppLauncherUtils.launchApp(context, intent), 292 null); 293 launchablesMap.put(componentName, appMetaData); 294 } 295 } 296 297 List<ResolveInfo> disabledActivities = getDisabledActivities( 298 packageManager, mEnabledPackages); 299 300 for (ResolveInfo info : disabledActivities) { 301 String packageName = info.activityInfo.packageName; 302 String className = info.activityInfo.name; 303 ComponentName componentName = new ComponentName(packageName, className); 304 if (!shouldAddToLaunchables(componentName, appsToHide, customMediaComponents, 305 appTypes, APP_TYPE_LAUNCHABLES)) { 306 continue; 307 } 308 boolean isDistractionOptimized = 309 isActivityDistractionOptimized(carPackageManager, packageName, className); 310 311 Intent intent = new Intent(Intent.ACTION_MAIN) 312 .setComponent(componentName) 313 .addCategory(Intent.CATEGORY_LAUNCHER) 314 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 315 316 AppMetaData appMetaData = new AppMetaData( 317 info.activityInfo.loadLabel(packageManager), 318 componentName, 319 info.activityInfo.loadIcon(packageManager), 320 isDistractionOptimized, 321 context -> { 322 packageManager.setApplicationEnabledSetting(packageName, 323 PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0); 324 /* Fetch the current enabled setting to make sure the setting is synced 325 * before launching the activity. Otherwise, the activity may not 326 * launch. 327 */ 328 if (packageManager.getApplicationEnabledSetting(packageName) 329 != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { 330 throw new IllegalStateException( 331 "Failed to enable the disabled package [" + packageName 332 + "]"); 333 } 334 Log.i(TAG, "Successfully enabled package [" + packageName + "]"); 335 AppLauncherUtils.launchApp(context, intent); 336 }, 337 null); 338 launchablesMap.put(componentName, appMetaData); 339 } 340 } 341 342 return new LauncherAppsInfo(launchablesMap, mediaServicesMap); 343 } 344 345 /** 346 * Predicate that can be used to check if a given {@link ResolveInfo} resolves to a Video app. 347 */ 348 static class VideoAppPredicate implements Predicate<ResolveInfo> { 349 private final PackageManager mPackageManager; 350 VideoAppPredicate(PackageManager packageManager)351 VideoAppPredicate(PackageManager packageManager) { 352 mPackageManager = packageManager; 353 } 354 355 @Override test(ResolveInfo resolveInfo)356 public boolean test(ResolveInfo resolveInfo) { 357 String packageName = resolveInfo != null ? getPackageName(resolveInfo) : null; 358 if (packageName == null) { 359 Log.w(TAG, "Unable to determine packageName from resolveInfo"); 360 return false; 361 } 362 List<String> automotiveAppTypes = 363 getAutomotiveAppTypes(mPackageManager, getPackageName(resolveInfo)); 364 return automotiveAppTypes.contains(TYPE_VIDEO); 365 } 366 getPackageName(ResolveInfo resolveInfo)367 protected String getPackageName(ResolveInfo resolveInfo) { 368 // A valid ResolveInfo should have exactly one of these set. 369 if (resolveInfo.activityInfo != null) { 370 return resolveInfo.activityInfo.packageName; 371 } 372 if (resolveInfo.serviceInfo != null) { 373 return resolveInfo.serviceInfo.packageName; 374 } 375 if (resolveInfo.providerInfo != null) { 376 return resolveInfo.providerInfo.packageName; 377 } 378 // Unexpected case. 379 return null; 380 } 381 } 382 383 384 /** 385 * Returns whether app identified by {@code packageName} declares itself as a video app. 386 */ isVideoApp(PackageManager packageManager, String packageName)387 public static boolean isVideoApp(PackageManager packageManager, String packageName) { 388 return getAutomotiveAppTypes(packageManager, packageName).contains(TYPE_VIDEO); 389 } 390 391 /** 392 * Queries an app manifest and resources to determine the types of AAOS app it declares itself 393 * as. 394 * 395 * @param packageManager {@link PackageManager} to query. 396 * @param packageName App package. 397 * @return List of AAOS app-types from XML resources. 398 */ getAutomotiveAppTypes(PackageManager packageManager, String packageName)399 public static List<String> getAutomotiveAppTypes(PackageManager packageManager, 400 String packageName) { 401 ApplicationInfo appInfo; 402 Resources appResources; 403 try { 404 appInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA); 405 appResources = packageManager.getResourcesForApplication(appInfo); 406 } catch (PackageManager.NameNotFoundException e) { 407 Log.w(TAG, "Unexpected package not found for: " + packageName, e); 408 return new ArrayList<>(); 409 } 410 411 int resourceId = 412 appInfo.metaData != null 413 ? appInfo.metaData.getInt("com.android.automotive", -1) : -1; 414 if (resourceId == -1) { 415 return new ArrayList<>(); 416 } 417 try (XmlResourceParser parser = appResources.getXml(resourceId)) { 418 return parseAutomotiveAppTypes(parser); 419 } 420 } 421 422 @VisibleForTesting parseAutomotiveAppTypes(XmlPullParser parser)423 static List<String> parseAutomotiveAppTypes(XmlPullParser parser) { 424 try { 425 // This pattern for parsing can be seen in Javadocs for XmlPullParser. 426 List<String> appTypes = new ArrayList<>(); 427 ArrayDeque<String> tagStack = new ArrayDeque<>(); 428 int eventType = parser.getEventType(); 429 while (eventType != XmlPullParser.END_DOCUMENT) { 430 if (eventType == XmlPullParser.START_TAG) { 431 String tag = parser.getName(); 432 if (Log.isLoggable(TAG, Log.VERBOSE)) { 433 Log.v(TAG, "Start tag " + tag); 434 } 435 tagStack.addFirst(tag); 436 if (!validTagStack(tagStack)) { 437 Log.w(TAG, "Invalid XML; tagStack: " + tagStack); 438 return new ArrayList<>(); 439 } 440 if (TAG_USES.equals(tag)) { 441 String nameValue = 442 parser.getAttributeValue(/* namespace= */ null , ATTRIBUTE_NAME); 443 if (TextUtils.isEmpty(nameValue)) { 444 Log.w(TAG, "Invalid XML; uses tag with missing/empty name attribute"); 445 return new ArrayList<>(); 446 } 447 appTypes.add(nameValue); 448 if (appTypes.size() > MAX_APP_TYPES) { 449 Log.w(TAG, "Too many uses tags in automotiveApp tag"); 450 return new ArrayList<>(); 451 } 452 if (Log.isLoggable(TAG, Log.VERBOSE)) { 453 Log.v(TAG, "Found appType: " + nameValue); 454 } 455 } 456 } else if (eventType == XmlPullParser.END_TAG) { 457 if (Log.isLoggable(TAG, Log.VERBOSE)) { 458 Log.v(TAG, "End tag " + parser.getName()); 459 } 460 tagStack.removeFirst(); 461 } 462 eventType = parser.next(); 463 } 464 return appTypes; 465 } catch (XmlPullParserException | IOException e) { 466 Log.w(TAG, "Unexpected exception whiling parsing XML resource", e); 467 return new ArrayList<>(); 468 } 469 } 470 validTagStack(ArrayDeque<String> tagStack)471 private static boolean validTagStack(ArrayDeque<String> tagStack) { 472 // Expected to be called after a new tag is pushed on this stack. 473 // Ensures that XML is of form: 474 // <automotiveApp> 475 // <uses/> 476 // <uses/> 477 // .... 478 // </automotiveApp> 479 switch (tagStack.size()) { 480 case 1: 481 return TAG_AUTOMOTIVE_APP.equals(tagStack.peekFirst()); 482 case 2: 483 return TAG_USES.equals(tagStack.peekFirst()); 484 default: 485 return false; 486 } 487 } 488 489 getDisabledActivities( PackageManager packageManager, Set<String> enabledPackages)490 private static List<ResolveInfo> getDisabledActivities( 491 PackageManager packageManager, Set<String> enabledPackages) { 492 List<ResolveInfo> allActivities = packageManager.queryIntentActivities( 493 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), 494 PackageManager.GET_RESOLVED_FILTER 495 | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS); 496 497 List<ResolveInfo> disabledActivities = new ArrayList<>(); 498 for (int i = 0; i < allActivities.size(); ++i) { 499 ResolveInfo info = allActivities.get(i); 500 try { 501 if (!enabledPackages.contains(info.activityInfo.packageName) 502 && packageManager.getApplicationEnabledSetting( 503 info.activityInfo.packageName) 504 == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) { 505 disabledActivities.add(info); 506 } 507 } catch (RuntimeException e) { 508 if (e instanceof IllegalArgumentException) { 509 /* Don't throw exception when the package is missing, which happens when a 510 * package is being uninstalled and the internal datastructures are being 511 * updated. 512 */ 513 continue; 514 } 515 throw e; 516 } 517 } 518 return disabledActivities; 519 } 520 shouldAddToLaunchables(@onNull ComponentName componentName, @NonNull Set<String> appsToHide, @NonNull Set<String> customMediaComponents, @AppTypes int appTypesToShow, @AppTypes int componentAppType)521 private static boolean shouldAddToLaunchables(@NonNull ComponentName componentName, 522 @NonNull Set<String> appsToHide, 523 @NonNull Set<String> customMediaComponents, 524 @AppTypes int appTypesToShow, 525 @AppTypes int componentAppType) { 526 if (appsToHide.contains(componentName.getPackageName())) { 527 return false; 528 } 529 switch (componentAppType) { 530 // Process media services 531 case APP_TYPE_MEDIA_SERVICES: 532 // For a media service in customMediaComponents, if its application's launcher 533 // activity will be shown in the Launcher, don't show the service's icon in the 534 // Launcher. 535 if (customMediaComponents.contains(componentName.flattenToString()) 536 && (appTypesToShow & APP_TYPE_LAUNCHABLES) != 0) { 537 return false; 538 } 539 return true; 540 // Process activities 541 case APP_TYPE_LAUNCHABLES: 542 return true; 543 default: 544 Log.e(TAG, "Invalid componentAppType : " + componentAppType); 545 return false; 546 } 547 } 548 selectMediaSourceAndFinish(Context context, ComponentName componentName, CarMediaManager carMediaManager)549 private static void selectMediaSourceAndFinish(Context context, ComponentName componentName, 550 CarMediaManager carMediaManager) { 551 try { 552 carMediaManager.setMediaSource(componentName, CarMediaManager.MEDIA_SOURCE_MODE_BROWSE); 553 if (context instanceof Activity) { 554 ((Activity) context).finish(); 555 } 556 } catch (CarNotConnectedException e) { 557 Log.e(TAG, "Car not connected", e); 558 } 559 } 560 561 /** 562 * Gets if an activity is distraction optimized. 563 * 564 * @param carPackageManager The {@link CarPackageManager} system service 565 * @param packageName The package name of the app 566 * @param activityName The requested activity name 567 * @return true if the supplied activity is distraction optimized 568 */ isActivityDistractionOptimized( CarPackageManager carPackageManager, String packageName, String activityName)569 static boolean isActivityDistractionOptimized( 570 CarPackageManager carPackageManager, String packageName, String activityName) { 571 boolean isDistractionOptimized = false; 572 // try getting distraction optimization info 573 try { 574 if (carPackageManager != null) { 575 isDistractionOptimized = 576 carPackageManager.isActivityDistractionOptimized(packageName, activityName); 577 } 578 } catch (CarNotConnectedException e) { 579 Log.e(TAG, "Car not connected when getting DO info", e); 580 } 581 return isDistractionOptimized; 582 } 583 } 584