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.util.SparseBooleanArray;
27import android.view.View;
28import android.widget.LinearLayout;
29
30import com.android.systemui.R;
31import com.android.systemui.statusbar.phone.ActivityStarter;
32
33import java.net.URISyntaxException;
34import java.util.ArrayList;
35import java.util.Arrays;
36import 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 */
44class 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
76    public CarNavigationBarController(Context context,
77                                      CarNavigationBarView navBar,
78                                      ActivityStarter activityStarter) {
79        mContext = context;
80        mNavBar = navBar;
81        mActivityStarter = activityStarter;
82        bind();
83    }
84
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
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
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
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
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     */
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
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
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
285    private void startActivity(Intent intent) {
286        if (mActivityStarter != null && intent != null) {
287            mActivityStarter.startActivity(intent, false);
288        }
289    }
290
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
313    private void onFacetLongClicked(int index) {
314        setCurrentFacet(index);
315        startActivity(mLongPressIntents.get(index));
316    }
317
318    private List<Intent> createEmptyIntentList(int size) {
319        return Arrays.asList(new Intent[size]);
320    }
321}
322