1 /*
2  * Copyright (C) 2020 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.systemui.car.systembar;
18 
19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
21 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
22 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
23 
24 import android.app.ActivityTaskManager;
25 import android.app.ActivityTaskManager.RootTaskInfo;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.os.RemoteException;
32 import android.util.Log;
33 import android.view.View;
34 import android.view.ViewGroup;
35 
36 import com.android.systemui.dagger.SysUISingleton;
37 
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Set;
42 
43 /**
44  * CarSystemBarButtons can optionally have selection state that toggles certain visual indications
45  * based on whether the active application on screen is associated with it. This is basically a
46  * similar concept to a radio button group.
47  *
48  * This class controls the selection state of CarSystemBarButtons that have opted in to have such
49  * selection state-dependent visual indications.
50  */
51 @SysUISingleton
52 public class ButtonSelectionStateController {
53     private static final String TAG = ButtonSelectionStateController.class.getSimpleName();
54 
55     private final Set<CarSystemBarButton> mRegisteredViews = new HashSet<>();
56 
57     protected final Context mContext;
58     protected ButtonMap mButtonsByCategory = new ButtonMap();
59     protected ButtonMap mButtonsByPackage = new ButtonMap();
60     protected ButtonMap mButtonsByComponentName = new ButtonMap();
61     protected HashSet<CarSystemBarButton> mSelectedButtons;
62 
ButtonSelectionStateController(Context context)63     public ButtonSelectionStateController(Context context) {
64         mContext = context;
65         mSelectedButtons = new HashSet<>();
66     }
67 
68     /**
69      * Iterate through a view looking for CarSystemBarButton and add it to the controller if it
70      * opted in to be highlighted when the active application is associated with it.
71      *
72      * @param v the View that may contain CarFacetButtons
73      */
addAllButtonsWithSelectionState(View v)74     protected void addAllButtonsWithSelectionState(View v) {
75         if (v instanceof CarSystemBarButton) {
76             if (((CarSystemBarButton) v).hasSelectionState()) {
77                 addButtonWithSelectionState((CarSystemBarButton) v);
78             }
79         } else if (v instanceof ViewGroup) {
80             ViewGroup viewGroup = (ViewGroup) v;
81             for (int i = 0; i < viewGroup.getChildCount(); i++) {
82                 addAllButtonsWithSelectionState(viewGroup.getChildAt(i));
83             }
84         }
85     }
86 
87     /** Removes all buttons from the button maps. */
removeAll()88     protected void removeAll() {
89         mButtonsByCategory.clear();
90         mButtonsByPackage.clear();
91         mButtonsByComponentName.clear();
92         mSelectedButtons.clear();
93         mRegisteredViews.clear();
94     }
95 
96     /**
97      * This will unselect the currently selected CarSystemBarButton and determine which one should
98      * be selected next. It does this by reading the properties on the CarSystemBarButton and
99      * seeing if they are a match with the supplied StackInfo list.
100      * The order of selection detection is ComponentName, PackageName then Category
101      * They will then be compared with the supplied StackInfo list.
102      * The StackInfo is expected to be supplied in order of recency and StackInfo will only be used
103      * for consideration if it has the same displayId as the CarSystemBarButton.
104      *
105      * @param taskInfoList of the currently running application
106      * @param validDisplay index of the valid display
107      */
108 
taskChanged(List<RootTaskInfo> taskInfoList, int validDisplay)109     protected void taskChanged(List<RootTaskInfo> taskInfoList, int validDisplay) {
110         RootTaskInfo validTaskInfo = null;
111 
112         for (RootTaskInfo taskInfo : taskInfoList) {
113             // Find the first stack info with a topActivity in the primary display.
114             // TODO: We assume that CarFacetButton will launch an app only in the primary display.
115             // We need to extend the functionality to handle the multiple display properly.
116             if (taskInfo.topActivity != null && taskInfo.displayAreaFeatureId == validDisplay) {
117                 validTaskInfo = taskInfo;
118                 break;
119             }
120         }
121 
122         if (validTaskInfo == null) {
123             // No stack was found that was on the same display as the buttons thus return
124             return;
125         }
126         int displayId = validTaskInfo.displayId;
127 
128         // Clear all registered views
129         clearAllSelectedButtons(displayId);
130 
131         HashSet<CarSystemBarButton> selectedButtons = findSelectedButtons(validTaskInfo);
132 
133         if (selectedButtons != null) {
134             selectedButtons.forEach(carSystemBarButton -> {
135                 if (carSystemBarButton.getDisplayId() == displayId) {
136                     carSystemBarButton.setSelected(true);
137                     mSelectedButtons.add(carSystemBarButton);
138                 }
139             });
140         }
141     }
142 
clearAllSelectedButtons(int displayId)143     protected void clearAllSelectedButtons(int displayId) {
144         mRegisteredViews.forEach(carSystemBarButton -> {
145             if (carSystemBarButton.getDisplayId() == displayId) {
146                 carSystemBarButton.setSelected(false);
147             }
148         });
149         mSelectedButtons.clear();
150     }
151 
152     /**
153      * Defaults to Display.DEFAULT_DISPLAY when no parameter is provided for the validDisplay.
154      *
155      * @param taskInfoList of the currently running application
156      */
taskChanged(List<RootTaskInfo> taskInfoList)157     protected void taskChanged(List<RootTaskInfo> taskInfoList) {
158         taskChanged(taskInfoList, FEATURE_DEFAULT_TASK_CONTAINER);
159     }
160 
161     /**
162      * Add navigation button to this controller if it uses selection state.
163      */
addButtonWithSelectionState(CarSystemBarButton carSystemBarButton)164     private void addButtonWithSelectionState(CarSystemBarButton carSystemBarButton) {
165         if (mRegisteredViews.contains(carSystemBarButton)) {
166             return;
167         }
168         String[] categories = carSystemBarButton.getCategories();
169         for (int i = 0; i < categories.length; i++) {
170             mButtonsByCategory.add(categories[i], carSystemBarButton);
171         }
172 
173         String[] packages = carSystemBarButton.getPackages();
174         for (int i = 0; i < packages.length; i++) {
175             mButtonsByPackage.add(packages[i], carSystemBarButton);
176         }
177         String[] componentNames = carSystemBarButton.getComponentName();
178         for (int i = 0; i < componentNames.length; i++) {
179             mButtonsByComponentName.add(componentNames[i], carSystemBarButton);
180         }
181 
182         mRegisteredViews.add(carSystemBarButton);
183     }
184 
findSelectedButtons(RootTaskInfo validTaskInfo)185     private HashSet<CarSystemBarButton> findSelectedButtons(RootTaskInfo validTaskInfo) {
186         ComponentName topActivity = getTopActivity(validTaskInfo);
187         if (topActivity == null) return null;
188 
189         String packageName = topActivity.getPackageName();
190 
191         HashSet<CarSystemBarButton> selectedButtons =
192                 findButtonsByComponentName(topActivity);
193         if (selectedButtons == null) {
194             selectedButtons = mButtonsByPackage.get(packageName);
195         }
196         if (selectedButtons == null) {
197             String category = getPackageCategory(packageName);
198             if (category != null) {
199                 selectedButtons = mButtonsByCategory.get(category);
200             }
201         }
202 
203         return selectedButtons;
204     }
205 
getTopActivity(RootTaskInfo validTaskInfo)206     protected ComponentName getTopActivity(RootTaskInfo validTaskInfo) {
207         // Window mode being WINDOW_MODE_MULTI_WINDOW implies TaskView might be visible on the
208         // display. In such cases, topActivity reported by validTaskInfo will be the one hosted in
209         // TaskView and not necessarily the main activity visible on display. Thus we should get
210         // rootTaskInfo instead.
211         if (validTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
212             try {
213                 RootTaskInfo rootTaskInfo =
214                         ActivityTaskManager.getService().getRootTaskInfoOnDisplay(
215                                 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_UNDEFINED,
216                                 validTaskInfo.displayId);
217                 return rootTaskInfo.topActivity;
218             } catch (RemoteException e) {
219                 Log.e(TAG, "findSelectedButtons: Failed getting root task info", e);
220             }
221         } else {
222             return validTaskInfo.topActivity;
223         }
224 
225         return null;
226     }
227 
findButtonsByComponentName( ComponentName componentName)228     private HashSet<CarSystemBarButton> findButtonsByComponentName(
229             ComponentName componentName) {
230         HashSet<CarSystemBarButton> buttons =
231                 mButtonsByComponentName.get(componentName.flattenToShortString());
232         return (buttons != null) ? buttons :
233                 mButtonsByComponentName.get(componentName.flattenToString());
234     }
235 
getPackageCategory(String packageName)236     private String getPackageCategory(String packageName) {
237         PackageManager pm = mContext.getPackageManager();
238         Set<String> supportedCategories = mButtonsByCategory.keySet();
239         for (String category : supportedCategories) {
240             Intent intent = new Intent();
241             intent.setPackage(packageName);
242             intent.setAction(Intent.ACTION_MAIN);
243             intent.addCategory(category);
244             List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
245             if (list.size() > 0) {
246                 // Cache this package name into ButtonsByPackage map, so we won't have to query
247                 // all categories next time this package name shows up.
248                 mButtonsByPackage.put(packageName, mButtonsByCategory.get(category));
249                 return category;
250             }
251         }
252         return null;
253     }
254 
255     // simple multi-map
256     private static class ButtonMap extends HashMap<String, HashSet<CarSystemBarButton>> {
257 
add(String key, CarSystemBarButton value)258         public boolean add(String key, CarSystemBarButton value) {
259             if (containsKey(key)) {
260                 return get(key).add(value);
261             }
262             HashSet<CarSystemBarButton> set = new HashSet<>();
263             set.add(value);
264             put(key, set);
265             return true;
266         }
267     }
268 }
269