Add titles to PreferenceFragments and PreferenceScreens for watches.

Preferences lack a title on watch type devices due to lack of ActionBar
support. A custom ListView was added to use a custom wrapper adapter to
add a persistent header view at the top of the ListView that developers
would not be able to remove via the ListView API.

Bug: 27962897
Change-Id: I6bccecf85592d9507e0c7a04c9a035617001e9ef
diff --git a/core/java/android/preference/PreferenceFragment.java b/core/java/android/preference/PreferenceFragment.java
index d4a3582..73fa01e 100644
--- a/core/java/android/preference/PreferenceFragment.java
+++ b/core/java/android/preference/PreferenceFragment.java
@@ -27,12 +27,14 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.text.TextUtils;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnKeyListener;
 import android.view.ViewGroup;
 import android.widget.ListView;
+import android.widget.TextView;
 
 /**
  * Shows a hierarchy of {@link Preference} objects as
@@ -366,6 +368,20 @@
     private void bindPreferences() {
         final PreferenceScreen preferenceScreen = getPreferenceScreen();
         if (preferenceScreen != null) {
+            View root = getView();
+            if (root != null) {
+                View titleView = root.findViewById(android.R.id.title);
+                if (titleView instanceof TextView) {
+                    CharSequence title = preferenceScreen.getTitle();
+                    if (TextUtils.isEmpty(title)) {
+                        titleView.setVisibility(View.GONE);
+                    } else {
+                        ((TextView) titleView).setText(title);
+                        titleView.setVisibility(View.VISIBLE);
+                    }
+                }
+            }
+
             preferenceScreen.bind(getListView());
         }
         onBindPreferences();
diff --git a/core/java/android/preference/PreferenceScreen.java b/core/java/android/preference/PreferenceScreen.java
index b1317e6..2305b05 100644
--- a/core/java/android/preference/PreferenceScreen.java
+++ b/core/java/android/preference/PreferenceScreen.java
@@ -19,6 +19,8 @@
 import android.app.Dialog;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -31,6 +33,7 @@
 import android.widget.AdapterView;
 import android.widget.ListAdapter;
 import android.widget.ListView;
+import android.widget.TextView;
 
 /**
  * Represents a top-level {@link Preference} that
@@ -91,13 +94,33 @@
     private Dialog mDialog;
 
     private ListView mListView;
-    
+
+    private int mLayoutResId = com.android.internal.R.layout.preference_list_fragment;
+    private Drawable mDividerDrawable;
+    private boolean mDividerSpecified;
+
     /**
      * Do NOT use this constructor, use {@link PreferenceManager#createPreferenceScreen(Context)}.
      * @hide-
      */
     public PreferenceScreen(Context context, AttributeSet attrs) {
         super(context, attrs, com.android.internal.R.attr.preferenceScreenStyle);
+
+        TypedArray a = context.obtainStyledAttributes(null,
+                com.android.internal.R.styleable.PreferenceScreen,
+                com.android.internal.R.attr.preferenceScreenStyle,
+                0);
+
+        mLayoutResId = a.getResourceId(
+                com.android.internal.R.styleable.PreferenceScreen_screenLayout,
+                mLayoutResId);
+        if (a.hasValueOrEmpty(com.android.internal.R.styleable.PreferenceScreen_divider)) {
+            mDividerDrawable =
+                    a.getDrawable(com.android.internal.R.styleable.PreferenceScreen_divider);
+            mDividerSpecified = true;
+        }
+
+        a.recycle();
     }
 
     /**
@@ -163,18 +186,30 @@
 
         LayoutInflater inflater = (LayoutInflater)
                 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        View childPrefScreen = inflater.inflate(
-                com.android.internal.R.layout.preference_list_fragment, null);
+        View childPrefScreen = inflater.inflate(mLayoutResId, null);
+        View titleView = childPrefScreen.findViewById(android.R.id.title);
         mListView = (ListView) childPrefScreen.findViewById(android.R.id.list);
+        if (mDividerSpecified) {
+            mListView.setDivider(mDividerDrawable);
+        }
+
         bind(mListView);
 
         // Set the title bar if title is available, else no title bar
         final CharSequence title = getTitle();
         Dialog dialog = mDialog = new Dialog(context, context.getThemeResId());
         if (TextUtils.isEmpty(title)) {
+            if (titleView != null) {
+                titleView.setVisibility(View.GONE);
+            }
             dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
         } else {
-            dialog.setTitle(title);
+            if (titleView instanceof TextView) {
+                ((TextView) titleView).setText(title);
+                titleView.setVisibility(View.VISIBLE);
+            } else {
+                dialog.setTitle(title);
+            }
         }
         dialog.setContentView(childPrefScreen);
         dialog.setOnDismissListener(this);
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
index 8f908fb..b0f19d7 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -112,8 +112,8 @@
         public boolean isSelectable;
     }
 
-    private ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList();
-    private ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList();
+    ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList();
+    ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList();
 
     Drawable mDivider;
     int mDividerHeight;
@@ -279,7 +279,7 @@
         // Wrap the adapter if it wasn't already wrapped.
         if (mAdapter != null) {
             if (!(mAdapter instanceof HeaderViewListAdapter)) {
-                mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
+                wrapHeaderListAdapterInternal();
             }
 
             // In the case of re-adding a header view, or adding one later on,
@@ -373,7 +373,7 @@
         // Wrap the adapter if it wasn't already wrapped.
         if (mAdapter != null) {
             if (!(mAdapter instanceof HeaderViewListAdapter)) {
-                mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
+                wrapHeaderListAdapterInternal();
             }
 
             // In the case of re-adding a footer view, or adding one later on,
@@ -476,7 +476,7 @@
         mRecycler.clear();
 
         if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
-            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
+            mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
         } else {
             mAdapter = adapter;
         }
@@ -2228,7 +2228,7 @@
      * after the header views.
      */
     public void setSelectionAfterHeaderView() {
-        final int count = mHeaderViewInfos.size();
+        final int count = getHeaderViewsCount();
         if (count > 0) {
             mNextSelectedPosition = 0;
             return;
@@ -3356,7 +3356,7 @@
             bounds.right = mRight - mLeft - mPaddingRight;
 
             final int count = getChildCount();
-            final int headerCount = mHeaderViewInfos.size();
+            final int headerCount = getHeaderViewsCount();
             final int itemCount = mItemCount;
             final int footerLimit = (itemCount - mFooterViewInfos.size());
             final boolean headerDividers = mHeaderDividersEnabled;
@@ -3940,7 +3940,7 @@
         if (drawDividers) {
             final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
             final int itemCount = mItemCount;
-            final int headerCount = mHeaderViewInfos.size();
+            final int headerCount = getHeaderViewsCount();
             final int footerLimit = (itemCount - mFooterViewInfos.size());
             final boolean isHeader = (itemIndex < headerCount);
             final boolean isFooter = (itemIndex >= footerLimit);
@@ -4052,4 +4052,24 @@
 
         encoder.addProperty("recycleOnMeasure", recycleOnMeasure());
     }
+
+    /** @hide */
+    protected HeaderViewListAdapter wrapHeaderListAdapterInternal(
+            ArrayList<ListView.FixedViewInfo> headerViewInfos,
+            ArrayList<ListView.FixedViewInfo> footerViewInfos,
+            ListAdapter adapter) {
+        return new HeaderViewListAdapter(headerViewInfos, footerViewInfos, adapter);
+    }
+
+    /** @hide */
+    protected void wrapHeaderListAdapterInternal() {
+        mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, mAdapter);
+    }
+
+    /** @hide */
+    protected void dispatchDataSetObserverOnChangedInternal() {
+        if (mDataSetObserver != null) {
+            mDataSetObserver.onChanged();
+        }
+    }
 }
