Allow setting MenuItems via XML

Test: Manually
Change-Id: I91c4210c942cd061ebbf9c5b05e4afde86751124
diff --git a/car-ui-lib/res/values/attrs.xml b/car-ui-lib/res/values/attrs.xml
index d2ebcf8..163d586 100644
--- a/car-ui-lib/res/values/attrs.xml
+++ b/car-ui-lib/res/values/attrs.xml
@@ -37,6 +37,51 @@
             <enum name="close" value="1"/>
             <enum name="down" value="2"/>
         </attr>
+        <!-- XML resource of MenuItems. See Toolbar.setMenuItems(int) for more information. -->
+        <attr name="menuItems" format="reference"/>
+    </declare-styleable>
+
+    <declare-styleable name="CarUiToolbarMenuItem">
+        <!-- Show/hide the MenuItem -->
+        <attr name="visible" format="boolean"/>
+        <!-- Title -->
+        <attr name="title" format="string"/>
+        <!-- Icon -->
+        <attr name="icon" format="reference"/>
+        <!-- True to tint the icon to a consistent color. Default true, all the other booleans default to false -->
+        <attr name="tinted" format="boolean"/>
+        <!-- Show both the icon and title at the same time -->
+        <attr name="showIconAndTitle" format="boolean"/>
+        <!-- True if this MenuItem should be a switch -->
+        <attr name="checkable" format="boolean"/>
+        <!-- Whether the switch should be checked or not. Setting this implies checkable=true -->
+        <attr name="checked" format="boolean"/>
+        <!-- True if this MenuItem should be activatable, in which case it will visually toggle states when clicked -->
+        <attr name="activatable" format="boolean"/>
+        <!-- Whether the MenuItem starts activated. Setting this implies activatable=true -->
+        <attr name="activated" format="boolean"/>
+        <!-- How to display the MenuItem. "always" means always show it on the toolbar, "never" means never show it on the toolbar and instead show it in the overflow menu -->
+        <attr name="displayBehavior" format="enum">
+            <enum name="always" value="0"/>
+            <enum name="never" value="1"/>
+        </attr>
+        <!-- Ux restrictions required to interact with this MenuItem -->
+        <attr name="uxRestrictions">
+            <!-- Values are copied from android.car.drivingstate.CarUxRestrictions. Note:
+            UX_RESTRICTIONS_BASELINE is not allowed here because it's useless and confusing. -->
+            <flag name="UX_RESTRICTIONS_NO_DIALPAD" value="1"/>
+            <flag name="UX_RESTRICTIONS_NO_FILTERING" value="2"/>
+            <flag name="UX_RESTRICTIONS_LIMIT_STRING_LENGTH" value="4"/>
+            <flag name="UX_RESTRICTIONS_NO_KEYBOARD" value="8"/>
+            <flag name="UX_RESTRICTIONS_NO_VIDEO" value="16"/>
+            <flag name="UX_RESTRICTIONS_LIMIT_CONTENT" value="32"/>
+            <flag name="UX_RESTRICTIONS_NO_SETUP" value="64"/>
+            <flag name="UX_RESTRICTIONS_NO_TEXT_MESSAGE" value="128"/>
+            <flag name="UX_RESTRICTIONS_NO_VOICE_TRANSCRIPTION" value="256"/>
+            <flag name="UX_RESTRICTIONS_FULLY_RESTRICTED" value="511"/>
+        </attr>
+        <!-- The name of a method that takes a MenuItem as an argument in you'r toolbar's Activity. Will be called when the MenuItem is clicked -->
+        <attr name="onClick" format="string"/>
     </declare-styleable>
 
     <!-- Theme attribute to specifying a default style for all CarUiToolbars -->
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java
index 5b01235..2df4d95 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java
@@ -25,7 +25,7 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.car.ui.R;
-import com.android.car.ui.utils.ResourceUtils;
+import com.android.car.ui.utils.CarUiUtils;
 /**
  * Code drop from {androidx.car.widget.CarUiSmoothScroller}
  *
@@ -54,13 +54,13 @@
     }
 
     private void init(Context context) {
-        mMillisecondsPerInch = ResourceUtils.getFloat(context.getResources(),
+        mMillisecondsPerInch = CarUiUtils.getFloat(context.getResources(),
                 R.dimen.car_ui_scrollbar_milliseconds_per_inch);
-        mDecelerationTimeDivisor = ResourceUtils.getFloat(context.getResources(),
+        mDecelerationTimeDivisor = CarUiUtils.getFloat(context.getResources(),
                 R.dimen.car_ui_scrollbar_deceleration_times_divisor);
         mInterpolator =
                 new DecelerateInterpolator(
-                        ResourceUtils.getFloat(context.getResources(),
+                        CarUiUtils.getFloat(context.getResources(),
                                 R.dimen.car_ui_scrollbar_decelerate_interpolator_factor));
         mDensityDpi = context.getResources().getDisplayMetrics().densityDpi;
         mMillisecondsPerPixel = mMillisecondsPerInch / mDensityDpi;
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java b/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
index bb8b3fe..263e681 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
@@ -35,7 +35,7 @@
 
 import com.android.car.ui.R;
 import com.android.car.ui.recyclerview.CarUiRecyclerView.ScrollBarPosition;
-import com.android.car.ui.utils.ResourceUtils;
+import com.android.car.ui.utils.CarUiUtils;
 
 /**
  * The default scroll bar widget for the {@link CarUiRecyclerView}.
@@ -92,7 +92,7 @@
 
         Resources res = rv.getContext().getResources();
 
-        mButtonDisabledAlpha = ResourceUtils.getFloat(res, R.dimen.car_ui_button_disabled_alpha);
+        mButtonDisabledAlpha = CarUiUtils.getFloat(res, R.dimen.car_ui_button_disabled_alpha);
 
         if (scrollBarAboveRecyclerView) {
             parent.addView(mScrollView);
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
index 9901d69..9d0c254 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
@@ -229,7 +229,9 @@
 
     /** Sets the Icon of this MenuItem to a drawable resource. */
     public void setIcon(int resId) {
-        setIcon(mContext.getDrawable(resId));
+        setIcon(resId == 0
+                ? null
+                : mContext.getDrawable(resId));
     }
 
     /** Returns if this is the search MenuItem, which has special behavior when searching */
