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, CarMediaManager carMediaManager)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             CarMediaManager carMediaManager) {
182 
183         if (launcherApps == null || carPackageManager == null || packageManager == null
184                 || carMediaManager == null) {
185             return EMPTY_APPS_INFO;
186         }
187 
188         List<ResolveInfo> mediaServices = packageManager.queryIntentServices(
189                 new Intent(MediaBrowserService.SERVICE_INTERFACE),
190                 PackageManager.GET_RESOLVED_FILTER);
191         List<LauncherActivityInfo> availableActivities =
192                 launcherApps.getActivityList(null, Process.myUserHandle());
193 
194         Map<ComponentName, AppMetaData> launchablesMap = new HashMap<>(
195                 mediaServices.size() + availableActivities.size());
196         Map<ComponentName, ResolveInfo> mediaServicesMap = new HashMap<>(mediaServices.size());
197 
198         // Process media services
199         if ((appTypes & APP_TYPE_MEDIA_SERVICES) != 0) {
200             for (ResolveInfo info : mediaServices) {
201                 String packageName = info.serviceInfo.packageName;
202                 String className = info.serviceInfo.name;
203                 ComponentName componentName = new ComponentName(packageName, className);
204                 mediaServicesMap.put(componentName, info);
205                 if (shouldAddToLaunchables(componentName, blackList, customMediaComponents,
206                         appTypes, APP_TYPE_MEDIA_SERVICES)) {
207                     final boolean isDistractionOptimized = true;
208 
209                     Intent intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE);
210                     intent.putExtra(Car.CAR_EXTRA_MEDIA_COMPONENT, componentName.flattenToString());
211 
212                     AppMetaData appMetaData = new AppMetaData(
213                         info.serviceInfo.loadLabel(packageManager),
214                         componentName,
215                         info.serviceInfo.loadIcon(packageManager),
216                         isDistractionOptimized,
217                         context -> {
218                             if (openMediaCenter) {
219                                 AppLauncherUtils.launchApp(context, intent);
220                             } else {
221                                 selectMediaSourceAndFinish(context, componentName, carMediaManager);
222                             }
223                         },
224                         context -> AppLauncherUtils.launchApp(context,
225                             packageManager.getLaunchIntentForPackage(packageName)));
226                     launchablesMap.put(componentName, appMetaData);
227                 }
228             }
229         }
230 
231         // Process activities
232         if ((appTypes & APP_TYPE_LAUNCHABLES) != 0) {
233             for (LauncherActivityInfo info : availableActivities) {
234                 ComponentName componentName = info.getComponentName();
235                 String packageName = componentName.getPackageName();
236                 if (shouldAddToLaunchables(componentName, blackList, customMediaComponents,
237                         appTypes, APP_TYPE_LAUNCHABLES)) {
238                     boolean isDistractionOptimized =
239                         isActivityDistractionOptimized(carPackageManager, packageName,
240                             info.getName());
241 
242                     Intent intent = new Intent(Intent.ACTION_MAIN)
243                         .setComponent(componentName)
244                         .addCategory(Intent.CATEGORY_LAUNCHER)
245                         .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
246 
247                     AppMetaData appMetaData = new AppMetaData(
248                         info.getLabel(),
249                         componentName,
250                         info.getBadgedIcon(0),
251                         isDistractionOptimized,
252                         context -> AppLauncherUtils.launchApp(context, intent),
253                         null);
254                     launchablesMap.put(componentName, appMetaData);
255                 }
256             }
257         }
258 
259         return new LauncherAppsInfo(launchablesMap, mediaServicesMap);
260     }
261 
shouldAddToLaunchables(@onNull ComponentName componentName, @NonNull Set<String> blackList, @NonNull Set<String> customMediaComponents, @AppTypes int appTypesToShow, @AppTypes int componentAppType)262     private static boolean shouldAddToLaunchables(@NonNull ComponentName componentName,
263             @NonNull Set<String> blackList,
264             @NonNull Set<String> customMediaComponents,
265             @AppTypes int appTypesToShow,
266             @AppTypes int componentAppType) {
267         if (blackList.contains(componentName.getPackageName())) {
268             return false;
269         }
270         switch (componentAppType) {
271             // Process media services
272             case APP_TYPE_MEDIA_SERVICES:
273                 // For a media service in customMediaComponents, if its application's launcher
274                 // activity will be shown in the Launcher, don't show the service's icon in the
275                 // Launcher.
276                 if (customMediaComponents.contains(componentName.flattenToString())
277                         && (appTypesToShow & APP_TYPE_LAUNCHABLES) != 0) {
278                     return false;
279                 }
280                 return true;
281             // Process activities
282             case APP_TYPE_LAUNCHABLES:
283                 return true;
284             default:
285                 Log.e(TAG, "Invalid componentAppType : " + componentAppType);
286                 return false;
287         }
288     }
289 
selectMediaSourceAndFinish(Context context, ComponentName componentName, CarMediaManager carMediaManager)290     private static void selectMediaSourceAndFinish(Context context, ComponentName componentName,
291             CarMediaManager carMediaManager) {
292         try {
293             carMediaManager.setMediaSource(componentName, CarMediaManager.MEDIA_SOURCE_MODE_BROWSE);
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