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