1 /*
2  * Copyright (C) 2023 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 import static android.car.settings.CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS;
21 import static android.car.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED;
22 
23 import static java.lang.annotation.RetentionPolicy.SOURCE;
24 
25 import android.app.Activity;
26 import android.app.ActivityOptions;
27 import android.car.Car;
28 import android.car.CarNotConnectedException;
29 import android.car.content.pm.CarPackageManager;
30 import android.car.media.CarMediaManager;
31 import android.content.ComponentName;
32 import android.content.ContentResolver;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.pm.LauncherActivityInfo;
36 import android.content.pm.LauncherApps;
37 import android.content.pm.PackageManager;
38 import android.content.pm.ResolveInfo;
39 import android.os.Process;
40 import android.os.UserHandle;
41 import android.provider.Settings;
42 import android.service.media.MediaBrowserService;
43 import android.text.TextUtils;
44 import android.util.ArraySet;
45 import android.util.Log;
46 import android.util.Pair;
47 import android.view.View;
48 
49 import androidx.annotation.IntDef;
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 
53 import com.android.car.carlaunchercommon.shortcuts.AppInfoShortcutItem;
54 import com.android.car.carlaunchercommon.shortcuts.ForceStopShortcutItem;
55 import com.android.car.carlaunchercommon.shortcuts.PinShortcutItem;
56 import com.android.car.dockutil.Flags;
57 import com.android.car.dockutil.events.DockEventSenderHelper;
58 import com.android.car.media.common.source.MediaSource;
59 import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup;
60 
61 import com.google.common.collect.Sets;
62 
63 import java.lang.annotation.Retention;
64 import java.net.URISyntaxException;
65 import java.util.ArrayList;
66 import java.util.Arrays;
67 import java.util.Collections;
68 import java.util.Comparator;
69 import java.util.HashMap;
70 import java.util.List;
71 import java.util.Map;
72 import java.util.Objects;
73 import java.util.Set;
74 import java.util.function.Consumer;
75 import java.util.stream.Collectors;
76 
77 /**
78  * Util class that contains helper method used by app launcher classes.
79  */
80 public class AppLauncherUtils {
81     private static final String TAG = "AppLauncherUtils";
82     private static final String ANDROIDX_CAR_APP_LAUNCHABLE = "androidx.car.app.launchable";
83 
84     @Retention(SOURCE)
85     @IntDef({APP_TYPE_LAUNCHABLES, APP_TYPE_MEDIA_SERVICES})
86     @interface AppTypes {}
87 
88     static final int APP_TYPE_LAUNCHABLES = 1;
89     static final int APP_TYPE_MEDIA_SERVICES = 2;
90 
91     // This value indicates if TOS has not been accepted by the user
92     private static final String TOS_NOT_ACCEPTED = "1";
93     // This value indicates if TOS is in uninitialized state
94     private static final String TOS_UNINITIALIZED = "0";
95     static final String TOS_DISABLED_APPS_SEPARATOR = ",";
96     static final String PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR = ";";
97 
98     // Max no. of uses tags in automotiveApp XML. This is an arbitrary limit to be defensive
99     // to bad input.
100     private static final int MAX_APP_TYPES = 64;
101     private static final String PACKAGE_URI_PREFIX = "package:";
102 
AppLauncherUtils()103     private AppLauncherUtils() {
104     }
105 
106     /**
107      * Comparator for {@link AppMetaData} that sorts the list
108      * by the "displayName" property in ascending order.
109      */
110     static final Comparator<AppMetaData> ALPHABETICAL_COMPARATOR = Comparator
111             .comparing(AppMetaData::getDisplayName, String::compareToIgnoreCase);
112 
113     /**
114      * Helper method that launches the app given the app's AppMetaData.
115      */
launchApp(Context context, Intent intent)116     public static void launchApp(Context context, Intent intent) {
117         ActivityOptions options = ActivityOptions.makeBasic();
118         options.setLaunchDisplayId(context.getDisplay().getDisplayId());
119         context.startActivity(intent, options.toBundle());
120     }
121 
122     /** Bundles application and services info. */
123     static class LauncherAppsInfo {
124         /*
125          * Map of all car launcher components' (including launcher activities and media services)
126          * metadata keyed by ComponentName.
127          */
128         private final Map<ComponentName, AppMetaData> mLaunchables;
129 
130         /** Map of all the media services keyed by ComponentName. */
131         private final Map<ComponentName, ResolveInfo> mMediaServices;
132 
LauncherAppsInfo(@onNull Map<ComponentName, AppMetaData> launchablesMap, @NonNull Map<ComponentName, ResolveInfo> mediaServices)133         LauncherAppsInfo(@NonNull Map<ComponentName, AppMetaData> launchablesMap,
134                 @NonNull Map<ComponentName, ResolveInfo> mediaServices) {
135             mLaunchables = launchablesMap;
136             mMediaServices = mediaServices;
137         }
138 
139         /** Returns true if all maps are empty. */
isEmpty()140         boolean isEmpty() {
141             return mLaunchables.isEmpty() && mMediaServices.isEmpty();
142         }
143 
144         /**
145          * Returns whether the given componentName is a media service.
146          */
isMediaService(ComponentName componentName)147         boolean isMediaService(ComponentName componentName) {
148             return mMediaServices.containsKey(componentName);
149         }
150 
151         /** Returns the {@link AppMetaData} for the given componentName. */
152         @Nullable
getAppMetaData(ComponentName componentName)153         AppMetaData getAppMetaData(ComponentName componentName) {
154             return mLaunchables.get(componentName);
155         }
156 
157         /** Returns a new list of all launchable components' {@link AppMetaData}. */
158         @NonNull
getLaunchableComponentsList()159         List<AppMetaData> getLaunchableComponentsList() {
160             return new ArrayList<>(mLaunchables.values());
161         }
162 
163         /** Returns list of Media Services for the launcher **/
164         @NonNull
getMediaServices()165         Map<ComponentName, ResolveInfo> getMediaServices() {
166             return mMediaServices;
167         }
168     }
169 
170     private static final LauncherAppsInfo EMPTY_APPS_INFO = new LauncherAppsInfo(
171             Collections.emptyMap(), Collections.emptyMap());
172 
173     /**
174      * Gets the media source in a given package. If there are multiple sources in the package,
175      * returns the first one.
176      */
getMediaSource(@onNull PackageManager packageManager, @NonNull String packageName)177     static ComponentName getMediaSource(@NonNull PackageManager packageManager,
178             @NonNull String packageName) {
179         Intent mediaIntent = new Intent();
180         mediaIntent.setPackage(packageName);
181         mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
182 
183         List<ResolveInfo> mediaServices = packageManager.queryIntentServices(mediaIntent,
184                 PackageManager.GET_RESOLVED_FILTER);
185 
186         if (mediaServices == null || mediaServices.isEmpty()) {
187             return null;
188         }
189         String defaultService = mediaServices.get(0).serviceInfo.name;
190         if (!TextUtils.isEmpty(defaultService)) {
191             return new ComponentName(packageName, defaultService);
192         }
193         return null;
194     }
195 
196     /**
197      * Gets all the components that we want to see in the launcher in unsorted order, including
198      * launcher activities and media services.
199      *
200      * @param appsToHide            A (possibly empty) list of apps (package names) to hide
201      * @param appTypes              Types of apps to show (e.g.: all, or media sources only)
202      * @param openMediaCenter       Whether launcher should navigate to media center when the
203      *                              user selects a media source.
204      * @param launcherApps          The {@link LauncherApps} system service
205      * @param carPackageManager     The {@link CarPackageManager} system service
206      * @param packageManager        The {@link PackageManager} system service
207      *                              of such apps are always excluded.
208      * @param carMediaManager       The {@link CarMediaManager} system service
209      * @return a new {@link LauncherAppsInfo}
210      */
211     @NonNull
getLauncherApps( Context context, @NonNull Set<String> appsToHide, @AppTypes int appTypes, boolean openMediaCenter, LauncherApps launcherApps, CarPackageManager carPackageManager, PackageManager packageManager, CarMediaManager carMediaManager, ShortcutsListener shortcutsListener, String mirroringAppPkgName, Intent mirroringAppRedirect)212     static LauncherAppsInfo getLauncherApps(
213             Context context,
214             @NonNull Set<String> appsToHide,
215             @AppTypes int appTypes,
216             boolean openMediaCenter,
217             LauncherApps launcherApps,
218             CarPackageManager carPackageManager,
219             PackageManager packageManager,
220             CarMediaManager carMediaManager,
221             ShortcutsListener shortcutsListener,
222             String mirroringAppPkgName,
223             Intent mirroringAppRedirect) {
224 
225         if (launcherApps == null || carPackageManager == null || packageManager == null
226                 || carMediaManager == null) {
227             return EMPTY_APPS_INFO;
228         }
229 
230         // Using new list since we require a mutable list to do removeIf.
231         List<ResolveInfo> mediaServices = new ArrayList<>();
232         mediaServices.addAll(
233                 packageManager.queryIntentServices(
234                         new Intent(MediaBrowserService.SERVICE_INTERFACE),
235                         PackageManager.GET_RESOLVED_FILTER));
236 
237         List<LauncherActivityInfo> availableActivities =
238                 launcherApps.getActivityList(null, Process.myUserHandle());
239 
240         int launchablesSize = mediaServices.size() + availableActivities.size();
241         Map<ComponentName, AppMetaData> launchablesMap = new HashMap<>(launchablesSize);
242         Map<ComponentName, ResolveInfo> mediaServicesMap = new HashMap<>(mediaServices.size());
243         Set<String> mEnabledPackages = new ArraySet<>(launchablesSize);
244         Set<String> tosDisabledPackages = getTosDisabledPackages(context);
245         Set<ComponentName> mediaServiceComponents = mediaServices.stream()
246                 .map(resolveInfo -> new ComponentName(resolveInfo.serviceInfo.packageName,
247                         resolveInfo.serviceInfo.name))
248                 .collect(Collectors.toSet());
249 
250         Set<String> customMediaComponents = Sets.newHashSet(
251                 context.getResources().getStringArray(
252                         com.android.car.media.common.R.array.custom_media_packages));
253 
254         // Process media services
255         if ((appTypes & APP_TYPE_MEDIA_SERVICES) != 0) {
256             for (ResolveInfo info : mediaServices) {
257                 String packageName = info.serviceInfo.packageName;
258                 String className = info.serviceInfo.name;
259                 ComponentName componentName = new ComponentName(packageName, className);
260                 mediaServicesMap.put(componentName, info);
261                 mEnabledPackages.add(packageName);
262                 if (shouldAddToLaunchables(context, componentName, appsToHide,
263                         customMediaComponents, appTypes, APP_TYPE_MEDIA_SERVICES)) {
264                     CharSequence displayName = info.serviceInfo.loadLabel(packageManager);
265                     AppMetaData appMetaData = new AppMetaData(
266                             displayName,
267                             componentName,
268                             info.serviceInfo.loadIcon(packageManager),
269                             /* isDistractionOptimized= */ true,
270                             /* isMirroring = */ false,
271                             /* isDisabledByTos= */ tosDisabledPackages.contains(packageName),
272                             contextArg -> {
273                                 if (openMediaCenter) {
274                                     AppLauncherUtils.launchApp(contextArg,
275                                             createMediaLaunchIntent(componentName));
276                                 } else {
277                                     selectMediaSourceAndFinish(contextArg, componentName,
278                                             carMediaManager);
279                                 }
280                             },
281                             buildShortcuts(componentName, displayName, shortcutsListener,
282                                     carMediaManager, mediaServiceComponents));
283                     launchablesMap.put(componentName, appMetaData);
284                 }
285             }
286         }
287 
288         // Process activities
289         if ((appTypes & APP_TYPE_LAUNCHABLES) != 0) {
290             for (LauncherActivityInfo info : availableActivities) {
291                 ComponentName componentName = info.getComponentName();
292                 mEnabledPackages.add(componentName.getPackageName());
293                 if (shouldAddToLaunchables(context, componentName, appsToHide,
294                         customMediaComponents, appTypes, APP_TYPE_LAUNCHABLES)) {
295                     boolean isDistractionOptimized =
296                             isActivityDistractionOptimized(carPackageManager,
297                                     componentName.getPackageName(), info.getName());
298                     boolean isDisabledByTos = tosDisabledPackages
299                             .contains(componentName.getPackageName());
300 
301                     CharSequence displayName = info.getLabel();
302                     boolean isMirroring = componentName.getPackageName()
303                             .equals(mirroringAppPkgName);
304                     AppMetaData appMetaData = new AppMetaData(
305                             displayName,
306                             componentName,
307                             info.getBadgedIcon(0),
308                             isDistractionOptimized,
309                             isMirroring,
310                             isDisabledByTos,
311                             contextArg -> {
312                                 if (componentName.getPackageName().equals(mirroringAppPkgName)) {
313                                     Log.d(TAG, "non-media service package name "
314                                             + "equals mirroring pkg name");
315                                 }
316                                 AppLauncherUtils.launchApp(contextArg,
317                                         isMirroring ? mirroringAppRedirect :
318                                                 createAppLaunchIntent(componentName));
319                             },
320                             buildShortcuts(componentName, displayName, shortcutsListener,
321                                     carMediaManager, mediaServiceComponents));
322                     launchablesMap.put(componentName, appMetaData);
323                 }
324             }
325 
326             List<ResolveInfo> disabledActivities = getDisabledActivities(context, packageManager,
327                     mEnabledPackages);
328             for (ResolveInfo info : disabledActivities) {
329                 String packageName = info.activityInfo.packageName;
330                 String className = info.activityInfo.name;
331                 ComponentName componentName = new ComponentName(packageName, className);
332                 if (!shouldAddToLaunchables(context, componentName, appsToHide,
333                         customMediaComponents, appTypes, APP_TYPE_LAUNCHABLES)) {
334                     continue;
335                 }
336                 boolean isDistractionOptimized =
337                         isActivityDistractionOptimized(carPackageManager, packageName, className);
338                 boolean isDisabledByTos = tosDisabledPackages.contains(packageName);
339 
340                 CharSequence displayName = info.activityInfo.loadLabel(packageManager);
341                 AppMetaData appMetaData = new AppMetaData(
342                         displayName,
343                         componentName,
344                         info.activityInfo.loadIcon(packageManager),
345                         isDistractionOptimized,
346                         /* isMirroring = */ false,
347                         isDisabledByTos,
348                         contextArg -> {
349                             packageManager.setApplicationEnabledSetting(packageName,
350                                     PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
351                             // Fetch the current enabled setting to make sure the setting is synced
352                             // before launching the activity. Otherwise, the activity may not
353                             // launch.
354                             if (packageManager.getApplicationEnabledSetting(packageName)
355                                     != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
356                                 throw new IllegalStateException(
357                                         "Failed to enable the disabled package [" + packageName
358                                                 + "]");
359                             }
360                             Log.i(TAG, "Successfully enabled package [" + packageName + "]");
361                             AppLauncherUtils.launchApp(contextArg,
362                                     createAppLaunchIntent(componentName));
363                         },
364                         buildShortcuts(componentName, displayName, shortcutsListener,
365                                 carMediaManager, mediaServiceComponents));
366                 launchablesMap.put(componentName, appMetaData);
367             }
368 
369             List<ResolveInfo> restrictedActivities = getTosDisabledActivities(
370                     context,
371                     packageManager,
372                     mEnabledPackages
373             );
374             for (ResolveInfo info: restrictedActivities) {
375                 String packageName = info.activityInfo.packageName;
376                 String className = info.activityInfo.name;
377                 ComponentName componentName = new ComponentName(packageName, className);
378 
379                 boolean isDistractionOptimized =
380                         isActivityDistractionOptimized(carPackageManager, packageName, className);
381                 boolean isDisabledByTos = tosDisabledPackages.contains(packageName);
382 
383                 AppMetaData appMetaData = new AppMetaData(
384                         info.activityInfo.loadLabel(packageManager),
385                         componentName,
386                         info.activityInfo.loadIcon(packageManager),
387                         isDistractionOptimized,
388                         /* isMirroring = */ false,
389                         isDisabledByTos,
390                         contextArg -> {
391                             Intent tosIntent = getIntentForTosAcceptanceFlow(contextArg);
392                             launchApp(contextArg, tosIntent);
393                         },
394                         null
395                 );
396                 launchablesMap.put(componentName, appMetaData);
397             }
398         }
399 
400         return new LauncherAppsInfo(launchablesMap, mediaServicesMap);
401     }
402 
403     /**
404      * Gets the intent for launching the TOS acceptance flow
405      *
406      * @param context The app context
407      * @return TOS intent, or null
408      */
409     @Nullable
getIntentForTosAcceptanceFlow(Context context)410     public static Intent getIntentForTosAcceptanceFlow(Context context) {
411         String tosIntentName =
412                 context.getResources().getString(R.string.user_tos_activity_intent);
413         try {
414             return Intent.parseUri(tosIntentName, Intent.URI_ANDROID_APP_SCHEME);
415         } catch (URISyntaxException se) {
416             Log.e(TAG, "Invalid intent URI in user_tos_activity_intent", se);
417             return null;
418         }
419     }
420 
buildShortcuts( ComponentName componentName, CharSequence displayName, ShortcutsListener shortcutsListener, CarMediaManager carMediaManager, Set<ComponentName> mediaServiceComponents)421     private static Consumer<Pair<Context, View>> buildShortcuts(
422             ComponentName componentName, CharSequence displayName,
423             ShortcutsListener shortcutsListener, CarMediaManager carMediaManager,
424             Set<ComponentName> mediaServiceComponents) {
425         return pair -> {
426             CarUiShortcutsPopup.Builder carUiShortcutsPopupBuilder =
427                     new CarUiShortcutsPopup.Builder()
428                             .addShortcut(new ForceStopShortcutItem(
429                                     pair.first,
430                                     componentName.getPackageName(),
431                                     displayName,
432                                     carMediaManager,
433                                     mediaServiceComponents
434                             ))
435                             .addShortcut(new AppInfoShortcutItem(pair.first,
436                                     componentName.getPackageName(),
437                                     UserHandle.getUserHandleForUid(Process.myUid())));
438             if (Flags.dockFeature()) {
439                 carUiShortcutsPopupBuilder
440                         .addShortcut(buildPinToDockShortcut(componentName, pair.first));
441             }
442             CarUiShortcutsPopup carUiShortcutsPopup = carUiShortcutsPopupBuilder
443                     .build(pair.first, pair.second);
444 
445             carUiShortcutsPopup.show();
446             shortcutsListener.onShortcutsShow(carUiShortcutsPopup);
447         };
448     }
449 
buildPinToDockShortcut( ComponentName componentName, Context context)450     private static CarUiShortcutsPopup.ShortcutItem buildPinToDockShortcut(
451             ComponentName componentName, Context context) {
452         DockEventSenderHelper mHelper = new DockEventSenderHelper(context);
453         return new PinShortcutItem(context.getResources(), /* isItemPinned= */ false,
454                 /* pinItemClickDelegate= */ () -> mHelper.sendPinEvent(componentName),
455                 /* unpinItemClickDelegate= */ () -> mHelper.sendUnpinEvent(componentName)
456         );
457     }
458 
getDisabledActivities(Context context, PackageManager packageManager, Set<String> enabledPackages)459     private static List<ResolveInfo> getDisabledActivities(Context context,
460             PackageManager packageManager, Set<String> enabledPackages) {
461         return getActivitiesFromSystemPreferences(
462                 context,
463                 packageManager,
464                 enabledPackages,
465                 KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE,
466                 PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS,
467                 PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR);
468     }
469 
getTosDisabledActivities( Context context, PackageManager packageManager, Set<String> enabledPackages)470     private static List<ResolveInfo> getTosDisabledActivities(
471             Context context,
472             PackageManager packageManager,
473             Set<String> enabledPackages) {
474         return getActivitiesFromSystemPreferences(
475                 context,
476                 packageManager,
477                 enabledPackages,
478                 KEY_UNACCEPTED_TOS_DISABLED_APPS,
479                 PackageManager.MATCH_DISABLED_COMPONENTS,
480                 TOS_DISABLED_APPS_SEPARATOR);
481     }
482 
483     /**
484      * Get a list of activities from packages in system preferences by key
485      * @param context the app context
486      * @param packageManager The PackageManager
487      * @param enabledPackages Set of packages enabled by system
488      * @param settingsKey Key to read from system preferences
489      * @param sep Separator
490      *
491      * @return List of activities read from system preferences
492      */
getActivitiesFromSystemPreferences( Context context, PackageManager packageManager, Set<String> enabledPackages, String settingsKey, int filter, String sep)493     private static List<ResolveInfo> getActivitiesFromSystemPreferences(
494             Context context,
495             PackageManager packageManager,
496             Set<String> enabledPackages,
497             String settingsKey,
498             int filter,
499             String sep) {
500         ContentResolver contentResolverForUser = context.createContextAsUser(
501                         UserHandle.getUserHandleForUid(Process.myUid()), /* flags= */ 0)
502                 .getContentResolver();
503         String settingsValue = Settings.Secure.getString(contentResolverForUser, settingsKey);
504         Set<String> packages = TextUtils.isEmpty(settingsValue) ? new ArraySet<>()
505                 : new ArraySet<>(Arrays.asList(settingsValue.split(
506                         sep)));
507 
508         if (packages.isEmpty()) {
509             return Collections.emptyList();
510         }
511 
512         List<ResolveInfo> allActivities = packageManager.queryIntentActivities(
513                 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER),
514                 PackageManager.ResolveInfoFlags.of(PackageManager.GET_RESOLVED_FILTER
515                         | filter));
516 
517         List<ResolveInfo> activities = new ArrayList<>();
518         for (int i = 0; i < allActivities.size(); ++i) {
519             ResolveInfo info = allActivities.get(i);
520             if (!enabledPackages.contains(info.activityInfo.packageName)
521                     && packages.contains(info.activityInfo.packageName)) {
522                 activities.add(info);
523             }
524         }
525         return activities;
526     }
527 
shouldAddToLaunchables(Context context, @NonNull ComponentName componentName, @NonNull Set<String> appsToHide, @NonNull Set<String> customMediaComponents, @AppTypes int appTypesToShow, @AppTypes int componentAppType)528     private static boolean shouldAddToLaunchables(Context context,
529             @NonNull ComponentName componentName,
530             @NonNull Set<String> appsToHide,
531             @NonNull Set<String> customMediaComponents,
532             @AppTypes int appTypesToShow,
533             @AppTypes int componentAppType) {
534         if (appsToHide.contains(componentName.getPackageName())) {
535             return false;
536         }
537         switch (componentAppType) {
538             // Process media services
539             case APP_TYPE_MEDIA_SERVICES:
540                 // For a media service in customMediaComponents, if its application's launcher
541                 // activity will be shown in the Launcher, don't show the service's icon in the
542                 // Launcher.
543                 if (customMediaComponents.contains(componentName.flattenToString())) {
544                     if ((appTypesToShow & APP_TYPE_LAUNCHABLES) != 0) {
545                         if (Log.isLoggable(TAG, Log.DEBUG)) {
546                             Log.d(TAG, "MBS for custom media app " + componentName
547                                     + " is skipped in app launcher");
548                         }
549                         return false;
550                     }
551                     // Media switcher use case should still show
552                     if (Log.isLoggable(TAG, Log.DEBUG)) {
553                         Log.d(TAG, "MBS for custom media app " + componentName
554                                 + " is included in media switcher");
555                     }
556                     return true;
557                 }
558                 // Only Keep MBS that is a media template
559                 return MediaSource.isMediaTemplate(context, componentName);
560             // Process activities
561             case APP_TYPE_LAUNCHABLES:
562                 return true;
563             default:
564                 Log.e(TAG, "Invalid componentAppType : " + componentAppType);
565                 return false;
566         }
567     }
568 
selectMediaSourceAndFinish(Context context, ComponentName componentName, CarMediaManager carMediaManager)569     private static void selectMediaSourceAndFinish(Context context, ComponentName componentName,
570             CarMediaManager carMediaManager) {
571         try {
572             carMediaManager.setMediaSource(componentName, CarMediaManager.MEDIA_SOURCE_MODE_BROWSE);
573             if (context instanceof Activity) {
574                 ((Activity) context).finish();
575             }
576         } catch (CarNotConnectedException e) {
577             Log.e(TAG, "Car not connected", e);
578         }
579     }
580 
581     /**
582      * Gets if an activity is distraction optimized.
583      *
584      * @param carPackageManager The {@link CarPackageManager} system service
585      * @param packageName       The package name of the app
586      * @param activityName      The requested activity name
587      * @return true if the supplied activity is distraction optimized
588      */
isActivityDistractionOptimized( CarPackageManager carPackageManager, String packageName, String activityName)589     static boolean isActivityDistractionOptimized(
590             CarPackageManager carPackageManager, String packageName, String activityName) {
591         boolean isDistractionOptimized = false;
592         // try getting distraction optimization info
593         try {
594             if (carPackageManager != null) {
595                 isDistractionOptimized =
596                         carPackageManager.isActivityDistractionOptimized(packageName, activityName);
597             }
598         } catch (CarNotConnectedException e) {
599             Log.e(TAG, "Car not connected when getting DO info", e);
600         }
601         return isDistractionOptimized;
602     }
603 
604     /**
605      * Callback when a ShortcutsPopup View is shown
606      */
607     protected interface ShortcutsListener {
608 
onShortcutsShow(CarUiShortcutsPopup carUiShortcutsPopup)609         void onShortcutsShow(CarUiShortcutsPopup carUiShortcutsPopup);
610     }
611 
612     /**
613      * Returns a set of packages that are disabled by tos
614      *
615      * @param context The application context
616      * @return Set of packages disabled by tos
617      */
getTosDisabledPackages(Context context)618     public static Set<String> getTosDisabledPackages(Context context) {
619         ContentResolver contentResolverForUser = context.createContextAsUser(
620                         UserHandle.getUserHandleForUid(Process.myUid()), /* flags= */ 0)
621                 .getContentResolver();
622         String settingsValue = Settings.Secure.getString(contentResolverForUser,
623                 KEY_UNACCEPTED_TOS_DISABLED_APPS);
624         return TextUtils.isEmpty(settingsValue) ? new ArraySet<>()
625                 : new ArraySet<>(Arrays.asList(settingsValue.split(
626                         TOS_DISABLED_APPS_SEPARATOR)));
627     }
628 
createMediaLaunchIntent(ComponentName componentName)629     private static Intent createMediaLaunchIntent(ComponentName componentName) {
630         return new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE)
631                 .putExtra(Car.CAR_EXTRA_MEDIA_COMPONENT, componentName.flattenToString());
632     }
633 
createAppLaunchIntent(ComponentName componentName)634     private static Intent createAppLaunchIntent(ComponentName componentName) {
635         return new Intent(Intent.ACTION_MAIN)
636                 .setComponent(componentName)
637                 .addCategory(Intent.CATEGORY_LAUNCHER)
638                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
639     }
640 
641     /**
642      * Check if a user has accepted TOS
643      *
644      * @param context The application context
645      * @return true if the user has accepted Tos, false otherwise
646      */
tosAccepted(Context context)647     public static boolean tosAccepted(Context context) {
648         ContentResolver contentResolverForUser = context.createContextAsUser(
649                         UserHandle.getUserHandleForUid(Process.myUid()), /* flags= */ 0)
650                 .getContentResolver();
651         String settingsValue = Settings.Secure.getString(
652                 contentResolverForUser,
653                 KEY_USER_TOS_ACCEPTED);
654         return !Objects.equals(settingsValue, TOS_NOT_ACCEPTED);
655     }
656 
657     /**
658      * Check if TOS status is uninitialized
659      *
660      * @param context The application context
661      *
662      * @return true if tos is uninitialized, false otherwise
663      */
tosStatusUninitialized(Context context)664     static boolean tosStatusUninitialized(Context context) {
665         ContentResolver contentResolverForUser = context.createContextAsUser(
666                         UserHandle.getUserHandleForUid(Process.myUid()), /* flags= */ 0)
667                 .getContentResolver();
668         String settingsValue = Settings.Secure.getString(
669                 contentResolverForUser,
670                 KEY_USER_TOS_ACCEPTED);
671         return Objects.equals(settingsValue, TOS_UNINITIALIZED);
672     }
673 }
674