@@ -300,7 +302,19 @@
          * <p>The icon's color and size will be changed to match the other MenuItems.
          */
         public Builder setIcon(int resId) {
-            mIcon = mContext.getDrawable(resId);
+            mIcon = resId == 0
+                    ? null
+                    : mContext.getDrawable(resId);
+            return this;
+        }
+
+        /**
+         * Sets the icon to a drawable.
+         *
+         * <p>The icon's color and size will be changed to match the other MenuItems.
+         */
+        public Builder setIcon(Drawable icon) {
+            mIcon = icon;
             return this;
         }
 
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
index 3cda47a..c2ef149 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
@@ -15,8 +15,14 @@
  */
 package com.android.car.ui.toolbar;
 
+import android.app.Activity;
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Xml;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -25,10 +31,23 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import androidx.annotation.XmlRes;
+
 import com.android.car.ui.R;
+import com.android.car.ui.utils.CarUiUtils;
 import com.android.car.ui.utils.CarUxRestrictionsUtil;
 import com.android.car.ui.uxr.DrawableStateView;
 
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 class MenuItemRenderer implements MenuItem.Listener {
 
     private static final int[] RESTRICTED_STATE = new int[] {R.attr.state_ux_restricted};
@@ -194,4 +213,102 @@
     private boolean isRestricted() {
         return CarUxRestrictionsUtil.isRestricted(mMenuItem.getUxRestrictions(), mUxRestrictions);
     }
+
+    static List<MenuItem> readMenuItemList(Context c, @XmlRes int resId) {
+        if (resId == 0) {
+            return Collections.emptyList();
+        }
+
+        try (XmlResourceParser parser = c.getResources().getXml(resId)) {
+            AttributeSet attrs = Xml.asAttributeSet(parser);
+            List<MenuItem> menuItems = new ArrayList<>();
+
+            parser.next();
+            parser.next();
+            parser.require(XmlPullParser.START_TAG, null, "MenuItems");
+            while (parser.next() != XmlPullParser.END_TAG) {
+                menuItems.add(readMenuItem(c, parser, attrs));
+            }
+
+            return menuItems;
+        } catch (XmlPullParserException | IOException e) {
+            throw new RuntimeException("Unable to parse Menu Items", e);
+        }
+    }
+
+    private static MenuItem readMenuItem(Context c, XmlResourceParser parser, AttributeSet attrs)
+            throws XmlPullParserException, IOException {
+
+        parser.require(XmlPullParser.START_TAG, null, "MenuItem");
+
+        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CarUiToolbarMenuItem);
+        try {
+            String title = a.getString(R.styleable.CarUiToolbarMenuItem_title);
+            Drawable icon = a.getDrawable(R.styleable.CarUiToolbarMenuItem_icon);
+            boolean tinted = a.getBoolean(R.styleable.CarUiToolbarMenuItem_tinted, true);
+            boolean visible = a.getBoolean(R.styleable.CarUiToolbarMenuItem_visible, true);
+            boolean showIconAndTitle = a.getBoolean(
+                    R.styleable.CarUiToolbarMenuItem_showIconAndTitle, false);
+            boolean checkable = a.getBoolean(R.styleable.CarUiToolbarMenuItem_checkable, false);
+            boolean checked = a.getBoolean(R.styleable.CarUiToolbarMenuItem_checked, false);
+            boolean checkedExists = a.hasValue(R.styleable.CarUiToolbarMenuItem_checked);
+            boolean activatable = a.getBoolean(R.styleable.CarUiToolbarMenuItem_activatable, false);
+            boolean activated = a.getBoolean(R.styleable.CarUiToolbarMenuItem_activated, false);
+            boolean activatedExists = a.hasValue(R.styleable.CarUiToolbarMenuItem_activated);
+            int displayBehaviorInt = a.getInt(R.styleable.CarUiToolbarMenuItem_displayBehavior, 0);
+            int uxRestrictions = a.getInt(R.styleable.CarUiToolbarMenuItem_uxRestrictions, 0);
+            String onClickMethod = a.getString(R.styleable.CarUiToolbarMenuItem_onClick);
+            MenuItem.OnClickListener onClickListener = null;
+
+            if (onClickMethod != null) {
+                Activity activity = CarUiUtils.getActivity(c);
+                if (activity == null) {
+                    throw new RuntimeException("Couldn't find an activity for the MenuItem");
+                }
+
+                try {
+                    Method m = activity.getClass().getMethod(onClickMethod, MenuItem.class);
+                    onClickListener = i -> {
+                        try {
+                            m.invoke(activity, i);
+                        } catch (InvocationTargetException | IllegalAccessException e) {
+                            throw new RuntimeException("Couldn't call the MenuItem's listener", e);
+                        }
+                    };
+                } catch (NoSuchMethodException e) {
+                    throw new RuntimeException("OnClick method "
+                            + onClickMethod + "(MenuItem) not found in your activity", e);
+                }
+            }
+
+            MenuItem.DisplayBehavior displayBehavior = displayBehaviorInt == 0
+                    ? MenuItem.DisplayBehavior.ALWAYS
+                    : MenuItem.DisplayBehavior.NEVER;
+
+            parser.next();
+            parser.require(XmlPullParser.END_TAG, null, "MenuItem");
+
+            MenuItem.Builder builder = new MenuItem.Builder(c)
+                    .setTitle(title)
+                    .setIcon(icon)
+                    .setOnClickListener(onClickListener)
+                    .setUxRestrictions(uxRestrictions)
+                    .setTinted(tinted)
+                    .setVisible(visible)
+                    .setShowIconAndTitle(showIconAndTitle)
+                    .setDisplayBehavior(displayBehavior);
+
+            if (checkable || checkedExists) {
+                builder.setChecked(checked);
+            }
+
+            if (activatable || activatedExists) {
+                builder.setActivated(activated);
+            }
+
+            return builder.build();
+        } finally {
+            a.recycle();
+        }
+    }
 }
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
index 01e7c97..3a6a2d8 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
@@ -19,7 +19,6 @@
 import android.app.AlertDialog;
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
-import android.content.ContextWrapper;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
 import android.os.Parcel;
