Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1c6d0589 authored by Victor Chan's avatar Victor Chan
Browse files

Highlight car nav facet when recent task changes.

CarStatusBar will now register a ITaskStackListener
to handle changes in task stack and highlight the
appropriate facet for the current task. Currently using resource
defined package names and category to the filter for a given
facet. OEMs are expected to use categories definied in Intent.java

Also refactored business logic from CarNavigationBarView
into CarNavigationBar controller.

Change-Id: I203917ea43f2f488a1167f27dab84f1c451b3e93
parent 2300b834
Loading
Loading
Loading
Loading
+27 −0
Original line number Diff line number Diff line
<!--
  ~ 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
  -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="48.0dp"
        android:height="48.0dp"
        android:viewportWidth="48.0"
        android:viewportHeight="48.0">
    <path
        android:fillColor="#FFFFFFFF"
        android:pathData="M14.0,20.0l10.0,10.0 10.0,-10.0z"/>
    <path
        android:pathData="M0 0h48v48H0z"
        android:fillColor="#00000000"/>
</vector>
+42 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
**
** Copyright 2016, 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.
*/
-->

<com.android.systemui.statusbar.car.CarNavigationButton
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_height="match_parent"
        android:layout_width="wrap_content"
        android:orientation="horizontal"
        android:gravity="center">
    <com.android.keyguard.AlphaOptimizedImageButton
            android:id="@+id/car_nav_button_icon"
            android:layout_height="match_parent"
            android:layout_width="wrap_content"
            android:layout_centerInParent="true"
            android:animateLayoutChanges="true">
    </com.android.keyguard.AlphaOptimizedImageButton>

    <com.android.keyguard.AlphaOptimizedImageButton
            android:id="@+id/car_nav_button_more_icon"
            android:layout_height="match_parent"
            android:layout_width="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toRightOf="@+id/car_nav_button_icon"
            android:animateLayoutChanges="true">
    </com.android.keyguard.AlphaOptimizedImageButton>
</com.android.systemui.statusbar.car.CarNavigationButton>
+5 −3
Original line number Diff line number Diff line
@@ -22,7 +22,9 @@
         isn't a longpress action associated with a shortcut item, put in an empty item to make
         sure everything lines up.
    -->
    <array name="car_shortcut_icons" />
    <array name="car_shortcut_intent_uris" />
    <array name="car_shortcut_longpress_intent_uris" />
    <array name="car_facet_icons" />
    <array name="car_facet_intent_uris" />
    <array name="car_facet_longpress_intent_uris" />
    <array name="car_facet_package_filters"/>
    <array name="car_facet_category_filters"/>
</resources>
+275 −0
Original line number Diff line number Diff line
/*
 * 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.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.view.View;
import android.widget.LinearLayout;
import com.android.systemui.R;
import com.android.systemui.statusbar.phone.ActivityStarter;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

/**
 * A controller to populate data for CarNavigationBarView and handle user interactions.
 * <p/>
 * 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 {

    // 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_DEMILITER = ";";

    private Context mContext;
    private CarNavigationBarView mNavBar;
    private ActivityStarter mActivityStarter;

    // Set of categories each facet will filter on.
    private List<String[]> mFacetCategories = new ArrayList<String[]>();
    // Set of package names each facet will filter on.
    private List<String[]> mFacetPackages = new ArrayList<String[]>();

    private SimpleArrayMap<String, Integer> mFacetCategoryMap
            = new SimpleArrayMap<String, Integer>();
    private SimpleArrayMap<String, Integer> mFacetPackageMap
            = new SimpleArrayMap<String, Integer>();

    private List<Intent> mIntents = new ArrayList<Intent>();
    private List<Intent> mLongPressIntents = new ArrayList<Intent>();

    private List<CarNavigationButton> mNavButtons = new ArrayList<CarNavigationButton>();

    private int mCurrentFacetIndex;

    public CarNavigationBarController(Context context,
                                      CarNavigationBarView navBar,
                                      ActivityStarter activityStarter) {
        mContext = context;
        mNavBar = navBar;
        mActivityStarter = activityStarter;
        bind();
    }

    public void taskChanged(String packageName) {
        // 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));
        }
    }

    private void bind() {
        // Read up arrays_car.xml and populate the navigation bar here.
        Resources r = mContext.getResources();
        TypedArray icons = r.obtainTypedArray(R.array.car_facet_icons);
        TypedArray intents = r.obtainTypedArray(R.array.car_facet_intent_uris);
        TypedArray longpressIntents =
                r.obtainTypedArray(R.array.car_facet_longpress_intent_uris);
        TypedArray facetPackageNames = r.obtainTypedArray(R.array.car_facet_package_filters);

        TypedArray facetCategories = r.obtainTypedArray(R.array.car_facet_category_filters);

        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; i < icons.length(); i++) {
            Drawable icon = icons.getDrawable(i);
            try {
                mIntents.add(i,
                        Intent.parseUri(intents.getString(i), Intent.URI_INTENT_SCHEME));

                String longpressUri = longpressIntents.getString(i);
                boolean hasLongpress = !longpressUri.isEmpty();
                if (hasLongpress) {
                    mLongPressIntents.add(i,
                            Intent.parseUri(longpressUri, Intent.URI_INTENT_SCHEME));
                }

                CarNavigationButton button = createNavButton(icon, i, hasLongpress);
                mNavButtons.add(button);
                mNavBar.addButton(button, createNavButton(icon, i, hasLongpress));

                initFacetFilterMaps(i,
                        facetPackageNames.getString(i).split(FACET_FILTER_DEMILITER),
                        facetCategories.getString(i).split(FACET_FILTER_DEMILITER));
            } catch (URISyntaxException e) {
                throw new RuntimeException("Malformed intent uri", e);
            }
        }
    }

    private void initFacetFilterMaps(int id, String[] packageNames, String[] categories){
        mFacetCategories.add(categories);
        for (int i = 0; i < categories.length; i++) {
            mFacetCategoryMap.put(categories[i], id);
        }

        mFacetPackages.add(packageNames);
        for (int i = 0; i < packageNames.length; i++) {
            mFacetPackageMap.put(packageNames[i], 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<ResolveInfo> 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.
     */
    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;
    }

    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 */,
                    facetHasMultiplePackages(index)  /* showMoreIcon */);
        }
        mCurrentFacetIndex = index;
    }

    private CarNavigationButton createNavButton(Drawable icon, final int id,
                                                boolean longClickEnabled) {
        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);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                setCurrentFacet(id);
                onFacetClicked(id);
            }
        });

        if (longClickEnabled) {
            button.setLongClickable(true);
            button.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    onFacetLongClicked(id);
                    setCurrentFacet(id);
                    return true;
                }
            });
        } else {
            button.setLongClickable(false);
        }
        return button;
    }

    private void startActivity(Intent intent) {
        if (mActivityStarter != null && intent != null) {
            mActivityStarter.startActivity(intent, true);
        }
    }

    private void onFacetClicked(int index) {
        // TODO: determine what data to pass to the trampoline, so it can start
        // the default app or the lens picker.
        startActivity(mIntents.get(index));
    }

    private void onFacetLongClicked(int index) {
        // TODO: determine what data to pass to the trampoline, so it can start
        // the default app or the lens picker.
        startActivity(mLongPressIntents.get(index));
    }
}
+11 −88
Original line number Diff line number Diff line
@@ -17,36 +17,29 @@
package com.android.systemui.statusbar.car;

