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 // Clear all registered views 131 mRegisteredViews.forEach(carSystemBarButton -> { 132 if (carSystemBarButton.getDisplayId() == displayId) { 133 carSystemBarButton.setSelected(false); 134 } 135 }); 136 mSelectedButtons.clear(); 137 138 HashSet<CarSystemBarButton> selectedButtons = findSelectedButtons(validTaskInfo); 139 140 if (selectedButtons != null) { 141 selectedButtons.forEach(carSystemBarButton -> { 142 if (carSystemBarButton.getDisplayId() == displayId) { 143 carSystemBarButton.setSelected(true); 144 mSelectedButtons.add(carSystemBarButton); 145 } 146 }); 147 } 148 } 149 150 /** 151 * Defaults to Display.DEFAULT_DISPLAY when no parameter is provided for the validDisplay. 152 * 153 * @param taskInfoList 154 */ taskChanged(List<RootTaskInfo> taskInfoList)155 protected void taskChanged(List<RootTaskInfo> taskInfoList) { 156 taskChanged(taskInfoList, Display.DEFAULT_DISPLAY); 157 } 158 159 /** 160 * Add navigation button to this controller if it uses selection state. 161 */ addButtonWithSelectionState(CarSystemBarButton carSystemBarButton)162 private void addButtonWithSelectionState(CarSystemBarButton carSystemBarButton) { 163 if (mRegisteredViews.contains(carSystemBarButton)) { 164 return; 165 } 166 String[] categories = carSystemBarButton.getCategories(); 167 for (int i = 0; i < categories.length; i++) { 168 mButtonsByCategory.add(categories[i], carSystemBarButton); 169 } 170 171 String[] packages = carSystemBarButton.getPackages(); 172 for (int i = 0; i < packages.length; i++) { 173 mButtonsByPackage.add(packages[i], carSystemBarButton); 174 } 175 String[] componentNames = carSystemBarButton.getComponentName(); 176 for (int i = 0; i < componentNames.length; i++) { 177 mButtonsByComponentName.add(componentNames[i], carSystemBarButton); 178 } 179 180 mRegisteredViews.add(carSystemBarButton); 181 } 182 findSelectedButtons(RootTaskInfo validTaskInfo)183 private HashSet<CarSystemBarButton> findSelectedButtons(RootTaskInfo validTaskInfo) { 184 ComponentName topActivity = null; 185 186 // Window mode being WINDOW_MODE_MULTI_WINDOW implies TaskView might be visible on the 187 // display. In such cases, topActivity reported by validTaskInfo will be the one hosted in 188 // TaskView and not necessarily the main activity visible on display. Thus we should get 189 // rootTaskInfo instead. 190 if (validTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { 191 try { 192 RootTaskInfo rootTaskInfo = 193 ActivityTaskManager.getService().getRootTaskInfoOnDisplay( 194 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_UNDEFINED, 195 validTaskInfo.displayId); 196 topActivity = rootTaskInfo.topActivity; 197 } catch (RemoteException e) { 198 Log.e(TAG, "findSelectedButtons: Failed getting root task info", e); 199 } 200 } else { 201 topActivity = validTaskInfo.topActivity; 202 } 203 204 if (topActivity == null) return null; 205 206 String packageName = topActivity.getPackageName(); 207 208 HashSet<CarSystemBarButton> selectedButtons = 209 findButtonsByComponentName(topActivity); 210 if (selectedButtons == null) { 211 selectedButtons = mButtonsByPackage.get(packageName); 212 } 213 if (selectedButtons == null) { 214 String category = getPackageCategory(packageName); 215 if (category != null) { 216 selectedButtons = mButtonsByCategory.get(category); 217 } 218 } 219 220 return selectedButtons; 221 } 222 findButtonsByComponentName( ComponentName componentName)223 private HashSet<CarSystemBarButton> findButtonsByComponentName( 224 ComponentName componentName) { 225 HashSet<CarSystemBarButton> buttons = 226 mButtonsByComponentName.get(componentName.flattenToShortString()); 227 return (buttons != null) ? buttons : 228 mButtonsByComponentName.get(componentName.flattenToString()); 229 } 230 getPackageCategory(String packageName)231 private String getPackageCategory(String packageName) { 232 PackageManager pm = mContext.getPackageManager(); 233 Set<String> supportedCategories = mButtonsByCategory.keySet(); 234 for (String category : supportedCategories) { 235 Intent intent = new Intent(); 236 intent.setPackage(packageName); 237 intent.setAction(Intent.ACTION_MAIN); 238 intent.addCategory(category); 239 List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); 240 if (list.size() > 0) { 241 // Cache this package name into ButtonsByPackage map, so we won't have to query 242 // all categories next time this package name shows up. 243 mButtonsByPackage.put(packageName, mButtonsByCategory.get(category)); 244 return category; 245 } 246 } 247 return null; 248 } 249 250 // simple multi-map 251 private static class ButtonMap extends HashMap<String, HashSet<CarSystemBarButton>> { 252 add(String key, CarSystemBarButton value)253 public boolean add(String key, CarSystemBarButton value) { 254 if (containsKey(key)) { 255 return get(key).add(value); 256 } 257 HashSet<CarSystemBarButton> set = new HashSet<>(); 258 set.add(value); 259 put(key, set); 260 return true; 261 } 262 } 263 } 264