@@ -39,8 +38,10 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
+import androidx.annotation.XmlRes;
 
 import com.android.car.ui.R;
+import com.android.car.ui.utils.CarUiUtils;
 import com.android.car.ui.utils.CarUxRestrictionsUtil;
 
 import java.util.ArrayList;
@@ -184,6 +185,7 @@
             mTitle.setText(a.getString(R.styleable.CarUiToolbar_title));
             setLogo(a.getResourceId(R.styleable.CarUiToolbar_logo, 0));
             setBackgroundShown(a.getBoolean(R.styleable.CarUiToolbar_showBackground, true));
+            setMenuItems(a.getResourceId(R.styleable.CarUiToolbar_menuItems, 0));
             mShowMenuItemsWhileSearching = a.getBoolean(
                     R.styleable.CarUiToolbar_showMenuItemsWhileSearching, false);
             String searchHint = a.getString(R.styleable.CarUiToolbar_searchHint);
@@ -593,6 +595,38 @@
         setState(mState);
     }
 
+    /**
+     * Sets the {@link MenuItem Menuitems} to display to a list defined in XML.
+     *
+     * The XML file must have one <MenuItems> tag, with a variable number of <MenuItem>
+     * child tags. See CarUiToolbarMenuItem in CarUi's attrs.xml for a list of available attributes.
+     *
+     * Example:
+     * <pre>
+     * <MenuItems>
+     *     <MenuItem
+     *         app:title="Foo"/>
+     *     <MenuItem
+     *         app:title="Bar"
+     *         app:icon="@drawable/ic_tracklist"
+     *         app:onClick="xmlMenuItemClicked"/>
+     *     <MenuItem
+     *         app:title="Bar"
+     *         app:checkable="true"
+     *         app:uxRestrictions="FULLY_RESTRICTED"
+     *         app:onClick="xmlMenuItemClicked"/>
+     * </MenuItems>
+     * </pre>
+     *
+     * @see #setMenuItems(List)
+     * @return The MenuItems that were loaded from XML.
+     */
+    public List<MenuItem> setMenuItems(@XmlRes int resId) {
+        List<MenuItem> menuItems = MenuItemRenderer.readMenuItemList(mContext, resId);
+        setMenuItems(menuItems);
+        return menuItems;
+    }
+
     /** Gets the {@link MenuItem MenuItems} currently displayed */
     @NonNull
     public List<MenuItem> getMenuItems() {
@@ -655,17 +689,6 @@
         mSearchView.setSearchQuery(query);
     }
 
