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