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