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