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