import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.R.color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;

import com.android.systemui.R;
import com.android.systemui.statusbar.phone.ActivityStarter;
import com.android.systemui.statusbar.phone.NavigationBarView;
import com.android.systemui.statusbar.phone.NavigationBarGestureHelper;
import com.android.systemui.statusbar.policy.KeyButtonView;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * A custom navigation bar for the automotive use case.
 * <p>
 * The navigation bar in the automotive use case is more like a list of shortcuts, which we
 * expect to be customizable by the car OEMs. This implementation populates the nav_buttons layout
 * from resources rather than the layout file so customization would then mean updating
 * arrays_car.xml appropriately in an overlay.
 * The navigation bar in the automotive use case is more like a list of shortcuts, rendered
 * in a linear layout.
 */
class CarNavigationBarView extends NavigationBarView {
    private ActivityStarter mActivityStarter;
    private LinearLayout mNavButtons;
    private LinearLayout mLightsOutButtons;

    public CarNavigationBarView(Context context, AttributeSet attrs) {
        super(context, attrs);
@@ -54,83 +47,13 @@ class CarNavigationBarView extends NavigationBarView {

    @Override
    public void onFinishInflate() {
        // Read up arrays_car.xml and populate the navigation bar here.
        Context context = getContext();
        Resources r = getContext().getResources();
        TypedArray icons = r.obtainTypedArray(R.array.car_shortcut_icons);
        TypedArray intents = r.obtainTypedArray(R.array.car_shortcut_intent_uris);
        TypedArray longpressIntents =
                r.obtainTypedArray(R.array.car_shortcut_longpress_intent_uris);

        if (icons.length() != intents.length()) {
            throw new RuntimeException("car_shortcut_icons and car_shortcut_intents do not match");
        }

        LinearLayout navButtons = (LinearLayout) findViewById(R.id.nav_buttons);
        LinearLayout lightsOut = (LinearLayout) findViewById(R.id.lights_out);

        for (int i = 0; i < icons.length(); i++) {
            Drawable icon = icons.getDrawable(i);

            try {
                Intent intent = Intent.parseUri(intents.getString(i), Intent.URI_INTENT_SCHEME);
                Intent longpress = null;
                String longpressUri = longpressIntents.getString(i);
                if (!longpressUri.isEmpty()) {
                    longpress = Intent.parseUri(longpressUri, Intent.URI_INTENT_SCHEME);
                }

                // nav_buttons and lights_out should match exactly.
                navButtons.addView(makeButton(context, icon, intent, longpress));
                lightsOut.addView(makeButton(context, icon, intent, longpress));
            } catch (URISyntaxException e) {
                throw new RuntimeException("Malformed intent uri", e);
            }
        }
    }

    private ImageButton makeButton(Context context, Drawable icon,
            final Intent intent, final Intent longpress) {
        ImageButton button = new ImageButton(context);

        button.setImageDrawable(icon);
        button.setScaleType(ScaleType.CENTER);
        button.setBackgroundColor(color.transparent);
        LinearLayout.LayoutParams lp =
                new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1);
        button.setLayoutParams(lp);

        button.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mActivityStarter != null) {
                    mActivityStarter.startActivity(intent, true);
                }
            }
        });

        // Long click handlers are optional.
        if (longpress != null) {
            button.setLongClickable(true);
            button.setOnLongClickListener(new OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    if (mActivityStarter != null) {
                        mActivityStarter.startActivity(longpress, true);
                        return true;
                    }
                    return false;
                }
            });
        } else {
            button.setLongClickable(false);
        }

        return button;
        mNavButtons = (LinearLayout) findViewById(R.id.nav_buttons);
        mLightsOutButtons = (LinearLayout) findViewById(R.id.lights_out);
    }

    public void setActivityStarter(ActivityStarter activityStarter) {
        mActivityStarter = activityStarter;
    public void addButton(CarNavigationButton button, CarNavigationButton lightsOutButton){
        mNavButtons.addView(button);
        mLightsOutButtons.addView(lightsOutButton);
    }

    @Override
Loading