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