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