diff --git a/core/java/com/android/internal/widget/WatchHeaderListView.java b/core/java/com/android/internal/widget/WatchHeaderListView.java
new file mode 100644
index 0000000..3d32d86
--- /dev/null
+++ b/core/java/com/android/internal/widget/WatchHeaderListView.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 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.
+ */
+package com.android.internal.widget;
+
+import android.annotation.IdRes;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.HeaderViewListAdapter;
+
+import java.util.ArrayList;
+
+import com.android.internal.util.Predicate;
+
+public class WatchHeaderListView extends ListView {
+    private View mTopPanel;
+
+    public WatchHeaderListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public WatchHeaderListView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public WatchHeaderListView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected HeaderViewListAdapter wrapHeaderListAdapterInternal(
+            ArrayList<ListView.FixedViewInfo> headerViewInfos,
+            ArrayList<ListView.FixedViewInfo> footerViewInfos,
+            ListAdapter adapter) {
+        return new WatchHeaderListAdapter(headerViewInfos, footerViewInfos, adapter);
+    }
+
+    @Override
+    public void addView(View child, ViewGroup.LayoutParams params) {
+        if (mTopPanel == null) {
+            setTopPanel(child);
+        } else {
+            throw new IllegalStateException("WatchHeaderListView can host only one header");
+        }
+    }
+
+    public void setTopPanel(View v) {
+        mTopPanel = v;
+        wrapAdapterIfNecessary();
+    }
+
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        super.setAdapter(adapter);
+        wrapAdapterIfNecessary();
+    }
+
+    @Override
+    protected View findViewTraversal(@IdRes int id) {
+        View v = super.findViewTraversal(id);
+        if (v == null && mTopPanel != null && !mTopPanel.isRootNamespace()) {
+            return mTopPanel.findViewById(id);
+        }
+        return v;
+    }
+
+    @Override
+    protected View findViewWithTagTraversal(Object tag) {
+        View v = super.findViewWithTagTraversal(tag);
+        if (v == null && mTopPanel != null && !mTopPanel.isRootNamespace()) {
+            return mTopPanel.findViewWithTag(tag);
+        }
+        return v;
+    }
+
+    @Override
+    protected View findViewByPredicateTraversal(Predicate<View> predicate, View childToSkip) {
+        View v = super.findViewByPredicateTraversal(predicate, childToSkip);
+        if (v == null && mTopPanel != null && mTopPanel != childToSkip
+                && !mTopPanel.isRootNamespace()) {
+            return mTopPanel.findViewByPredicate(predicate);
+        }
+        return v;
+    }
+
+    @Override
+    public int getHeaderViewsCount() {
+        return mTopPanel == null ? super.getHeaderViewsCount() : super.getHeaderViewsCount() + 1;
+    }
+
+    private void wrapAdapterIfNecessary() {
+        ListAdapter adapter = getAdapter();
+        if (adapter != null && mTopPanel != null) {
+            if (!(adapter instanceof WatchHeaderListAdapter)) {
+                wrapHeaderListAdapterInternal();
+            }
+
+            ((WatchHeaderListAdapter) getAdapter()).setTopPanel(mTopPanel);
+            dispatchDataSetObserverOnChangedInternal();
+        }
+    }
+
+    private static class WatchHeaderListAdapter extends HeaderViewListAdapter {
+        private View mTopPanel;
+
+        public WatchHeaderListAdapter(
+                ArrayList<ListView.FixedViewInfo> headerViewInfos,
+                ArrayList<ListView.FixedViewInfo> footerViewInfos,
+                ListAdapter adapter) {
+            super(headerViewInfos, footerViewInfos, adapter);
+        }
+
+        public void setTopPanel(View v) {
+            mTopPanel = v;
+        }
+
+        private int getTopPanelCount() {
+            return mTopPanel == null ? 0 : 1;
+        }
+
+        @Override
+        public int getCount() {
+            return super.getCount() + getTopPanelCount();
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return mTopPanel == null && super.areAllItemsEnabled();
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            if (mTopPanel != null) {
+                if (position == 0) {
+                    return false;
+                } else {
+                    return super.isEnabled(position - 1);
+                }
+            }
+
+            return super.isEnabled(position);
+        }
+
+        @Override
+        public Object getItem(int position) {
+            if (mTopPanel != null) {
+                if (position == 0) {
+                    return null;
+                } else {
+                    return super.getItem(position - 1);
+                }
+            }
+
+            return super.getItem(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            int numHeaders = getHeadersCount() + getTopPanelCount();
+            if (getWrappedAdapter() != null && position >= numHeaders) {
+                int adjPosition = position - numHeaders;
+                int adapterCount = getWrappedAdapter().getCount();
+                if (adjPosition < adapterCount) {
+                    return getWrappedAdapter().getItemId(adjPosition);
+                }
+            }
+            return -1;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (mTopPanel != null) {
+                if (position == 0) {
+                    return mTopPanel;
+                } else {
+                    return super.getView(position - 1, convertView, parent);
+                }
+            }
+
+            return super.getView(position, convertView, parent);
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            int numHeaders = getHeadersCount() + getTopPanelCount();
+            if (getWrappedAdapter() != null && position >= numHeaders) {
+                int adjPosition = position - numHeaders;
+                int adapterCount = getWrappedAdapter().getCount();
+                if (adjPosition < adapterCount) {
+                    return getWrappedAdapter().getItemViewType(adjPosition);
+                }
+            }
+
+            return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+        }
+    }
+}
diff --git a/core/res/res/layout-watch/preference_list_fragment_material.xml b/core/res/res/layout-watch/preference_list_fragment_material.xml
new file mode 100644
index 0000000..ae8f203
--- /dev/null
+++ b/core/res/res/layout-watch/preference_list_fragment_material.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent"
+    android:background="@android:color/transparent"
+    android:layout_removeBorders="true">
+
+    <FrameLayout
+        android:id="@android:id/list_container"
+        android:layout_width="match_parent"
+        android:layout_height="0px"
+        android:layout_weight="1">
+        <com.android.internal.widget.WatchHeaderListView
+            android:id="@android:id/list"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            style="?attr/preferenceFragmentListStyle"
+            android:scrollbarStyle="@integer/preference_fragment_scrollbarStyle"
+            android:clipToPadding="false"
+            android:drawSelectorOnTop="false"
+            android:cacheColorHint="@android:color/transparent"
+            android:scrollbarAlwaysDrawVerticalTrack="true">
+             <TextView
+                android:id="@android:id/title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingStart="@dimen/dialog_padding_material"
+                android:paddingEnd="@dimen/dialog_padding_material"
+                android:paddingBottom="8dp"
+                android:textAppearance="@style/TextAppearance.Material.Title"
+                android:gravity="center" />
+        </com.android.internal.widget.WatchHeaderListView>
+    </FrameLayout>
+
+    <TextView android:id="@android:id/empty"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:padding="@dimen/preference_fragment_padding_side"
+        android:gravity="center"
+        android:visibility="gone" />
+
+    <RelativeLayout android:id="@+id/button_bar"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:layout_weight="0"
+        android:visibility="gone">
+
+        <Button android:id="@+id/back_button"
+            android:layout_width="150dip"
+            android:layout_height="wrap_content"
+            android:layout_margin="5dip"
+            android:layout_alignParentStart="true"
+            android:text="@string/back_button_label"
+        />
+        <LinearLayout
+            android:orientation="horizontal"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentEnd="true">
+
+            <Button android:id="@+id/skip_button"
+                android:layout_width="150dip"
+                android:layout_height="wrap_content"
+                android:layout_margin="5dip"
+                android:text="@string/skip_button_label"
+                android:visibility="gone"
+            />
+
+            <Button android:id="@+id/next_button"
+                android:layout_width="150dip"
+                android:layout_height="wrap_content"
+                android:layout_margin="5dip"
+                android:text="@string/next_button_label"
+            />
+        </LinearLayout>
+    </RelativeLayout>
+</LinearLayout>
diff --git a/core/res/res/values-watch/styles_material.xml b/core/res/res/values-watch/styles_material.xml
index 5c4c632..f5735e6 100644
--- a/core/res/res/values-watch/styles_material.xml
+++ b/core/res/res/values-watch/styles_material.xml
@@ -57,6 +57,10 @@
         <item name="divider">@empty</item>
     </style>
 
