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