1 /* 2 * Copyright (C) 2015 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 package com.android.systemui.statusbar.car; 17 18 import android.content.Context; 19 import android.content.Intent; 20 import android.content.pm.PackageManager; 21 import android.content.pm.ResolveInfo; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.graphics.drawable.Drawable; 25 import android.support.v4.util.SimpleArrayMap; 26 import android.util.SparseBooleanArray; 27 import android.view.View; 28 import android.widget.LinearLayout; 29 30 import com.android.systemui.R; 31 import com.android.systemui.statusbar.phone.ActivityStarter; 32 33 import java.net.URISyntaxException; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.List; 37 38 /** 39 * A controller to populate data for CarNavigationBarView and handle user interactions. 40 * <p/> 41 * Each button inside the navigation bar is defined by data in arrays_car.xml. OEMs can customize 42 * the navigation buttons by updating arrays_car.xml appropriately in an overlay. 43 */ 44 class CarNavigationBarController { 45 private static final String EXTRA_FACET_CATEGORIES = "categories"; 46 private static final String EXTRA_FACET_PACKAGES = "packages"; 47 private static final String EXTRA_FACET_ID = "filter_id"; 48 private static final String EXTRA_FACET_LAUNCH_PICKER = "launch_picker"; 49 50 // Each facet of the navigation bar maps to a set of package names or categories defined in 51 // arrays_car.xml. Package names for a given facet are delimited by ";" 52 private static final String FACET_FILTER_DEMILITER = ";"; 53 54 private Context mContext; 55 private CarNavigationBarView mNavBar; 56 private ActivityStarter mActivityStarter; 57 58 // Set of categories each facet will filter on. 59 private List<String[]> mFacetCategories = new ArrayList<String[]>(); 60 // Set of package names each facet will filter on. 61 private List<String[]> mFacetPackages = new ArrayList<String[]>(); 62 63 private SimpleArrayMap<String, Integer> mFacetCategoryMap 64 = new SimpleArrayMap<String, Integer>(); 65 private SimpleArrayMap<String, Integer> mFacetPackageMap 66 = new SimpleArrayMap<String, Integer>(); 67 68 private List<Intent> mIntents; 69 private List<Intent> mLongPressIntents; 70 71 private List<CarNavigationButton> mNavButtons = new ArrayList<CarNavigationButton>(); 72 73 private int mCurrentFacetIndex; 74 private SparseBooleanArray mFacetHasMultipleAppsCache = new SparseBooleanArray(); 75 CarNavigationBarController(Context context, CarNavigationBarView navBar, ActivityStarter activityStarter)76 public CarNavigationBarController(Context context, 77 CarNavigationBarView navBar, 78 ActivityStarter activityStarter) { 79 mContext = context; 80 mNavBar = navBar; 81 mActivityStarter = activityStarter; 82 bind(); 83 } 84 taskChanged(String packageName)85 public void taskChanged(String packageName) { 86 // If the package name belongs to a filter, then highlight appropriate button in 87 // the navigation bar. 88 if (mFacetPackageMap.containsKey(packageName)) { 89 setCurrentFacet(mFacetPackageMap.get(packageName)); 90 } 91 92 // Check if the package matches any of the categories for the facets 93 String category = getPackageCategory(packageName); 94 if (category != null) { 95 setCurrentFacet(mFacetCategoryMap.get(category)); 96 } 97 } 98 onPackageChange(String packageName)99 public void onPackageChange(String packageName) { 100 if (mFacetPackageMap.containsKey(packageName)) { 101 int index = mFacetPackageMap.get(packageName); 102 mFacetHasMultipleAppsCache.put(index, facetHasMultiplePackages(index)); 103 // No need to check categories because we've already refreshed the cache. 104 return; 105 } 106 107 String category = getPackageCategory(packageName); 108 if (mFacetCategoryMap.containsKey(category)) { 109 int index = mFacetCategoryMap.get(category); 110 mFacetHasMultipleAppsCache.put(index, facetHasMultiplePackages(index)); 111 } 112 } 113 bind()114 private void bind() { 115 // Read up arrays_car.xml and populate the navigation bar here. 116 Resources r = mContext.getResources(); 117 TypedArray icons = r.obtainTypedArray(R.array.car_facet_icons); 118 TypedArray intents = r.obtainTypedArray(R.array.car_facet_intent_uris); 119 TypedArray longpressIntents = 120 r.obtainTypedArray(R.array.car_facet_longpress_intent_uris); 121 TypedArray facetPackageNames = r.obtainTypedArray(R.array.car_facet_package_filters); 122 123 TypedArray facetCategories = r.obtainTypedArray(R.array.car_facet_category_filters); 124 125 if (icons.length() != intents.length() 126 || icons.length() != longpressIntents.length() 127 || icons.length() != facetPackageNames.length() 128 || icons.length() != facetCategories.length()) { 129 throw new RuntimeException("car_facet array lengths do not match"); 130 } 131 132 mIntents = createEmptyIntentList(icons.length()); 133 mLongPressIntents = createEmptyIntentList(icons.length()); 134 135 for (int i = 0; i < icons.length(); i++) { 136 Drawable icon = icons.getDrawable(i); 137 try { 138 mIntents.set(i, 139 Intent.parseUri(intents.getString(i), Intent.URI_INTENT_SCHEME)); 140 141 String longpressUri = longpressIntents.getString(i); 142 boolean hasLongpress = !longpressUri.isEmpty(); 143 if (hasLongpress) { 144 mLongPressIntents.set(i, 145 Intent.parseUri(longpressUri, Intent.URI_INTENT_SCHEME)); 146 } 147 148 CarNavigationButton button = createNavButton(icon, i, hasLongpress); 149 mNavButtons.add(button); 150 mNavBar.addButton(button, 151 createNavButton(icon, i, hasLongpress) /* lightsOutButton */); 152 153 initFacetFilterMaps(i, 154 facetPackageNames.getString(i).split(FACET_FILTER_DEMILITER), 155 facetCategories.getString(i).split(FACET_FILTER_DEMILITER)); 156 mFacetHasMultipleAppsCache.put(i, facetHasMultiplePackages(i)); 157 } catch (URISyntaxException e) { 158 throw new RuntimeException("Malformed intent uri", e); 159 } 160 } 161 } 162 initFacetFilterMaps(int id, String[] packageNames, String[] categories)163 private void initFacetFilterMaps(int id, String[] packageNames, String[] categories) { 164 mFacetCategories.add(categories); 165 for (int i = 0; i < categories.length; i++) { 166 mFacetCategoryMap.put(categories[i], id); 167 } 168 169 mFacetPackages.add(packageNames); 170 for (int i = 0; i < packageNames.length; i++) { 171 mFacetPackageMap.put(packageNames[i], id); 172 } 173 } 174 getPackageCategory(String packageName)175 private String getPackageCategory(String packageName) { 176 PackageManager pm = mContext.getPackageManager(); 177 int size = mFacetCategories.size(); 178 // For each facet, check if the given package name matches one of its categories 179 for (int i = 0; i < size; i++) { 180 String[] categories = mFacetCategories.get(i); 181 for (int j = 0; j < categories.length; j++) { 182 String category = categories[j]; 183 Intent intent = new Intent(); 184 intent.setPackage(packageName); 185 intent.setAction(Intent.ACTION_MAIN); 186 intent.addCategory(category); 187 List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); 188 if (list.size() > 0) { 189 // Cache this package name into facetPackageMap, so we won't have to query 190 // all categories next time this package name shows up. 191 mFacetPackageMap.put(packageName, mFacetCategoryMap.get(category)); 192 return category; 193 } 194 } 195 } 196 return null; 197 } 198 199 /** 200 * Helper method to check if a given facet has multiple packages associated with it. 201 * This can be resource defined package names or package names filtered by facet category. 202 */ facetHasMultiplePackages(int index)203 private boolean facetHasMultiplePackages(int index) { 204 PackageManager pm = mContext.getPackageManager(); 205 206 // Check if the packages defined for the filter actually exists on the device 207 String[] packages = mFacetPackages.get(index); 208 if (packages.length > 1) { 209 int count = 0; 210 for (int i = 0; i < packages.length; i++) { 211 count += pm.getLaunchIntentForPackage(packages[i]) != null ? 1 : 0; 212 if (count > 1) { 213 return true; 214 } 215 } 216 } 217 218 // If there weren't multiple packages defined for the facet, check the categories 219 // and see if they resolve to multiple package names 220 String categories[] = mFacetCategories.get(index); 221 222 int count = 0; 223 for (int i = 0; i < categories.length; i++) { 224 String category = categories[i]; 225 Intent intent = new Intent(); 226 intent.setAction(Intent.ACTION_MAIN); 227 intent.addCategory(category); 228 count += pm.queryIntentActivities(intent, 0).size(); 229 if (count > 1) { 230 return true; 231 } 232 } 233 return false; 234 } 235 setCurrentFacet(int index)236 private void setCurrentFacet(int index) { 237 if (index == mCurrentFacetIndex) { 238 return; 239 } 240 241 if (mNavButtons.get(mCurrentFacetIndex) != null) { 242 mNavButtons.get(mCurrentFacetIndex) 243 .setSelected(false /* selected */, false /* showMoreIcon */); 244 } 245 246 if (mNavButtons.get(index) != null) { 247 mNavButtons.get(index).setSelected(true /* selected */, 248 mFacetHasMultipleAppsCache.get(index) /* showMoreIcon */); 249 } 250 mCurrentFacetIndex = index; 251 } 252 createNavButton(Drawable icon, final int id, boolean longClickEnabled)253 private CarNavigationButton createNavButton(Drawable icon, final int id, 254 boolean longClickEnabled) { 255 CarNavigationButton button = (CarNavigationButton) View.inflate(mContext, 256 R.layout.car_navigation_button, null); 257 button.setResources(icon); 258 LinearLayout.LayoutParams lp = 259 new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1); 260 button.setLayoutParams(lp); 261 262 button.setOnClickListener(new View.OnClickListener() { 263 @Override 264 public void onClick(View v) { 265 onFacetClicked(id); 266 } 267 }); 268 269 if (longClickEnabled) { 270 button.setLongClickable(true); 271 button.setOnLongClickListener(new View.OnLongClickListener() { 272 @Override 273 public boolean onLongClick(View v) { 274 onFacetLongClicked(id); 275 return true; 276 } 277 }); 278 } else { 279 button.setLongClickable(false); 280 } 281 282 return button; 283 } 284 startActivity(Intent intent)285 private void startActivity(Intent intent) { 286 if (mActivityStarter != null && intent != null) { 287 mActivityStarter.startActivity(intent, false); 288 } 289 } 290 onFacetClicked(int index)291 private void onFacetClicked(int index) { 292 Intent intent = mIntents.get(index); 293 String packageName = intent.getPackage(); 294 295 if (packageName == null) { 296 return; 297 } 298 299 intent.putExtra(EXTRA_FACET_CATEGORIES, mFacetCategories.get(index)); 300 intent.putExtra(EXTRA_FACET_PACKAGES, mFacetPackages.get(index)); 301 // The facet is identified by the index in which it was added to the nav bar. 302 // This value can be used to determine which facet was selected 303 intent.putExtra(EXTRA_FACET_ID, Integer.toString(index)); 304 305 // If the current facet is clicked, we want to launch the picker by default 306 // rather than the "preferred/last run" app. 307 intent.putExtra(EXTRA_FACET_LAUNCH_PICKER, index == mCurrentFacetIndex); 308 309 setCurrentFacet(index); 310 startActivity(intent); 311 } 312 onFacetLongClicked(int index)313 private void onFacetLongClicked(int index) { 314 setCurrentFacet(index); 315 startActivity(mLongPressIntents.get(index)); 316 } 317 createEmptyIntentList(int size)318 private List<Intent> createEmptyIntentList(int size) { 319 return Arrays.asList(new Intent[size]); 320 } 321 } 322