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