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