/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.car;
import android.app.ActivityManager.StackId;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.support.v4.util.SimpleArrayMap;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.View;
import android.widget.LinearLayout;
import com.android.systemui.R;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
/**
* A controller to populate data for CarNavigationBarView and handle user interactions.
*
*
Each button inside the navigation bar is defined by data in arrays_car.xml. OEMs can
* customize the navigation buttons by updating arrays_car.xml appropriately in an overlay.
*/
class CarNavigationBarController {
private static final String TAG = "CarNavBarController";
private static final String EXTRA_FACET_CATEGORIES = "categories";
private static final String EXTRA_FACET_PACKAGES = "packages";
private static final String EXTRA_FACET_ID = "filter_id";
private static final String EXTRA_FACET_LAUNCH_PICKER = "launch_picker";
/**
* Each facet of the navigation bar maps to a set of package names or categories defined in
* arrays_car.xml. Package names for a given facet are delimited by ";".
*/
private static final String FACET_FILTER_DELIMITER = ";";
private final Context mContext;
private final CarNavigationBarView mNavBar;
private final CarStatusBar mStatusBar;
/**
* Set of categories each facet will filter on.
*/
private final List mFacetCategories = new ArrayList<>();
/**
* Set of package names each facet will filter on.
*/
private final List mFacetPackages = new ArrayList<>();
private final SimpleArrayMap mFacetCategoryMap = new SimpleArrayMap<>();
private final SimpleArrayMap mFacetPackageMap = new SimpleArrayMap<>();
private final List mNavButtons = new ArrayList<>();
private final SparseBooleanArray mFacetHasMultipleAppsCache = new SparseBooleanArray();
private int mCurrentFacetIndex;
private Intent mPersistentTaskIntent;
public CarNavigationBarController(Context context, CarNavigationBarView navBar,
CarStatusBar activityStarter) {
mContext = context;
mNavBar = navBar;
mStatusBar = activityStarter;
bind();
if (context.getResources().getBoolean(R.bool.config_enablePersistentDockedActivity)) {
setupPersistentDockedTask();
}
}
private void setupPersistentDockedTask() {
try {
mPersistentTaskIntent = Intent.parseUri(
mContext.getString(R.string.config_persistentDockedActivityIntentUri),
Intent.URI_INTENT_SCHEME);
} catch (URISyntaxException e) {
Log.e(TAG, "Malformed persistent task intent.");
}
}
public void taskChanged(String packageName, int stackId) {
// If the package name belongs to a filter, then highlight appropriate button in
// the navigation bar.
if (mFacetPackageMap.containsKey(packageName)) {
setCurrentFacet(mFacetPackageMap.get(packageName));
}
// Check if the package matches any of the categories for the facets
String category = getPackageCategory(packageName);
if (category != null) {
setCurrentFacet(mFacetCategoryMap.get(category));
}
// Set up the persistent docked task if needed.
if (mPersistentTaskIntent != null && !mStatusBar.hasDockedTask()
&& stackId != StackId.HOME_STACK_ID) {
mStatusBar.startActivityOnStack(mPersistentTaskIntent, StackId.DOCKED_STACK_ID);
}
}
public void onPackageChange(String packageName) {
if (mFacetPackageMap.containsKey(packageName)) {
int index = mFacetPackageMap.get(packageName);
mFacetHasMultipleAppsCache.put(index, facetHasMultiplePackages(index));
// No need to check categories because we've already refreshed the cache.
return;
}
String category = getPackageCategory(packageName);
if (mFacetCategoryMap.containsKey(category)) {
int index = mFacetCategoryMap.get(category);
mFacetHasMultipleAppsCache.put(index, facetHasMultiplePackages(index));
}
}
/**
* Iterates through the items in arrays_car.xml and sets up the facet bar buttons to
* perform the task in that configuration file when clicked or long-pressed.
*/
private void bind() {
Resources res = mContext.getResources();
TypedArray icons = res.obtainTypedArray(R.array.car_facet_icons);
TypedArray intents = res.obtainTypedArray(R.array.car_facet_intent_uris);
TypedArray longPressIntents = res.obtainTypedArray(R.array.car_facet_longpress_intent_uris);
TypedArray facetPackageNames = res.obtainTypedArray(R.array.car_facet_package_filters);
TypedArray facetCategories = res.obtainTypedArray(R.array.car_facet_category_filters);
try {
if (icons.length() != intents.length()
|| icons.length() != longPressIntents.length()
|| icons.length() != facetPackageNames.length()
|| icons.length() != facetCategories.length()) {
throw new RuntimeException("car_facet array lengths do not match");
}
for (int i = 0, size = icons.length(); i < size; i++) {
Drawable icon = icons.getDrawable(i);
CarNavigationButton button = createNavButton(icon);
initClickListeners(button, i, intents.getString(i), longPressIntents.getString(i));
mNavButtons.add(button);
mNavBar.addButton(button, createNavButton(icon) /* lightsOutButton */);
initFacetFilterMaps(i, facetPackageNames.getString(i).split(FACET_FILTER_DELIMITER),
facetCategories.getString(i).split(FACET_FILTER_DELIMITER));
mFacetHasMultipleAppsCache.put(i, facetHasMultiplePackages(i));
}
} finally {
// Clean up all the TypedArrays.
icons.recycle();
intents.recycle();
longPressIntents.recycle();
facetPackageNames.recycle();
facetCategories.recycle();
}
}
/**
* Recreates each of the buttons on a density or font scale change. This manual process is
* necessary since this class is not part of an activity that automatically gets recreated.
*/
public void onDensityOrFontScaleChanged() {
TypedArray icons = mContext.getResources().obtainTypedArray(R.array.car_facet_icons);
try {
int length = icons.length();
if (length != mNavButtons.size()) {
// This should not happen since the mNavButtons list is created from the length
// of the icons array in bind().
throw new RuntimeException("car_facet array lengths do not match number of "
+ "created buttons.");
}
for (int i = 0; i < length; i++) {
Drawable icon = icons.getDrawable(i);
// Setting a new icon will trigger a requestLayout() call if necessary.
mNavButtons.get(i).setResources(icon);
}
} finally {
icons.recycle();
}
}
private void initFacetFilterMaps(int id, String[] packageNames, String[] categories) {
mFacetCategories.add(categories);
for (String category : categories) {
mFacetCategoryMap.put(category, id);
}
mFacetPackages.add(packageNames);
for (String packageName : packageNames) {
mFacetPackageMap.put(packageName, id);
}
}
private String getPackageCategory(String packageName) {
PackageManager pm = mContext.getPackageManager();
int size = mFacetCategories.size();
// For each facet, check if the given package name matches one of its categories
for (int i = 0; i < size; i++) {
String[] categories = mFacetCategories.get(i);
for (int j = 0; j < categories.length; j++) {
String category = categories[j];
Intent intent = new Intent();
intent.setPackage(packageName);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(category);
List list = pm.queryIntentActivities(intent, 0);
if (list.size() > 0) {
// Cache this package name into facetPackageMap, so we won't have to query
// all categories next time this package name shows up.
mFacetPackageMap.put(packageName, mFacetCategoryMap.get(category));
return category;
}
}
}
return null;
}
/**
* Helper method to check if a given facet has multiple packages associated with it. This can
* be resource defined package names or package names filtered by facet category.
*
* @return {@code true} if the facet at the given index has more than one package.
*/
private boolean facetHasMultiplePackages(int index) {
PackageManager pm = mContext.getPackageManager();
// Check if the packages defined for the filter actually exists on the device
String[] packages = mFacetPackages.get(index);
if (packages.length > 1) {
int count = 0;
for (int i = 0; i < packages.length; i++) {
count += pm.getLaunchIntentForPackage(packages[i]) != null ? 1 : 0;
if (count > 1) {
return true;
}
}
}
// If there weren't multiple packages defined for the facet, check the categories
// and see if they resolve to multiple package names
String categories[] = mFacetCategories.get(index);
int count = 0;
for (int i = 0; i < categories.length; i++) {
String category = categories[i];
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(category);
count += pm.queryIntentActivities(intent, 0).size();
if (count > 1) {
return true;
}
}
return false;
}
/**
* Sets the facet at the given index to be the facet that is currently active. The button will
* be highlighted appropriately.
*/
private void setCurrentFacet(int index) {
if (index == mCurrentFacetIndex) {
return;
}
if (mNavButtons.get(mCurrentFacetIndex) != null) {
mNavButtons.get(mCurrentFacetIndex)
.setSelected(false /* selected */, false /* showMoreIcon */);
}
if (mNavButtons.get(index) != null) {
mNavButtons.get(index).setSelected(true /* selected */,
mFacetHasMultipleAppsCache.get(index) /* showMoreIcon */);
}
mCurrentFacetIndex = index;
}
/**
* Creates the View that is used for the buttons along the navigation bar.
*
* @param icon The icon to be used for the button.
*/
private CarNavigationButton createNavButton(Drawable icon) {
CarNavigationButton button = (CarNavigationButton) View.inflate(mContext,
R.layout.car_navigation_button, null);
button.setResources(icon);
LinearLayout.LayoutParams lp =
new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1);
button.setLayoutParams(lp);
return button;
}
/**
* Initializes the click and long click listeners that correspond to the given command string.
* The click listeners are attached to the given button.
*/
private void initClickListeners(View button, int index, String clickString,
String longPressString) {
// Each button at least have an action when pressed.
if (TextUtils.isEmpty(clickString)) {
throw new RuntimeException("Facet at index " + index + " does not have click action.");
}
try {
Intent intent = Intent.parseUri(clickString, Intent.URI_INTENT_SCHEME);
button.setOnClickListener(v -> onFacetClicked(intent, index));
} catch (URISyntaxException e) {
throw new RuntimeException("Malformed intent uri", e);
}
if (TextUtils.isEmpty(longPressString)) {
button.setLongClickable(false);
return;
}
try {
Intent intent = Intent.parseUri(longPressString, Intent.URI_INTENT_SCHEME);
button.setOnLongClickListener(v -> {
onFacetLongClicked(intent, index);
return true;
});
} catch (URISyntaxException e) {
throw new RuntimeException("Malformed long-press intent uri", e);
}
}
/**
* Handles a click on a facet. A click will trigger the given Intent.
*
* @param index The index of the facet that was clicked.
*/
private void onFacetClicked(Intent intent, int index) {
String packageName = intent.getPackage();
if (packageName == null) {
return;
}
intent.putExtra(EXTRA_FACET_CATEGORIES, mFacetCategories.get(index));
intent.putExtra(EXTRA_FACET_PACKAGES, mFacetPackages.get(index));
// The facet is identified by the index in which it was added to the nav bar.
// This value can be used to determine which facet was selected
intent.putExtra(EXTRA_FACET_ID, Integer.toString(index));
// If the current facet is clicked, we want to launch the picker by default
// rather than the "preferred/last run" app.
intent.putExtra(EXTRA_FACET_LAUNCH_PICKER, index == mCurrentFacetIndex);
int stackId = StackId.FULLSCREEN_WORKSPACE_STACK_ID;
if (intent.getCategories().contains(Intent.CATEGORY_HOME)) {
stackId = StackId.HOME_STACK_ID;
}
setCurrentFacet(index);
mStatusBar.startActivityOnStack(intent, stackId);
}
/**
* Handles a long-press on a facet. The long-press will trigger the given Intent.
*
* @param index The index of the facet that was clicked.
*/
private void onFacetLongClicked(Intent intent, int index) {
setCurrentFacet(index);
mStatusBar.startActivityOnStack(intent, StackId.FULLSCREEN_WORKSPACE_STACK_ID);
}
}