1 /* 2 * Copyright (C) 2023 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.portraitlauncher.homeactivities; 18 19 import android.annotation.MainThread; 20 import android.app.ActivityManager; 21 import android.app.TaskInfo; 22 import android.car.media.CarMediaIntents; 23 import android.content.BroadcastReceiver; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.pm.PackageManager; 29 import android.content.pm.ResolveInfo; 30 import android.net.Uri; 31 import android.os.Build; 32 import android.text.TextUtils; 33 import android.util.ArraySet; 34 import android.util.Log; 35 36 import androidx.car.app.CarContext; 37 38 import com.android.car.carlauncher.AppGridActivity; 39 import com.android.car.carlauncher.CarLauncherUtils; 40 import com.android.car.portraitlauncher.R; 41 42 import java.util.ArrayList; 43 import java.util.HashSet; 44 import java.util.List; 45 import java.util.Set; 46 47 /** 48 * Manages the task categories for {@link CarUiPortraitHomeScreen}, check which category a task 49 * belongs to. 50 */ 51 class TaskCategoryManager { 52 public static final String TAG = TaskCategoryManager.class.getSimpleName(); 53 private static final boolean DBG = Build.IS_DEBUGGABLE; 54 /** Stub geo data to help query navigation intent. */ 55 private static final String STUB_GEO_DATA = "geo:0.0,0,0"; 56 57 private final ComponentName mAppGridActivityComponent; 58 private final ComponentName mNotificationActivityComponent; 59 private final ComponentName mRecentsActivityComponent; 60 private final ComponentName mCalmModeComponent; 61 private final ArraySet<ComponentName> mIgnoreOpeningRootTaskViewComponentsSet; 62 private final Set<ComponentName> mFullScreenActivities; 63 private final Set<ComponentName> mBackgroundActivities; 64 private final Context mContext; 65 private final ApplicationInstallUninstallReceiver mApplicationInstallUninstallReceiver; 66 private final Set<OnApplicationInstallUninstallListener> 67 mOnApplicationInstallUninstallListeners; 68 private ComponentName mCurrentBackgroundApp; 69 TaskCategoryManager(Context context)70 TaskCategoryManager(Context context) { 71 mContext = context; 72 73 mFullScreenActivities = new HashSet<>(); 74 mBackgroundActivities = new HashSet<>(); 75 mIgnoreOpeningRootTaskViewComponentsSet = convertToComponentNames(mContext.getResources() 76 .getStringArray(R.array.config_ignoreOpeningForegroundDA)); 77 mAppGridActivityComponent = new ComponentName(context, AppGridActivity.class); 78 mNotificationActivityComponent = ComponentName.unflattenFromString( 79 mContext.getResources().getString(R.string.config_notificationActivity)); 80 mRecentsActivityComponent = ComponentName.unflattenFromString(mContext.getResources() 81 .getString(com.android.internal.R.string.config_recentsComponentName)); 82 mCalmModeComponent = ComponentName.unflattenFromString(mContext.getResources() 83 .getString(R.string.config_calmMode_componentName)); 84 mOnApplicationInstallUninstallListeners = new HashSet<>(); 85 86 updateFullScreenActivities(); 87 updateBackgroundActivityMap(); 88 89 mApplicationInstallUninstallReceiver = registerApplicationInstallUninstallReceiver( 90 mContext); 91 } 92 93 /** 94 * Refresh {@code mFullScreenActivities} and {@code mBackgroundActivities}. 95 */ refresh()96 void refresh() { 97 updateFullScreenActivities(); 98 updateBackgroundActivityMap(); 99 } 100 isHomeIntent(TaskInfo taskInfo)101 static boolean isHomeIntent(TaskInfo taskInfo) { 102 return taskInfo.baseIntent != null 103 && taskInfo.baseIntent.getCategories() != null 104 && taskInfo.baseIntent.getCategories().contains(Intent.CATEGORY_HOME); 105 } 106 logIfDebuggable(String message)107 private static void logIfDebuggable(String message) { 108 if (DBG) { 109 Log.d(TAG, message); 110 } 111 } 112 113 /** 114 * @return {@code true} if current task in panel was launched using media intent. 115 */ isMediaApp(TaskInfo taskInfo)116 public static boolean isMediaApp(TaskInfo taskInfo) { 117 return taskInfo != null && taskInfo.baseIntent != null 118 && CarMediaIntents.ACTION_MEDIA_TEMPLATE.equals(taskInfo.baseIntent.getAction()); 119 } 120 convertToComponentNames(String[] componentStrings)121 private static ArraySet<ComponentName> convertToComponentNames(String[] componentStrings) { 122 ArraySet<ComponentName> componentNames = new ArraySet<>(componentStrings.length); 123 for (int i = componentStrings.length - 1; i >= 0; i--) { 124 componentNames.add(ComponentName.unflattenFromString(componentStrings[i])); 125 } 126 return componentNames; 127 } 128 updateFullScreenActivities()129 void updateFullScreenActivities() { 130 mFullScreenActivities.clear(); 131 Intent voiceIntent = new Intent(Intent.ACTION_VOICE_ASSIST, /* uri= */ null); 132 List<ResolveInfo> result = mContext.getPackageManager().queryIntentActivitiesAsUser( 133 voiceIntent, PackageManager.MATCH_ALL, ActivityManager.getCurrentUser()); 134 135 for (ResolveInfo info : result) { 136 if (info == null || info.activityInfo == null 137 || info.activityInfo.getComponentName() == null) { 138 continue; 139 } 140 if (mFullScreenActivities.add(info.activityInfo.getComponentName())) { 141 logIfDebuggable("adding the following component to show on fullscreen: " 142 + info.activityInfo.getComponentName()); 143 } 144 } 145 146 mFullScreenActivities.addAll(convertToComponentNames(mContext.getResources() 147 .getStringArray(R.array.config_fullScreenActivities))); 148 } 149 updateBackgroundActivityMap()150 void updateBackgroundActivityMap() { 151 mBackgroundActivities.clear(); 152 Intent intent = new Intent(CarContext.ACTION_NAVIGATE, Uri.parse(STUB_GEO_DATA)); 153 List<ResolveInfo> result = mContext.getPackageManager().queryIntentActivitiesAsUser( 154 intent, PackageManager.MATCH_ALL, ActivityManager.getCurrentUser()); 155 156 for (ResolveInfo info : result) { 157 if (info == null || info.activityInfo == null 158 || info.activityInfo.getComponentName() == null) { 159 continue; 160 } 161 mBackgroundActivities.add(info.getComponentInfo().getComponentName()); 162 } 163 164 mBackgroundActivities.addAll(convertToComponentNames(mContext.getResources() 165 .getStringArray(R.array.config_backgroundActivities))); 166 mBackgroundActivities.add(mCalmModeComponent); 167 } 168 169 /** 170 * Returns whether the {@code TaskCategoryManager} is ready. If maps intent can be resolved, 171 * the {@code TaskCategoryManager} is ready. 172 */ isReady()173 public boolean isReady() { 174 Intent intent = CarLauncherUtils.getMapsIntent(mContext); 175 return intent.resolveActivity(mContext.getPackageManager()) != null; 176 } 177 registerOnApplicationInstallUninstallListener( OnApplicationInstallUninstallListener onApplicationInstallUninstallListener)178 void registerOnApplicationInstallUninstallListener( 179 OnApplicationInstallUninstallListener onApplicationInstallUninstallListener) { 180 mOnApplicationInstallUninstallListeners.add(onApplicationInstallUninstallListener); 181 } 182 unregisterOnApplicationInstallUninstallListener( OnApplicationInstallUninstallListener onApplicationInstallUninstallListener)183 void unregisterOnApplicationInstallUninstallListener( 184 OnApplicationInstallUninstallListener onApplicationInstallUninstallListener) { 185 mOnApplicationInstallUninstallListeners.remove(onApplicationInstallUninstallListener); 186 } 187 isBackgroundApp(TaskInfo taskInfo)188 boolean isBackgroundApp(TaskInfo taskInfo) { 189 return mBackgroundActivities.contains(taskInfo.baseActivity); 190 } 191 isBackgroundApp(ComponentName componentName)192 boolean isBackgroundApp(ComponentName componentName) { 193 return mBackgroundActivities.contains(componentName); 194 } 195 isCurrentBackgroundApp(ComponentName componentName)196 boolean isCurrentBackgroundApp(ComponentName componentName) { 197 return mCurrentBackgroundApp != null && mCurrentBackgroundApp.equals(componentName); 198 } 199 getCurrentBackgroundApp()200 ComponentName getCurrentBackgroundApp() { 201 return mCurrentBackgroundApp; 202 } 203 setCurrentBackgroundApp(ComponentName componentName)204 void setCurrentBackgroundApp(ComponentName componentName) { 205 mCurrentBackgroundApp = componentName; 206 } 207 isAppGridActivity(ComponentName componentName)208 boolean isAppGridActivity(ComponentName componentName) { 209 return mAppGridActivityComponent.equals(componentName); 210 } 211 isAppGridActivity(TaskInfo taskInfo)212 boolean isAppGridActivity(TaskInfo taskInfo) { 213 return mAppGridActivityComponent.equals(getVisibleActivity(taskInfo)); 214 } 215 getVisibleActivity(TaskInfo taskInfo)216 private ComponentName getVisibleActivity(TaskInfo taskInfo) { 217 if (taskInfo == null) { 218 return null; 219 } 220 if (taskInfo.topActivity != null) { 221 return taskInfo.topActivity; 222 } else if (taskInfo.baseActivity != null) { 223 return taskInfo.baseActivity; 224 } else { 225 return taskInfo.baseIntent.getComponent(); 226 } 227 } 228 getAppGridActivity()229 ComponentName getAppGridActivity() { 230 return mAppGridActivityComponent; 231 } 232 getFullScreenActivitiesList()233 List<ComponentName> getFullScreenActivitiesList() { 234 return mFullScreenActivities.stream().toList(); 235 } 236 getBackgroundActivitiesList()237 List<ComponentName> getBackgroundActivitiesList() { 238 return mBackgroundActivities.stream().toList(); 239 } 240 isFullScreenActivity(TaskInfo taskInfo)241 boolean isFullScreenActivity(TaskInfo taskInfo) { 242 return mFullScreenActivities.contains(taskInfo.baseActivity); 243 } 244 isNotificationActivity(TaskInfo taskInfo)245 boolean isNotificationActivity(TaskInfo taskInfo) { 246 return mNotificationActivityComponent.equals(getVisibleActivity(taskInfo)); 247 } 248 isRecentsActivity(TaskInfo taskInfo)249 boolean isRecentsActivity(TaskInfo taskInfo) { 250 return mRecentsActivityComponent.equals(taskInfo.baseActivity); 251 } 252 isCalmModeActivity(TaskInfo taskInfo)253 boolean isCalmModeActivity(TaskInfo taskInfo) { 254 return mCalmModeComponent.equals(taskInfo.baseActivity); 255 } 256 shouldIgnoreForApplicationPanel(TaskInfo taskInfo)257 boolean shouldIgnoreForApplicationPanel(TaskInfo taskInfo) { 258 return mIgnoreOpeningRootTaskViewComponentsSet.contains( 259 taskInfo.baseIntent.getComponent()); 260 } 261 onDestroy()262 public void onDestroy() { 263 mOnApplicationInstallUninstallListeners.clear(); 264 mContext.unregisterReceiver(mApplicationInstallUninstallReceiver); 265 } 266 267 /** 268 * Returns a list of activities that are tracked as background activities with given 269 * {@code packageName}. 270 */ getBackgroundActivitiesFromPackage(String packageName)271 List<ComponentName> getBackgroundActivitiesFromPackage(String packageName) { 272 List<ComponentName> list = new ArrayList<>(); 273 for (ComponentName componentName : mBackgroundActivities) { 274 if (componentName.getPackageName().equals(packageName)) { 275 list.add(componentName); 276 } 277 } 278 return list; 279 } 280 registerApplicationInstallUninstallReceiver( Context context)281 private ApplicationInstallUninstallReceiver registerApplicationInstallUninstallReceiver( 282 Context context) { 283 ApplicationInstallUninstallReceiver 284 installUninstallReceiver = new ApplicationInstallUninstallReceiver(); 285 IntentFilter filter = new IntentFilter(); 286 filter.addAction(Intent.ACTION_PACKAGE_ADDED); 287 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 288 filter.addDataScheme("package"); 289 context.registerReceiver(installUninstallReceiver, filter); 290 return installUninstallReceiver; 291 } 292 293 /** 294 * Listener for application installation and uninstallation. 295 */ 296 interface OnApplicationInstallUninstallListener { 297 /** 298 * Invoked when intent with {@link Intent.ACTION_PACKAGE_ADDED) is received. 299 */ onAppInstalled(String packageName)300 void onAppInstalled(String packageName); 301 /** 302 * Invoked when intent with {@link Intent.ACTION_PACKAGE_REMOVED}} is received. 303 */ onAppUninstall(String packageName)304 void onAppUninstall(String packageName); 305 } 306 307 private class ApplicationInstallUninstallReceiver extends BroadcastReceiver { 308 @MainThread 309 @Override onReceive(Context context, Intent intent)310 public void onReceive(Context context, Intent intent) { 311 String packageName = intent.getData().getSchemeSpecificPart(); 312 String action = intent.getAction(); 313 if (TextUtils.isEmpty(packageName) && TextUtils.isEmpty(action)) { 314 logIfDebuggable( 315 "Invalid intent with packageName=" + packageName + ", action=" + action); 316 // Ignoring empty announcements 317 return; 318 } 319 320 refresh(); 321 322 if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { 323 for (OnApplicationInstallUninstallListener listener : 324 mOnApplicationInstallUninstallListeners) { 325 listener.onAppInstalled(packageName); 326 } 327 } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { 328 for (OnApplicationInstallUninstallListener listener : 329 mOnApplicationInstallUninstallListeners) { 330 listener.onAppUninstall(packageName); 331 } 332 } else { 333 logIfDebuggable("Skip action " + action + " for package" + packageName); 334 } 335 } 336 } 337 } 338