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 android.car.settings.CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE; 20 21 import static java.lang.annotation.RetentionPolicy.SOURCE; 22 23 import android.app.Activity; 24 import android.app.ActivityOptions; 25 import android.car.Car; 26 import android.car.CarNotConnectedException; 27 import android.car.content.pm.CarPackageManager; 28 import android.car.media.CarMediaManager; 29 import android.content.ComponentName; 30 import android.content.ContentResolver; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.pm.ApplicationInfo; 34 import android.content.pm.LauncherActivityInfo; 35 import android.content.pm.LauncherApps; 36 import android.content.pm.PackageManager; 37 import android.content.pm.ResolveInfo; 38 import android.content.res.Resources; 39 import android.content.res.XmlResourceParser; 40 import android.os.Process; 41 import android.os.UserHandle; 42 import android.provider.Settings; 43 import android.service.media.MediaBrowserService; 44 import android.text.TextUtils; 45 import android.util.ArraySet; 46 import android.util.Log; 47 48 import androidx.annotation.IntDef; 49 import androidx.annotation.NonNull; 50 import androidx.annotation.Nullable; 51 import androidx.annotation.VisibleForTesting; 52 53 import org.xmlpull.v1.XmlPullParser; 54 import org.xmlpull.v1.XmlPullParserException; 55 56 import java.io.IOException; 57 import java.lang.annotation.Retention; 58 import java.util.ArrayDeque; 59 import java.util.ArrayList; 60 import java.util.Arrays; 61 import java.util.Collections; 62 import java.util.Comparator; 63 import java.util.HashMap; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Set; 67 import java.util.function.Predicate; 68 69 /** 70 * Util class that contains helper method used by app launcher classes. 71 */ 72 public class AppLauncherUtils { 73 private static final String TAG = "AppLauncherUtils"; 74 75 @Retention(SOURCE) 76 @IntDef({APP_TYPE_LAUNCHABLES, APP_TYPE_MEDIA_SERVICES}) 77 @interface AppTypes {} 78 static final int APP_TYPE_LAUNCHABLES = 1; 79 static final int APP_TYPE_MEDIA_SERVICES = 2; 80 81 private static final String TAG_AUTOMOTIVE_APP = "automotiveApp"; 82 private static final String TAG_USES = "uses"; 83 private static final String ATTRIBUTE_NAME = "name"; 84 private static final String TYPE_VIDEO = "video"; 85 static final String PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR = ";"; 86 87 // Max no. of uses tags in automotiveApp XML. This is an arbitrary limit to be defensive 88 // to bad input. 89 private static final int MAX_APP_TYPES = 64; 90 AppLauncherUtils()91 private AppLauncherUtils() { 92 } 93 94 /** 95 * Comparator for {@link AppMetaData} that sorts the list 96 * by the "displayName" property in ascending order. 97 */ 98 static final Comparator<AppMetaData> ALPHABETICAL_COMPARATOR = Comparator 99 .comparing(AppMetaData::getDisplayName, String::compareToIgnoreCase); 100 101 /** 102 * Helper method that launches the app given the app's AppMetaData. 103 * 104 * @param app the requesting app's AppMetaData 105 */ launchApp(Context context, Intent intent)106 static void launchApp(Context context, Intent intent) { 107 ActivityOptions options = ActivityOptions.makeBasic(); 108 options.setLaunchDisplayId(context.getDisplayId()); 109 context.startActivity(intent, options.toBundle()); 110 } 111 112 /** Bundles application and services info. */ 113 static class LauncherAppsInfo { 114 /* 115 * Map of all car launcher components' (including launcher activities and media services) 116 * metadata keyed by ComponentName. 117 */ 118 private final Map<ComponentName, AppMetaData> mLaunchables; 119 120 /** Map of all the media services keyed by ComponentName. */ 121 private final Map<ComponentName, ResolveInfo> mMediaServices; 122 LauncherAppsInfo(@onNull Map<ComponentName, AppMetaData> launchablesMap, @NonNull Map<ComponentName, ResolveInfo> mediaServices)123 LauncherAppsInfo(@NonNull Map<ComponentName, AppMetaData> launchablesMap, 124 @NonNull Map<ComponentName, ResolveInfo> mediaServices) { 125 mLaunchables = launchablesMap; 126 mMediaServices = mediaServices; 127 } 128 129 /** Returns true if all maps are empty. */ isEmpty()130 boolean isEmpty() { 131 return mLaunchables.isEmpty() && mMediaServices.isEmpty(); 132 } 133 134 /** 135 * Returns whether the given componentName is a media service. 136 */ isMediaService(ComponentName componentName)137 boolean isMediaService(ComponentName componentName) { 138 return mMediaServices.containsKey(componentName); 139 } 140 141 /** Returns the {@link AppMetaData} for the given componentName. */ 142 @Nullable getAppMetaData(ComponentName componentName)143 AppMetaData getAppMetaData(ComponentName componentName) { 144 return mLaunchables.get(componentName); 145 } 146 147 /** Returns a new list of all launchable components' {@link AppMetaData}. */ 148 @NonNull getLaunchableComponentsList()149 List<AppMetaData> getLaunchableComponentsList() { 150 return new ArrayList<>(mLaunchables.values()); 151 } 152 } 153 154 private final static LauncherAppsInfo EMPTY_APPS_INFO = new LauncherAppsInfo( 155 Collections.emptyMap(), Collections.emptyMap()); 156 157 /* 158 * Gets the media source in a given package. If there are multiple sources in the package, 159 * returns the first one. 160 */ getMediaSource(@onNull PackageManager packageManager, @NonNull String packageName)161 static ComponentName getMediaSource(@NonNull PackageManager packageManager, 162 @NonNull String packageName) { 163 Intent mediaIntent = new Intent(); 164 mediaIntent.setPackage(packageName); 165 mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE); 166 167 List<ResolveInfo> mediaServices = packageManager.queryIntentServices(mediaIntent, 168 PackageManager.GET_RESOLVED_FILTER); 169 170 if (mediaServices == null || mediaServices.isEmpty()) { 171 return null; 172 } 173 String defaultService = mediaServices.get(0).serviceInfo.name; 174 if (!TextUtils.isEmpty(defaultService)) { 175 return new ComponentName(packageName, defaultService); 176 } 177 return null; 178 } 179 180 /** 181 * Gets all the components that we want to see in the launcher in unsorted order, including 182 * launcher activities and media services. 183 * 184 * @param appsToHide A (possibly empty) list of apps (package names) to hide 185 * @param customMediaComponents A (possibly empty) list of media components (component names) 186 * that shouldn't be shown in Launcher because their applications' 187 * launcher activities will be shown 188 * @param appTypes Types of apps to show (e.g.: all, or media sources only) 189 * @param openMediaCenter Whether launcher should navigate to media center when the 190 * user selects a media source. 191 * @param launcherApps The {@link LauncherApps} system service 192 * @param carPackageManager The {@link CarPackageManager} system service 193 * @param packageManager The {@link PackageManager} system service 194 * @param videoAppPredicate Predicate that checks if a given {@link ResolveInfo} resolves 195 * to a video app. See {@link #VideoAppPredicate}. Media-services 196 * of such apps are always excluded. 197 * @param carMediaManager The {@link CarMediaManager} system service 198 * @return a new {@link LauncherAppsInfo} 199 */ 200 @NonNull getLauncherApps( Context context, @NonNull Set<String> appsToHide, @NonNull Set<String> customMediaComponents, @AppTypes int appTypes, boolean openMediaCenter, LauncherApps launcherApps, CarPackageManager carPackageManager, PackageManager packageManager, @NonNull Predicate<ResolveInfo> videoAppPredicate, CarMediaManager carMediaManager)201 static LauncherAppsInfo getLauncherApps( 202 Context context, 203 @NonNull Set<String> appsToHide, 204 @NonNull Set<String> customMediaComponents, 205 @AppTypes int appTypes, 206 boolean openMediaCenter, 207 LauncherApps launcherApps, 208 CarPackageManager carPackageManager, 209 PackageManager packageManager, 210 @NonNull Predicate<ResolveInfo> videoAppPredicate, 211 CarMediaManager carMediaManager) { 212 213 if (launcherApps == null || carPackageManager == null || packageManager == null 214 || carMediaManager == null) { 215 return EMPTY_APPS_INFO; 216 } 217 218 // Using new list since we require a mutable list to do removeIf. 219 List<ResolveInfo> mediaServices = new ArrayList<>(); 220 mediaServices.addAll( 221 packageManager.queryIntentServices( 222 new Intent(MediaBrowserService.SERVICE_INTERFACE), 223 PackageManager.GET_RESOLVED_FILTER)); 224 // Exclude Media Services from Video apps from being considered. These apps should offer a 225 // normal Launcher Activity as an entry point. 226 mediaServices.removeIf(videoAppPredicate); 227 228 List<LauncherActivityInfo> availableActivities = 229 launcherApps.getActivityList(null, Process.myUserHandle()); 230 231 int launchablesSize = mediaServices.size() + availableActivities.size(); 232 Map<ComponentName, AppMetaData> launchablesMap = new HashMap<>(launchablesSize); 233 Map<ComponentName, ResolveInfo> mediaServicesMap = new HashMap<>(mediaServices.size()); 234 Set<String> mEnabledPackages = new ArraySet<>(launchablesSize); 235 236 // Process media services 237 if ((appTypes & APP_TYPE_MEDIA_SERVICES) != 0) { 238 for (ResolveInfo info : mediaServices) { 239 String packageName = info.serviceInfo.packageName; 240 String className = info.serviceInfo.name; 241 ComponentName componentName = new ComponentName(packageName, className); 242 mediaServicesMap.put(componentName, info); 243 mEnabledPackages.add(packageName); 244 if (shouldAddToLaunchables(componentName, appsToHide, customMediaComponents, 245 appTypes, APP_TYPE_MEDIA_SERVICES)) { 246 final boolean isDistractionOptimized = true; 247 248 Intent intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE); 249 intent.putExtra(Car.CAR_EXTRA_MEDIA_COMPONENT, componentName.flattenToString()); 250 251 AppMetaData appMetaData = new AppMetaData( 252 info.serviceInfo.loadLabel(packageManager), 253 componentName, 254 info.serviceInfo.loadIcon(packageManager), 255 isDistractionOptimized, 256 contextArg -> { 257 if (openMediaCenter) { 258 AppLauncherUtils.launchApp(contextArg, intent); 259 } else { 260 selectMediaSourceAndFinish(contextArg, componentName, 261 carMediaManager); 262 } 263 }, 264 /* alternateLaunchCallback */ null); 265 launchablesMap.put(componentName, appMetaData); 266 } 267 } 268 } 269 270 // Process activities 271 if ((appTypes & APP_TYPE_LAUNCHABLES) != 0) { 272 for (LauncherActivityInfo info : availableActivities) { 273 ComponentName componentName = info.getComponentName(); 274 String packageName = componentName.getPackageName(); 275 mEnabledPackages.add(packageName); 276 if (shouldAddToLaunchables(componentName, appsToHide, customMediaComponents, 277 appTypes, APP_TYPE_LAUNCHABLES)) { 278 boolean isDistractionOptimized = 279 isActivityDistractionOptimized(carPackageManager, packageName, 280 info.getName()); 281 282 Intent intent = new Intent(Intent.ACTION_MAIN) 283 .setComponent(componentName) 284 .addCategory(Intent.CATEGORY_LAUNCHER) 285 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 286 287 AppMetaData appMetaData = new AppMetaData( 288 info.getLabel(), 289 componentName, 290 info.getBadgedIcon(0), 291 isDistractionOptimized, 292 contextArg -> AppLauncherUtils.launchApp(contextArg, intent), 293 /* alternateLaunchCallback */ null); 294 launchablesMap.put(componentName, appMetaData); 295 } 296 } 297 298 List<ResolveInfo> disabledActivities = getDisabledActivities(context, packageManager, 299 mEnabledPackages); 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 contextArg -> { 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(contextArg, intent); 336 }, 337 /* alternateLaunchCallback */ 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 getDisabledActivities(Context context, PackageManager packageManager, Set<String> enabledPackages)489 private static List<ResolveInfo> getDisabledActivities(Context context, 490 PackageManager packageManager, Set<String> enabledPackages) { 491 ContentResolver contentResolverForUser = context.createContextAsUser( 492 UserHandle.getUserHandleForUid(Process.myUid()), /* flags= */ 0) 493 .getContentResolver(); 494 String settingsValue = Settings.Secure.getString(contentResolverForUser, 495 KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE); 496 Set<String> disabledPackages = TextUtils.isEmpty(settingsValue) ? new ArraySet<>() 497 : new ArraySet<>(Arrays.asList(settingsValue.split( 498 PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR))); 499 if (disabledPackages.isEmpty()) { 500 return Collections.emptyList(); 501 } 502 503 List<ResolveInfo> allActivities = packageManager.queryIntentActivities( 504 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), 505 PackageManager.ResolveInfoFlags.of(PackageManager.GET_RESOLVED_FILTER 506 | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS)); 507 508 List<ResolveInfo> disabledActivities = new ArrayList<>(); 509 for (int i = 0; i < allActivities.size(); ++i) { 510 ResolveInfo info = allActivities.get(i); 511 if (!enabledPackages.contains(info.activityInfo.packageName) 512 && disabledPackages.contains(info.activityInfo.packageName)) { 513 disabledActivities.add(info); 514 } 515 } 516 return disabledActivities; 517 } 518 shouldAddToLaunchables(@onNull ComponentName componentName, @NonNull Set<String> appsToHide, @NonNull Set<String> customMediaComponents, @AppTypes int appTypesToShow, @AppTypes int componentAppType)519 private static boolean shouldAddToLaunchables(@NonNull ComponentName componentName, 520 @NonNull Set<String> appsToHide, 521 @NonNull Set<String> customMediaComponents, 522 @AppTypes int appTypesToShow, 523 @AppTypes int componentAppType) { 524 if (appsToHide.contains(componentName.getPackageName())) { 525 return false; 526 } 527 switch (componentAppType) { 528 // Process media services 529 case APP_TYPE_MEDIA_SERVICES: 530 // For a media service in customMediaComponents, if its application's launcher 531 // activity will be shown in the Launcher, don't show the service's icon in the 532 // Launcher. 533 if (customMediaComponents.contains(componentName.flattenToString()) 534 && (appTypesToShow & APP_TYPE_LAUNCHABLES) != 0) { 535 return false; 536 } 537 return true; 538 // Process activities 539 case APP_TYPE_LAUNCHABLES: 540 return true; 541 default: 542 Log.e(TAG, "Invalid componentAppType : " + componentAppType); 543 return false; 544 } 545 } 546 selectMediaSourceAndFinish(Context context, ComponentName componentName, CarMediaManager carMediaManager)547 private static void selectMediaSourceAndFinish(Context context, ComponentName componentName, 548 CarMediaManager carMediaManager) { 549 try { 550 carMediaManager.setMediaSource(componentName, CarMediaManager.MEDIA_SOURCE_MODE_BROWSE); 551 if (context instanceof Activity) { 552 ((Activity) context).finish(); 553 } 554 } catch (CarNotConnectedException e) { 555 Log.e(TAG, "Car not connected", e); 556 } 557 } 558 559 /** 560 * Gets if an activity is distraction optimized. 561 * 562 * @param carPackageManager The {@link CarPackageManager} system service 563 * @param packageName The package name of the app 564 * @param activityName The requested activity name 565 * @return true if the supplied activity is distraction optimized 566 */ isActivityDistractionOptimized( CarPackageManager carPackageManager, String packageName, String activityName)567 static boolean isActivityDistractionOptimized( 568 CarPackageManager carPackageManager, String packageName, String activityName) { 569 boolean isDistractionOptimized = false; 570 // try getting distraction optimization info 571 try { 572 if (carPackageManager != null) { 573 isDistractionOptimized = 574 carPackageManager.isActivityDistractionOptimized(packageName, activityName); 575 } 576 } catch (CarNotConnectedException e) { 577 Log.e(TAG, "Car not connected when getting DO info", e); 578 } 579 return isDistractionOptimized; 580 } 581 } 582