1 /* 2 * Copyright (C) 2018 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.statusbar.car; 18 19 import android.app.ActivityManager; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.PackageManager; 24 import android.content.pm.ResolveInfo; 25 import android.view.Display; 26 import android.view.View; 27 import android.view.ViewGroup; 28 29 import java.util.HashMap; 30 import java.util.HashSet; 31 import java.util.Iterator; 32 import java.util.List; 33 import java.util.Set; 34 35 import javax.inject.Inject; 36 import javax.inject.Singleton; 37 38 /** 39 * CarFacetButtons placed on the nav bar are designed to have visual indication that the active 40 * application on screen is associated with it. This is basically a similar concept to a radio 41 * button group. 42 */ 43 @Singleton 44 public class CarFacetButtonController { 45 46 protected ButtonMap mButtonsByCategory = new ButtonMap(); 47 protected ButtonMap mButtonsByPackage = new ButtonMap(); 48 protected ButtonMap mButtonsByComponentName = new ButtonMap(); 49 protected HashSet<CarFacetButton> mSelectedFacetButtons; 50 protected Context mContext; 51 52 @Inject CarFacetButtonController(Context context)53 public CarFacetButtonController(Context context) { 54 mContext = context; 55 mSelectedFacetButtons = new HashSet<>(); 56 } 57 58 /** 59 * Add facet button to this controller. The expected use is for the facet button 60 * to get a reference to this controller via {@link com.android.systemui.Dependency} 61 * and self add. 62 */ addFacetButton(CarFacetButton facetButton)63 public void addFacetButton(CarFacetButton facetButton) { 64 String[] categories = facetButton.getCategories(); 65 for (int i = 0; i < categories.length; i++) { 66 mButtonsByCategory.add(categories[i], facetButton); 67 } 68 69 String[] facetPackages = facetButton.getFacetPackages(); 70 for (int i = 0; i < facetPackages.length; i++) { 71 mButtonsByPackage.add(facetPackages[i], facetButton); 72 } 73 String[] componentNames = facetButton.getComponentName(); 74 for (int i = 0; i < componentNames.length; i++) { 75 mButtonsByComponentName.add(componentNames[i], facetButton); 76 } 77 } 78 removeAll()79 public void removeAll() { 80 mButtonsByCategory.clear(); 81 mButtonsByPackage.clear(); 82 mButtonsByComponentName.clear(); 83 mSelectedFacetButtons.clear(); 84 } 85 86 /** 87 * Iterate through a view looking for CarFacetButtons and adding them to the controller if found 88 * 89 * @param v the View that may contain CarFacetButtons 90 */ addAllFacetButtons(View v)91 public void addAllFacetButtons(View v) { 92 if (v instanceof CarFacetButton) { 93 addFacetButton((CarFacetButton) v); 94 } else if (v instanceof ViewGroup) { 95 ViewGroup viewGroup = (ViewGroup) v; 96 for (int i = 0; i < viewGroup.getChildCount(); i++) { 97 addAllFacetButtons(viewGroup.getChildAt(i)); 98 } 99 } 100 } 101 102 /** 103 * This will unselect the currently selected CarFacetButton and determine which one should be 104 * selected next. It does this by reading the properties on the CarFacetButton and seeing if 105 * they are a match with the supplied StackInfo list. 106 * The order of selection detection is ComponentName, PackageName then Category 107 * They will then be compared with the supplied StackInfo list. 108 * The StackInfo is expected to be supplied in order of recency and StackInfo will only be used 109 * for consideration if it has the same displayId as the CarFacetButtons. 110 * 111 * @param stackInfoList of the currently running application 112 */ taskChanged(List<ActivityManager.StackInfo> stackInfoList)113 public void taskChanged(List<ActivityManager.StackInfo> stackInfoList) { 114 ActivityManager.StackInfo validStackInfo = null; 115 for (ActivityManager.StackInfo stackInfo : stackInfoList) { 116 // Find the first stack info with a topActivity in the primary display. 117 // TODO: We assume that CarFacetButton will launch an app only in the primary display. 118 // We need to extend the functionality to handle the mutliple display properly. 119 if (stackInfo.topActivity != null && stackInfo.displayId == Display.DEFAULT_DISPLAY) { 120 validStackInfo = stackInfo; 121 break; 122 } 123 } 124 125 if (validStackInfo == null) { 126 // No stack was found that was on the same display as the facet buttons thus return 127 return; 128 } 129 130 if (mSelectedFacetButtons != null) { 131 Iterator<CarFacetButton> iterator = mSelectedFacetButtons.iterator(); 132 while(iterator.hasNext()) { 133 CarFacetButton carFacetButton = iterator.next(); 134 if (carFacetButton.getDisplayId() == validStackInfo.displayId) { 135 carFacetButton.setSelected(false); 136 iterator.remove(); 137 } 138 } 139 } 140 141 String packageName = validStackInfo.topActivity.getPackageName(); 142 HashSet<CarFacetButton> facetButton = 143 findFacetButtonByComponentName(validStackInfo.topActivity); 144 if (facetButton == null) { 145 facetButton = mButtonsByPackage.get(packageName); 146 } 147 148 if (facetButton == null) { 149 String category = getPackageCategory(packageName); 150 if (category != null) { 151 facetButton = mButtonsByCategory.get(category); 152 } 153 } 154 155 if (facetButton != null) { 156 for (CarFacetButton carFacetButton : facetButton) { 157 if (carFacetButton.getDisplayId() == validStackInfo.displayId) { 158 carFacetButton.setSelected(true); 159 mSelectedFacetButtons.add(carFacetButton); 160 } 161 } 162 } 163 164 } 165 findFacetButtonByComponentName(ComponentName componentName)166 private HashSet<CarFacetButton> findFacetButtonByComponentName(ComponentName componentName) { 167 HashSet<CarFacetButton> buttons = 168 mButtonsByComponentName.get(componentName.flattenToShortString()); 169 return (buttons != null) ? buttons : 170 mButtonsByComponentName.get(componentName.flattenToString()); 171 } 172 getPackageCategory(String packageName)173 protected String getPackageCategory(String packageName) { 174 PackageManager pm = mContext.getPackageManager(); 175 Set<String> supportedCategories = mButtonsByCategory.keySet(); 176 for (String category : supportedCategories) { 177 Intent intent = new Intent(); 178 intent.setPackage(packageName); 179 intent.setAction(Intent.ACTION_MAIN); 180 intent.addCategory(category); 181 List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); 182 if (list.size() > 0) { 183 // Cache this package name into facetPackageMap, so we won't have to query 184 // all categories next time this package name shows up. 185 mButtonsByPackage.put(packageName, mButtonsByCategory.get(category)); 186 return category; 187 } 188 } 189 return null; 190 } 191 192 // simple multi-map 193 private static class ButtonMap extends HashMap<String, HashSet<CarFacetButton>> { 194 add(String key, CarFacetButton value)195 public boolean add(String key, CarFacetButton value) { 196 if (containsKey(key)) { 197 return get(key).add(value); 198 } 199 HashSet<CarFacetButton> set = new HashSet<>(); 200 set.add(value); 201 put(key, set); 202 return true; 203 } 204 } 205 } 206