• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.Nullable;
22 import android.app.Activity;
23 import android.app.ActivityOptions;
24 import android.car.Car;
25 import android.car.CarNotConnectedException;
26 import android.car.content.pm.CarPackageManager;
27 import android.car.media.CarMediaManager;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
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.os.Process;
36 import android.service.media.MediaBrowserService;
37 import android.text.TextUtils;
38 import android.util.Log;
39 
40 import androidx.annotation.IntDef;
41 import androidx.annotation.NonNull;
42 
43 import java.lang.annotation.Retention;
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.Comparator;
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Set;
51 
52 /**
53  * Util class that contains helper method used by app launcher classes.
54  */
55 class AppLauncherUtils {
56     private static final String TAG = "AppLauncherUtils";
57 
58     @Retention(SOURCE)
59     @IntDef({APP_TYPE_LAUNCHABLES, APP_TYPE_MEDIA_SERVICES})
60     @interface AppTypes {}
61     static final int APP_TYPE_LAUNCHABLES = 1;
62     static final int APP_TYPE_MEDIA_SERVICES = 2;
63 
AppLauncherUtils()64     private AppLauncherUtils() {
65     }
66 
67     /**
68      * Comparator for {@link AppMetaData} that sorts the list
69      * by the "displayName" property in ascending order.
70      */
71     static final Comparator<AppMetaData> ALPHABETICAL_COMPARATOR = Comparator
72             .comparing(AppMetaData::getDisplayName, String::compareToIgnoreCase);
73 
74     /**
75      * Helper method that launches the app given the app's AppMetaData.
76      *
77      * @param app the requesting app's AppMetaData
78      */
launchApp(Context context, Intent intent)79     static void launchApp(Context context, Intent intent) {
80         ActivityOptions options = ActivityOptions.makeBasic();
81         options.setLaunchDisplayId(context.getDisplayId());
82         context.startActivity(intent, options.toBundle());
83     }
84 
85     /** Bundles application and services info. */
86     static class LauncherAppsInfo {
87         /*
88          * Map of all car launcher components' (including launcher activities and media services)
89          * metadata keyed by ComponentName.
90          */
91         private final Map<ComponentName, AppMetaData> mLaunchables;
92 
93         /** Map of all the media services keyed by ComponentName. */
94         private final Map<ComponentName, ResolveInfo> mMediaServices;
95 
LauncherAppsInfo(@onNull Map<ComponentName, AppMetaData> launchablesMap, @NonNull Map<ComponentName, ResolveInfo> mediaServices)96         LauncherAppsInfo(@NonNull Map<ComponentName, AppMetaData> launchablesMap,
97                 @NonNull Map<ComponentName, ResolveInfo> mediaServices) {
98             mLaunchables = launchablesMap;
99             mMediaServices = mediaServices;
100         }
101 
102         /** Returns true if all maps are empty. */
isEmpty()103         boolean isEmpty() {
104             return mLaunchables.isEmpty() && mMediaServices.isEmpty();
105         }
106 
107         /**
108          * Returns whether the given componentName is a media service.
109          */
isMediaService(ComponentName componentName)110         boolean isMediaService(ComponentName componentName) {
111             return mMediaServices.containsKey(componentName);
112         }
113 
114         /** Returns the {@link AppMetaData} for the given componentName. */
115         @Nullable
getAppMetaData(ComponentName componentName)116         AppMetaData getAppMetaData(ComponentName componentName) {
117             return mLaunchables.get(componentName);
118         }
119 
120         /** Returns a new list of all launchable components' {@link AppMetaData}. */
121         @NonNull
getLaunchableComponentsList()122         List<AppMetaData> getLaunchableComponentsList() {
123             return new ArrayList<>(mLaunchables.values());
124         }
125     }
126 
127     private final static LauncherAppsInfo EMPTY_APPS_INFO = new LauncherAppsInfo(
128             Collections.emptyMap(), Collections.emptyMap());
129 
130     /*
131      * Gets the media source in a given package. If there are multiple sources in the package,
132      * returns the first one.
133      */
getMediaSource(@onNull PackageManager packageManager, @NonNull String packageName)134     static ComponentName getMediaSource(@NonNull PackageManager packageManager,
135             @NonNull String packageName) {
136         Intent mediaIntent = new Intent();
137         mediaIntent.setPackage(packageName);
138         mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
139 
140         List<ResolveInfo> mediaServices = packageManager.queryIntentServices(mediaIntent,
141                 PackageManager.GET_RESOLVED_FILTER);
142 
143         if (mediaServices == null || mediaServices.isEmpty()) {
144             return null;
145         }
146         String defaultService = mediaServices.get(0).serviceInfo.name;
147         if (!TextUtils.isEmpty(defaultService)) {
148             return new ComponentName(packageName, defaultService);
149         }
150         return null;
151     }
152 
153     /**
154      * Gets all the components that we want to see in the launcher in unsorted order, including
155      * launcher activities and media services.
156      *
157      * @param appsToHide            A (possibly empty) list of apps (package names) to hide
158      * @param customMediaComponents A (possibly empty) list of media components (component names)
159      *                              that shouldn't be shown in Launcher because their applications'
160      *                              launcher activities will be shown
161      * @param appTypes              Types of apps to show (e.g.: all, or media sources only)
162      * @param openMediaCenter       Whether launcher should navigate to media center when the
163      *                              user selects a media source.
164      * @param launcherApps          The {@link LauncherApps} system service
165      * @param carPackageManager     The {@link CarPackageManager} system service
166      * @param packageManager        The {@link PackageManager} system service
167      * @return a new {@link LauncherAppsInfo}
168      */
169     @NonNull
getLauncherApps( @onNull Set<String> appsToHide, @NonNull Set<String> customMediaComponents, @AppTypes int appTypes, boolean openMediaCenter, LauncherApps launcherApps, CarPackageManager carPackageManager, PackageManager packageManager, CarMediaManager carMediaManager)170     static LauncherAppsInfo getLauncherApps(
171             @NonNull Set<String> appsToHide,
172             @NonNull Set<String> customMediaComponents,
173             @AppTypes int appTypes,
174             boolean openMediaCenter,
175             LauncherApps launcherApps,
176             CarPackageManager carPackageManager,
177             PackageManager packageManager,
178             CarMediaManager carMediaManager) {
179 
180         if (launcherApps == null || carPackageManager == null || packageManager == null
181                 || carMediaManager == null) {
182             return EMPTY_APPS_INFO;
183         }
184 
185         List<ResolveInfo> mediaServices = packageManager.queryIntentServices(
186                 new Intent(MediaBrowserService.SERVICE_INTERFACE),
187                 PackageManager.GET_RESOLVED_FILTER);
188         List<LauncherActivityInfo> availableActivities =
189                 launcherApps.getActivityList(null, Process.myUserHandle());
190 
191         Map<ComponentName, AppMetaData> launchablesMap = new HashMap<>(
192                 mediaServices.size() + availableActivities.size());
193         Map<ComponentName, ResolveInfo> mediaServicesMap = new HashMap<>(mediaServices.size());
194 
195         // Process media services
196         if ((appTypes & APP_TYPE_MEDIA_SERVICES) != 0) {
197             for (ResolveInfo info : mediaServices) {
198                 String packageName = info.serviceInfo.packageName;
199                 String className = info.serviceInfo.name;
200                 ComponentName componentName = new ComponentName(packageName, className);
201                 mediaServicesMap.put(componentName, info);
202                 if (shouldAddToLaunchables(componentName, appsToHide, customMediaComponents,
203                         appTypes, APP_TYPE_MEDIA_SERVICES)) {
204                     final boolean isDistractionOptimized = true;
205 
206                     Intent intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE);
207                     intent.putExtra(Car.CAR_EXTRA_MEDIA_COMPONENT, componentName.flattenToString());
208 
209                     AppMetaData appMetaData = new AppMetaData(
210                         info.serviceInfo.loadLabel(packageManager),
211                         componentName,
212                         info.serviceInfo.loadIcon(packageManager),
213                         isDistractionOptimized,
214                         context -> {
215                             if (openMediaCenter) {
216                                 AppLauncherUtils.launchApp(context, intent);
217                             } else {
218                                 selectMediaSourceAndFinish(context, componentName, carMediaManager);
219                             }
220                         },
221                         context -> {
222                             // getLaunchIntentForPackage looks for a main activity in the category
223                             // Intent.CATEGORY_INFO, then Intent.CATEGORY_LAUNCHER, and returns null
224                             // if neither are found
225                             Intent packageLaunchIntent =
226                                     packageManager.getLaunchIntentForPackage(packageName);
227                             AppLauncherUtils.launchApp(context,
228                                     packageLaunchIntent != null ? packageLaunchIntent : intent);
229                         });
230                     launchablesMap.put(componentName, appMetaData);
231                 }
232             }
233         }
234 
235         // Process activities
236         if ((appTypes & APP_TYPE_LAUNCHABLES) != 0) {
237             for (LauncherActivityInfo info : availableActivities) {
238                 ComponentName componentName = info.getComponentName();
239                 String packageName = componentName.getPackageName();
240                 if (shouldAddToLaunchables(componentName, appsToHide, customMediaComponents,
241                         appTypes, APP_TYPE_LAUNCHABLES)) {
242                     boolean isDistractionOptimized =
243                         isActivityDistractionOptimized(carPackageManager, packageName,
244                             info.getName());
245 
246                     Intent intent = new Intent(Intent.ACTION_MAIN)
247                         .setComponent(componentName)
248                         .addCategory(Intent.CATEGORY_LAUNCHER)
249                         .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
250 
251                     AppMetaData appMetaData = new AppMetaData(
252                         info.getLabel(),
253                         componentName,
254                         info.getBadgedIcon(0),
255                         isDistractionOptimized,
256                         context -> AppLauncherUtils.launchApp(context, intent),
257                         null);
258                     launchablesMap.put(componentName, appMetaData);
259                 }
260             }
261         }
262 
263         return new LauncherAppsInfo(launchablesMap, mediaServicesMap);
264     }
265 
shouldAddToLaunchables(@onNull ComponentName componentName, @NonNull Set<String> appsToHide, @NonNull Set<String> customMediaComponents, @AppTypes int appTypesToShow, @AppTypes int componentAppType)266     private static boolean shouldAddToLaunchables(@NonNull ComponentName componentName,
267             @NonNull Set<String> appsToHide,
268             @NonNull Set<String> customMediaComponents,
269             @AppTypes int appTypesToShow,
270             @AppTypes int componentAppType) {
271         if (appsToHide.contains(componentName.getPackageName())) {
272             return false;
273         }
274         switch (componentAppType) {
275             // Process media services
276             case APP_TYPE_MEDIA_SERVICES:
277                 // For a media service in customMediaComponents, if its application's launcher
278                 // activity will be shown in the Launcher, don't show the service's icon in the
279                 // Launcher.
280                 if (customMediaComponents.contains(componentName.flattenToString())
281                         && (appTypesToShow & APP_TYPE_LAUNCHABLES) != 0) {
282                     return false;
283                 }
284                 return true;
285             // Process activities
286             case APP_TYPE_LAUNCHABLES:
287                 return true;
288             default:
289                 Log.e(TAG, "Invalid componentAppType : " + componentAppType);
290                 return false;
291         }
292     }
293 
selectMediaSourceAndFinish(Context context, ComponentName componentName, CarMediaManager carMediaManager)294     private static void selectMediaSourceAndFinish(Context context, ComponentName componentName,
295             CarMediaManager carMediaManager) {
296         try {
297             carMediaManager.setMediaSource(componentName, CarMediaManager.MEDIA_SOURCE_MODE_BROWSE);
298             if (context instanceof Activity) {
299                 ((Activity) context).finish();
300             }
301         } catch (CarNotConnectedException e) {
302             Log.e(TAG, "Car not connected", e);
303         }
304     }
305 
306     /**
307      * Gets if an activity is distraction optimized.
308      *
309      * @param carPackageManager The {@link CarPackageManager} system service
310      * @param packageName       The package name of the app
311      * @param activityName      The requested activity name
312      * @return true if the supplied activity is distraction optimized
313      */
isActivityDistractionOptimized( CarPackageManager carPackageManager, String packageName, String activityName)314     static boolean isActivityDistractionOptimized(
315             CarPackageManager carPackageManager, String packageName, String activityName) {
316         boolean isDistractionOptimized = false;
317         // try getting distraction optimization info
318         try {
319             if (carPackageManager != null) {
320                 isDistractionOptimized =
321                         carPackageManager.isActivityDistractionOptimized(packageName, activityName);
322             }
323         } catch (CarNotConnectedException e) {
324             Log.e(TAG, "Car not connected when getting DO info", e);
325         }
326         return isDistractionOptimized;
327     }
328 }
329