+    <style name="Preference.Material.PreferenceScreen" parent="Preference.Material.BasePreferenceScreen">
+        <item name="divider">@empty</item>
+    </style>
+
     <style name="Widget.Material.TextView" parent="Widget.TextView">
         <item name="breakStrategy">balanced</item>
     </style>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 3f889f9..0c08564 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7901,6 +7901,13 @@
         <attr name="divider" />
     </declare-styleable>
 
+    <!-- Base attributes available to PreferenceScreen. -->
+    <declare-styleable name="PreferenceScreen">
+        <!-- The layout for the PreferenceScreen. This should rarely need to be changed. -->
+        <attr name="screenLayout" format="reference" />
+        <attr name="divider" />
+    </declare-styleable>
+
     <!-- Base attributes available to PreferenceActivity. -->
     <declare-styleable name="PreferenceActivity">
         <!-- The layout for the Preference Activity. This should rarely need to be changed. -->
diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml
index 8c1a927..4435537 100644
--- a/core/res/res/values/styles_material.xml
+++ b/core/res/res/values/styles_material.xml
@@ -81,7 +81,12 @@
         <item name="layout">@layout/preference_widget_seekbar_material</item>
     </style>
 
-    <style name="Preference.Material.PreferenceScreen"/>
+    <style name="Preference.Material.BasePreferenceScreen">
+        <item name="screenLayout">@layout/preference_list_fragment_material</item>
+        <item name="divider">?attr/listDivider</item>
+    </style>
+
+    <style name="Preference.Material.PreferenceScreen" parent="Preference.Material.BasePreferenceScreen"/>
 
     <style name="Preference.Material.DialogPreference">
         <item name="positiveButtonText">@string/ok</item>