-    private Activity getActivity() {
-        Context context = getContext();
-        while (context instanceof ContextWrapper) {
-            if (context instanceof Activity) {
-                return (Activity) context;
-            }
-            context = ((ContextWrapper) context).getBaseContext();
-        }
-        return null;
-    }
-
     /**
      * Sets the state of the toolbar. This will show/hide the appropriate elements of the toolbar
      * for the desired state.
@@ -685,7 +708,7 @@
             }
 
             if (!absorbed) {
-                Activity activity = getActivity();
+                Activity activity = CarUiUtils.getActivity(getContext());
                 if (activity != null) {
                     activity.onBackPressed();
                 }
diff --git a/car-ui-lib/src/com/android/car/ui/utils/ResourceUtils.java b/car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java
similarity index 62%
rename from car-ui-lib/src/com/android/car/ui/utils/ResourceUtils.java
rename to car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java
index 9835d69..fcbe695 100644
--- a/car-ui-lib/src/com/android/car/ui/utils/ResourceUtils.java
+++ b/car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java
@@ -15,17 +15,21 @@
  */
 package com.android.car.ui.utils;
 
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
 import android.content.res.Resources;
 import android.util.TypedValue;
 
 import androidx.annotation.DimenRes;
+import androidx.annotation.Nullable;
 
 /**
- * Collection of resource utility methods
+ * Collection of utility methods
  */
-public final class ResourceUtils {
+public final class CarUiUtils {
     /** This is a utility class */
-    private ResourceUtils() {}
+    private CarUiUtils() {}
 
     /**
      * Reads a float value from a dimens resource. This is necessary as {@link Resources#getFloat}
@@ -39,4 +43,21 @@
         res.getValue(resId, outValue, true);
         return outValue.getFloat();
     }
+
+    /**
+     * Gets the {@link Activity} for a certain {@link Context}.
+     *
+     * <p>It is possible the Context is not associated with an Activity, in which case
+     * this method will return null.
+     */
+    @Nullable
+    public static Activity getActivity(Context context) {
+        while (context instanceof ContextWrapper) {
+            if (context instanceof Activity) {
+                return (Activity) context;
+            }
+            context = ((ContextWrapper) context).getBaseContext();
+        }
+        return null;
+    }
 }
diff --git a/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml b/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml
new file mode 100644
index 0000000..119afea
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 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.
+  -->
+<MenuItems xmlns:app="http://schemas.android.com/apk/res-auto">
+    <MenuItem
+        app:title="@string/preferences_screen_title"/>
+    <MenuItem
+        app:title="Bar"
+        app:icon="@drawable/ic_tracklist"
+        app:onClick="xmlMenuItemClicked"/>
+    <MenuItem
+        app:title="Bar"
+        app:checkable="true"
+        app:uxRestrictions="UX_RESTRICTIONS_FULLY_RESTRICTED"
+        app:onClick="xmlMenuItemClicked"/>
+</MenuItems>
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
index 8b60d4d..cf55a5f 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
@@ -71,6 +71,11 @@
         mButtons.add(Pair.create("Change title", v ->
                 toolbar.setTitle(toolbar.getTitle() + " X")));
 
+        mButtons.add(Pair.create("MenuItem: Set to XML source", v -> {
+            mMenuItems.clear();
+            toolbar.setMenuItems(R.xml.menuitems);
+        }));
+
         mButtons.add(Pair.create("MenuItem: Add Icon", v -> {
             mMenuItems.add(MenuItem.Builder.createSettings(this, i ->
                     Toast.makeText(this, "Clicked", Toast.LENGTH_SHORT).show()));
@@ -234,6 +239,11 @@
         prv.setAdapter(mAdapter);
     }
 
+    public void xmlMenuItemClicked(MenuItem item) {
+        Toast.makeText(this, "Xml item clicked! " + item.getTitle(),
+                Toast.LENGTH_SHORT).show();
+    }
+
     private void getMenuItem(MenuItem.OnClickListener listener) {
         if (mMenuItems.size() == 1) {
             listener.onClick(mMenuItems.get(0));