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