OSDN Git Service

This hooks up the show all part of the search as well as footers
authorlinus_lee <llee@cyngn.com>
Sat, 20 Sep 2014 01:46:07 +0000 (18:46 -0700)
committerlinus_lee <llee@cyngn.com>
Thu, 20 Nov 2014 20:03:03 +0000 (12:03 -0800)
The divider is slightly off - I think to properly fix it, we need to bring
in the dividers into the list items and not have the list view handle it.  I
will add comments to Music-48 to track the divider issue

https://cyanogen.atlassian.net/browse/MUSIC-24

Change-Id: I756064154bc4b217a1981ff39bdee7e8000cf911

19 files changed:
res/drawable/inset_list_divider_no_padding.xml [new file with mode: 0644]
res/layout/list_base.xml
res/layout/list_base_nopadding.xml
res/layout/list_header.xml
res/layout/list_item_simple.xml
res/layout/list_search_footer.xml [new file with mode: 0644]
res/layout/list_search_header.xml [new file with mode: 0644]
res/values/colors.xml
res/values/dimens.xml
res/values/strings.xml
res/values/styles.xml
src/com/cyngn/eleven/Config.java
src/com/cyngn/eleven/adapters/SummarySearchAdapter.java
src/com/cyngn/eleven/sectionadapter/SectionAdapter.java
src/com/cyngn/eleven/sectionadapter/SectionCreator.java
src/com/cyngn/eleven/sectionadapter/SectionListContainer.java
src/com/cyngn/eleven/ui/activities/BaseActivity.java
src/com/cyngn/eleven/ui/activities/SearchActivity.java
src/com/cyngn/eleven/utils/SectionCreatorUtils.java

diff --git a/res/drawable/inset_list_divider_no_padding.xml b/res/drawable/inset_list_divider_no_padding.xml
new file mode 100644 (file)
index 0000000..763868f
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+     Copyright (C) 2014 The CyanogenMod 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.
+-->
+<inset xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <shape>
+        <solid android:color="@color/list_item_divider_color" />
+        <corners android:radius="1.0dip" />
+    </shape>
+
+</inset>
\ No newline at end of file
index bb70e39..a365b1e 100644 (file)
@@ -30,6 +30,6 @@
         android:drawSelectorOnTop="false"
         android:fadingEdge="vertical"
         android:fastScrollEnabled="true"
-        android:dividerHeight="1dp"
+        android:dividerHeight="@dimen/divider_height"
         android:divider="@drawable/inset_list_divider" />
 </FrameLayout>
\ No newline at end of file
index 0ba5af1..87ed176 100644 (file)
@@ -30,6 +30,6 @@
         android:drawSelectorOnTop="false"
         android:fadingEdge="vertical"
         android:fastScrollEnabled="true"
-        android:dividerHeight="1dp"
+        android:dividerHeight="@dimen/divider_height"
         android:divider="@drawable/dnd_list_divider"/>
 </FrameLayout>
\ No newline at end of file
index 085fa93..68b590f 100644 (file)
   limitations under the License.
 -->
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/header"
+    android:id="@+id/title"
     android:layout_width="match_parent"
-    android:layout_height="@dimen/list_item_header_height"
+    android:layout_height="30dp"
+    android:layout_alignParentBottom="true"
     android:paddingLeft="@dimen/list_preferred_item_padding"
     android:paddingRight="@dimen/list_preferred_item_padding"
     android:background="@null"
     android:gravity="center_vertical"
+    android:textAllCaps="true"
     android:textColor="@color/list_item_header_text_color"
     android:textSize="@dimen/list_item_header_size"
     android:fontFamily="sans-serif-light" />
index 5552d56..1332594 100644 (file)
@@ -20,7 +20,9 @@
     android:gravity="center_vertical"
     android:minHeight="@dimen/item_normal_height"
     android:paddingBottom="@dimen/list_item_padding_bottom"
-    android:paddingTop="@dimen/list_item_padding_top">
+    android:paddingTop="@dimen/list_item_padding_top"
+    android:paddingLeft="@dimen/list_preferred_item_padding"
+    android:paddingRight="@dimen/list_preferred_item_padding">
 
     <!-- center the text views vertically -->
     <LinearLayout
diff --git a/res/layout/list_search_footer.xml b/res/layout/list_search_footer.xml
new file mode 100644 (file)
index 0000000..457b611
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 Cyanogen, Inc.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@null"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:background="@null"
+        android:gravity="center"
+        android:textAllCaps="true"
+        android:textColor="@color/list_item_search_footer_text_color"
+        android:textSize="@dimen/list_item_footer_size"
+        android:textStyle="bold" />
+
+    <ImageView
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:src="@drawable/inset_list_divider_no_padding" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/list_search_header.xml b/res/layout/list_search_header.xml
new file mode 100644 (file)
index 0000000..d0aa8d1
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2012 Andrew Neal
+  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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="46dp"
+    android:background="@null">
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="20dp"
+        android:layout_alignParentBottom="true"
+        android:paddingLeft="@dimen/list_preferred_item_padding"
+        android:paddingRight="@dimen/list_preferred_item_padding"
+        android:background="@null"
+        android:gravity="center_vertical"
+        android:textAllCaps="true"
+        android:textColor="@color/list_item_header_text_color"
+        android:textSize="@dimen/list_item_header_size"
+        android:fontFamily="sans-serif-light" />
+</RelativeLayout>
\ No newline at end of file
index aa34581..24072b6 100644 (file)
@@ -80,6 +80,7 @@
     <color name="smart_playlist_item_background">#1931353f</color>
     <color name="list_item_background">#4ce4e9ed</color>
     <color name="list_item_header_text_color">#ff3d4049</color>
+    <color name="list_item_search_footer_text_color">#ff41a4f4</color>
     <color name="list_item_text_color">@color/default_text_color</color>
     <color name="list_item_text_color_light">@color/default_text_color_light</color>
     <color name="list_item_divider_color">#4c231f20</color>
@@ -90,6 +91,8 @@
     <color name="tpi_text_color">#bf3d4049</color>
     <color name="tpi_selected_text_color">#bf3d4049</color>
 
+    <!-- Search Colors -->
+    <color name="search_hint_color">#4cffffff</color>
 
     <!-- Color for the text on the audio player -->
     <color name="audio_player_text_color">@color/default_text_color_light</color>
index af6d700..06189b5 100644 (file)
@@ -31,9 +31,6 @@
     <dimen name="overflow_height">30.0dip</dimen>
     <dimen name="overflow_width">24.0dip</dimen>
 
-    <!-- List item section header -->
-    <dimen name="list_item_header_height">30.0dip</dimen>
-
     <!-- List and grid view padding -->
     <dimen name="list_preferred_item_padding">10.0dip</dimen>
     <dimen name="list_menu_item_padding_right">2.0dip</dimen>
@@ -42,6 +39,7 @@
     <dimen name="list_item_padding_top">10.0dip</dimen>
     <dimen name="list_item_padding_bottom">10.0dip</dimen>
     <dimen name="list_item_header_size">16.0sp</dimen>
+    <dimen name="list_item_footer_size">20.0sp</dimen>
     <dimen name="list_item_main_text_size">@dimen/text_size_small</dimen>
     <dimen name="list_item_secondary_text_size">@dimen/text_size_micro</dimen>
     <dimen name="list_item_queue_text_padding_left">15.0dip</dimen>
     <dimen name="no_results_text_padding_bottom">16.0dip</dimen>
     <dimen name="no_reuslts_text_main">20.0sp</dimen>
     <dimen name="no_results_text_secondary">14.0sp</dimen>
+
+    <!-- General consensus is to leave dividers at 1px instead of having different
+    partial scaled up values for different resolutions -->
+    <dimen name="divider_height">1px</dimen>
 </resources>
index 3356d91..f49af10 100644 (file)
@@ -18,7 +18,6 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
 
     <string name="app_name" translatable="false">Music</string>
-    <string name="app_name_uppercase" translatable="false">MUSIC</string>
 
     <!-- Page titles -->
     <string name="page_recent">Recent</string>
     <string name="header_n_albums"><xliff:g id="number">%d</xliff:g> Albums</string>
     <string name="header_5_plus_albums">5+ Albums</string>
 
+    <string name="footer_search_artists">Show all artists</string>
+    <string name="footer_search_albums">Show all albums</string>
+    <string name="footer_search_songs">Show all songs</string>
+    <string name="footer_search_playlists">Show all playlists</string>
+
+    <string name="searchHint">Search Music</string>
+    <string name="search_title_artists">All \"%s\" artists</string>
+    <string name="search_title_albums">All \"%s\" albums</string>
+    <string name="search_title_songs">All \"%s\" songs</string>
+    <string name="search_title_playlists">All \"%s\" playlists</string>
+
     <string name="duration_album_mins_only"><xliff:g id="format">%1$dm</xliff:g></string>
     <string name="duration_album_hour_mins"><xliff:g id="format">%1$dh %2$dm</xliff:g></string>
 
index 3e1ec0f..7d11d0f 100644 (file)
@@ -30,8 +30,8 @@
     </style>
 
     <style name="ActionBarWidgetTheme" parent="@android:style/Theme.Holo">
-        <!-- This is the color of the search text in the action bar -->
-        <item name="android:textColorHint">@android:color/white</item>
+        <!-- This is the color of the search text hint in the action bar -->
+        <item name="android:textColorHint">@color/search_hint_color</item>
         <item name="android:popupMenuStyle">@style/PopupMenu</item>
         <item name="android:dropDownListViewStyle">@style/DropDownListView</item>
         <item name="android:textAppearanceSmallPopupMenu">@style/SmallPopupMenu</item>
@@ -46,7 +46,7 @@
     <!-- Set the default list view divider color and size -->
     <style name="ListView" parent="@android:style/Widget.Holo.ListView">
         <item name="android:divider">@color/list_item_divider_color</item>
-        <item name="android:dividerHeight">1dp</item>
+        <item name="android:dividerHeight">@dimen/divider_height</item>
     </style>
     
     <!-- Sets up the pop up menu backgroudn resource -->
@@ -57,7 +57,7 @@
     <!-- Sets up the pop up menu divider color and height -->
     <style name="DropDownListView" parent="@android:style/Widget.Holo.ListView.DropDown">
         <item name="android:divider">@color/menu_divider_color</item>
-        <item name="android:dividerHeight">1dp</item>
+        <item name="android:dividerHeight">@dimen/divider_height</item>
     </style>
 
     <!-- Sets up the pop up menu text color and size -->
index 8fb973e..af8f1ee 100644 (file)
@@ -72,6 +72,11 @@ public final class Config {
      */
     public static final String SMART_PLAYLIST_TYPE = "smart_playlist_type";
 
+    /**
+     * Number of search results to show at the top level search
+     */
+    public static final int SEARCH_NUM_RESULTS_TO_GET = 3;
+
     public static enum SmartPlaylistType {
         LastAdded(-1),
         RecentlyPlayed(-2),
index 2f3c7a1..4673164 100644 (file)
@@ -9,6 +9,7 @@ import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
+import android.widget.TextView;
 
 import com.cyngn.eleven.R;
 import com.cyngn.eleven.cache.ImageFetcher;
@@ -27,7 +28,7 @@ import java.util.Locale;
 public final class SummarySearchAdapter extends ArrayAdapter<SearchResult> implements SectionAdapter.BasicAdapter {
 
     /**
-     * Number of views (ImageView and TextView)
+     * no-image list item type and with image type
      */
     private static final int VIEW_TYPE_COUNT = 2;
 
@@ -83,7 +84,7 @@ public final class SummarySearchAdapter extends ArrayAdapter<SearchResult> imple
                 // Asynchronously load the artist image into the adapter
                 mImageFetcher.loadArtistImage(item.mArtist, holder.mImage.get());
 
-                mHighlighter.setText(holder.mLineOne.get(), item.mArtist, mPrefix);
+                setText(holder.mLineOne.get(), item.mArtist);
 
                 String songCount = MusicUtils.makeLabel(getContext(), R.plurals.Nsongs, item.mSongCount);
                 String albumCount = MusicUtils.makeLabel(getContext(), R.plurals.Nalbums, item.mAlbumCount);
@@ -96,21 +97,20 @@ public final class SummarySearchAdapter extends ArrayAdapter<SearchResult> imple
                 mImageFetcher.loadAlbumImage(item.mArtist, item.mAlbum,
                         item.mId, holder.mImage.get());
 
-                mHighlighter.setText(holder.mLineOne.get(), item.mAlbum, mPrefix);
-                mHighlighter.setText(holder.mLineTwo.get(), item.mArtist, mPrefix);
+                setText(holder.mLineOne.get(), item.mAlbum);
+                setText(holder.mLineTwo.get(), item.mArtist);
                 break;
             case Song:
                 // Asynchronously load the album images into the adapter
                 mImageFetcher.loadAlbumImage(item.mArtist, item.mAlbum,
                         item.mAlbumId, holder.mImage.get());
 
-                mHighlighter.setText(holder.mLineOne.get(), item.mTitle, mPrefix);
-                mHighlighter.setText(holder.mLineTwo.get(),
-                        MusicUtils.makeCombinedString(getContext(), item.mArtist, item.mAlbum),
-                        mPrefix);
+                setText(holder.mLineOne.get(), item.mTitle);
+                setText(holder.mLineTwo.get(),
+                        MusicUtils.makeCombinedString(getContext(), item.mArtist, item.mAlbum));
                 break;
             case Playlist:
-                mHighlighter.setText(holder.mLineOne.get(), item.mTitle, mPrefix);
+                setText(holder.mLineOne.get(), item.mTitle);
                 String songs = MusicUtils.makeLabel(getContext(), R.plurals.Nsongs, item.mSongCount);
                 holder.mLineTwo.get().setText(songs);
                 holder.mLineThree.get().setVisibility(View.GONE);
@@ -121,6 +121,19 @@ public final class SummarySearchAdapter extends ArrayAdapter<SearchResult> imple
     }
 
     /**
+     * Sets the text onto the textview with highlighting if a prefix is defined
+     * @param textView
+     * @param text
+     */
+    private void setText(final TextView textView, final String text) {
+        if (mPrefix == null) {
+            textView.setText(text);
+        } else {
+            mHighlighter.setText(textView, text, mPrefix);
+        }
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
index a029c07..49e7b16 100644 (file)
@@ -13,6 +13,8 @@ import android.widget.BaseAdapter;
 import android.widget.TextView;
 
 import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.SectionCreatorUtils.Section;
+import com.cyngn.eleven.utils.SectionCreatorUtils.SectionType;
 
 import java.util.TreeMap;
 
@@ -42,9 +44,15 @@ public class SectionAdapter<TItem,
     protected TArrayAdapter mUnderlyingAdapter;
 
     /**
-     * A map of external position to the String to use as the header
+     * A map of external position to the Section type and Identifier
      */
-    protected TreeMap<Integer, String> mSectionHeaders;
+    protected TreeMap<Integer, Section> mSections;
+
+    protected int mHeaderLayoutId;
+    protected boolean mHeaderEnabled;
+
+    protected int mFooterLayoutId;
+    protected boolean mFooterEnabled;
 
     /**
      * {@link Context}
@@ -59,7 +67,10 @@ public class SectionAdapter<TItem,
     public SectionAdapter(final Activity context, final TArrayAdapter underlyingAdapter) {
         mContext = context;
         mUnderlyingAdapter = underlyingAdapter;
-        mSectionHeaders = new TreeMap<Integer, String>();
+        mSections = new TreeMap<Integer, Section>();
+        setupHeaderParameters(R.layout.list_header, false);
+        // since we have no good default footer, just re-use the header layout
+        setupFooterParameters(R.layout.list_header, false);
     }
 
     /**
@@ -75,13 +86,18 @@ public class SectionAdapter<TItem,
      */
     @Override
     public View getView(final int position, View convertView, final ViewGroup parent) {
-        if (isSectionHeader(position)) {
+        if (isSection(position)) {
             if (convertView == null) {
-                convertView = LayoutInflater.from(mContext).inflate(R.layout.list_header, parent, false);
+                int layoutId = mHeaderLayoutId;
+                if (isSectionFooter(position)) {
+                    layoutId = mFooterLayoutId;
+                }
+
+                convertView = LayoutInflater.from(mContext).inflate(layoutId, parent, false);
             }
 
-            TextView header = (TextView)convertView.findViewById(R.id.header);
-            header.setText(mSectionHeaders.get(position));
+            TextView title = (TextView)convertView.findViewById(R.id.title);
+            title.setText(mSections.get(position).mIdentifier);
         } else {
             convertView = mUnderlyingAdapter.getView(getInternalPosition(position), convertView, parent);
         }
@@ -90,11 +106,31 @@ public class SectionAdapter<TItem,
     }
 
     /**
+     * Setup the header parameters
+     * @param layoutId the layout id used to inflate
+     * @param enabled whether clicking is enabled on the header
+     */
+    public void setupHeaderParameters(int layoutId, boolean enabled) {
+        mHeaderLayoutId = layoutId;
+        mHeaderEnabled = enabled;
+    }
+
+    /**
+     * Setup the footer parameters
+     * @param layoutId the layout id used to inflate
+     * @param enabled whether clicking is enabled on the footer
+     */
+    public void setupFooterParameters(int layoutId, boolean enabled) {
+        mFooterLayoutId = layoutId;
+        mFooterEnabled = enabled;
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
     public int getCount() {
-        return mSectionHeaders.size() + mUnderlyingAdapter.getCount();
+        return mSections.size() + mUnderlyingAdapter.getCount();
     }
 
     /**
@@ -102,8 +138,8 @@ public class SectionAdapter<TItem,
      */
     @Override
     public Object getItem(int position) {
-        if (isSectionHeader(position)) {
-            return mSectionHeaders.get(position);
+        if (isSection(position)) {
+            return mSections.get(position);
         }
 
         return mUnderlyingAdapter.getItem(getInternalPosition(position));
@@ -115,7 +151,7 @@ public class SectionAdapter<TItem,
      * @return the underlying item or null if a section header is queried
      */
     public TItem getTItem(int position) {
-        if (isSectionHeader(position)) {
+        if (isSection(position)) {
             return null;
         }
 
@@ -146,6 +182,9 @@ public class SectionAdapter<TItem,
         if (isSectionHeader(position)) {
             // use the last view type id as the section header
             return getViewTypeCount() - 1;
+        } else if (isSectionFooter(position)) {
+            // use the last view type id as the section header
+            return getViewTypeCount() - 2;
         }
 
         return mUnderlyingAdapter.getItemViewType(getInternalPosition(position));
@@ -156,8 +195,8 @@ public class SectionAdapter<TItem,
      */
     @Override
     public int getViewTypeCount() {
-        // increment view type count by 1 for section headers
-        return mUnderlyingAdapter.getViewTypeCount() + 1;
+        // increment view type count by 2 for section headers and section footers
+        return mUnderlyingAdapter.getViewTypeCount() + 2;
     }
 
     /**
@@ -185,8 +224,13 @@ public class SectionAdapter<TItem,
      */
     @Override
     public boolean isEnabled(int position) {
-        // don't enable clicking/long press for section headers
-        return !isSectionHeader(position);
+        if (isSectionHeader(position)) {
+            return mHeaderEnabled;
+        } else if (isSectionFooter(position)) {
+            return mFooterEnabled;
+        }
+
+        return true;
     }
 
     /**
@@ -202,8 +246,26 @@ public class SectionAdapter<TItem,
      * @param position position in the overall lis
      * @return true if a section header
      */
-    private boolean isSectionHeader(int position) {
-        return mSectionHeaders.containsKey(position);
+    public boolean isSectionHeader(int position) {
+        return mSections.containsKey(position) && mSections.get(position).mType == SectionType.Header;
+    }
+
+    /**
+     * Determines whether the item at the position is a section footer
+     * @param position position in the overall lis
+     * @return true if a section footer
+     */
+    public boolean isSectionFooter(int position) {
+        return mSections.containsKey(position) && mSections.get(position).mType == SectionType.Footer;
+    }
+
+    /**
+     * Determines whether the item at the position is a section of some type
+     * @param position position in the overall lis
+     * @return true if the item is a section
+     */
+    public boolean isSection(int position) {
+        return mSections.containsKey(position);
     }
 
     /**
@@ -213,13 +275,13 @@ public class SectionAdapter<TItem,
      * @return the internal position
      */
     public int getInternalPosition(int position) {
-        if (isSectionHeader(position)) {
+        if (isSection(position)) {
             return -1;
         }
 
         int countSectionHeaders = 0;
 
-        for (Integer sectionPosition : mSectionHeaders.keySet()) {
+        for (Integer sectionPosition : mSections.keySet()) {
             if (sectionPosition <= position) {
                 countSectionHeaders++;
             } else {
@@ -237,7 +299,7 @@ public class SectionAdapter<TItem,
      */
     public int getExternalPosition(int internalPosition) {
         int externalPosition = internalPosition;
-        for (Integer sectionPosition : mSectionHeaders.keySet()) {
+        for (Integer sectionPosition : mSections.keySet()) {
             // because the section headers are tracking the 'merged' lists, we need to keep bumping
             // our position for each found section header
             if (sectionPosition <= externalPosition) {
@@ -257,10 +319,10 @@ public class SectionAdapter<TItem,
     public void setData(SectionListContainer<TItem> data) {
         mUnderlyingAdapter.unload();
 
-        if (data.mSectionIndices == null) {
-            mSectionHeaders.clear();
+        if (data.mSections == null) {
+            mSections.clear();
         } else {
-            mSectionHeaders = data.mSectionIndices;
+            mSections = data.mSections;
         }
 
         mUnderlyingAdapter.addAll(data.mListResults);
@@ -288,7 +350,7 @@ public class SectionAdapter<TItem,
 
     public void clear() {
         mUnderlyingAdapter.clear();
-        mSectionHeaders.clear();
+        mSections.clear();
     }
 
     /**
index 9e5a5f5..36fe74c 100644 (file)
@@ -56,12 +56,12 @@ public class SectionCreator<T> extends WrappedAsyncTaskLoader<SectionListContain
     @Override
     public SectionListContainer<T> loadInBackground() {
         List<T> results = mLoader.loadInBackground();
-        TreeMap<Integer, String> sectionHeaders = null;
+        TreeMap<Integer, SectionCreatorUtils.Section> sections = null;
 
         if (mComparator != null) {
-            sectionHeaders = SectionCreatorUtils.createSections(results, mComparator);
+            sections = SectionCreatorUtils.createSections(results, mComparator);
         }
 
-        return new SectionListContainer<T>(sectionHeaders, results);
+        return new SectionListContainer<T>(sections, results);
     }
 }
index 56eb169..a5fe678 100644 (file)
@@ -3,6 +3,8 @@
  */
 package com.cyngn.eleven.sectionadapter;
 
+import com.cyngn.eleven.utils.SectionCreatorUtils;
+
 import java.util.List;
 import java.util.TreeMap;
 
@@ -11,11 +13,12 @@ import java.util.TreeMap;
  * @param <T> the type of item that the list contains
  */
 public class SectionListContainer<T> {
-    public TreeMap<Integer, String> mSectionIndices;
+    public TreeMap<Integer, SectionCreatorUtils.Section> mSections;
     public List<T> mListResults;
 
-    public SectionListContainer(TreeMap<Integer, String> sectionIndices, List<T> results) {
-        mSectionIndices = sectionIndices;
+    public SectionListContainer(final TreeMap<Integer, SectionCreatorUtils.Section> sections,
+                                final List<T> results) {
+        mSections = sections;
         mListResults = results;
     }
 }
index b6dc98f..24f451a 100644 (file)
@@ -120,7 +120,7 @@ public abstract class BaseActivity extends FragmentActivity implements ServiceCo
         // Initialize the broadcast receiver
         mPlaybackStatus = new PlaybackStatus(this);
 
-        getActionBar().setTitle(getString(R.string.app_name_uppercase));
+        getActionBar().setTitle(getString(R.string.app_name).toUpperCase());
 
         // Set the layout
         setContentView(setContentView());
index 79f0671..aede621 100644 (file)
@@ -16,8 +16,10 @@ import android.app.SearchManager;
 import android.app.SearchableInfo;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.ServiceConnection;
 import android.database.Cursor;
+import android.graphics.Bitmap;
 import android.media.AudioManager;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -31,16 +33,20 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.AbsListView;
 import android.widget.AbsListView.OnScrollListener;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.SearchView;
 import android.widget.SearchView.OnQueryTextListener;
 import android.widget.TextView;
 
+import com.cyngn.eleven.Config;
 import com.cyngn.eleven.IElevenService;
 import com.cyngn.eleven.R;
 import com.cyngn.eleven.adapters.SummarySearchAdapter;
@@ -111,6 +117,17 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
     private SectionAdapter<SearchResult, SummarySearchAdapter> mAdapter;
 
     /**
+     * boolean tracking whether this is the search level when the user first enters search
+     * or if the user has clicked show all
+     */
+    private boolean mTopLevelSearch;
+
+    /**
+     * If the user has clicked show all, this tells us what type (Artist, Album, etc)
+     */
+    private ResultType mSearchType;
+
+    /**
      * {@inheritDoc}
      */
     @Override
@@ -123,11 +140,6 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
         // Control the media volume
         setVolumeControlStream(AudioManager.STREAM_MUSIC);
 
-        // Theme the action bar
-        final ActionBar actionBar = getActionBar();
-        actionBar.setTitle(getString(R.string.app_name_uppercase));
-        actionBar.setHomeButtonEnabled(true);
-
         // Bind Apollo's service
         mToken = MusicUtils.bindToService(this, this);
 
@@ -137,14 +149,13 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
         // get the input method manager
         mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
 
-        // Get the query String
-        mFilterString = getIntent().getStringExtra(SearchManager.QUERY);
-
         // Initialize the adapter
         SummarySearchAdapter adapter = new SummarySearchAdapter(this);
         mAdapter = new SectionAdapter<SearchResult, SummarySearchAdapter>(this, adapter);
         // Set the prefix
         mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
+        mAdapter.setupHeaderParameters(R.layout.list_search_header, false);
+        mAdapter.setupFooterParameters(R.layout.list_search_footer, true);
 
         // setup the no results container
         mNoResultsContainer = (NoResultsContainer)findViewById(R.id.no_results_container);
@@ -153,13 +164,50 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
 
         initListView();
 
+        // Theme the action bar
+        final ActionBar actionBar = getActionBar();
+        actionBar.setHomeButtonEnabled(true);
+        actionBar.setIcon(R.drawable.ic_action_search);
+
+        // Get the query String
+        mFilterString = getIntent().getStringExtra(SearchManager.QUERY);
+
+        // if we have a non-empty search string, this is a 2nd lvl search
         if (mFilterString != null && !mFilterString.isEmpty()) {
+            mTopLevelSearch = false;
+
+            // get the search type to filter by
+            int type = getIntent().getIntExtra(SearchManager.SEARCH_MODE, -1);
+            if (type >= 0 && type < ResultType.values().length) {
+                mSearchType = ResultType.values()[type];
+            }
+
+            int resourceId = 0;
+            switch (mSearchType) {
+                case Artist:
+                    resourceId = R.string.search_title_artists;
+                    break;
+                case Album:
+                    resourceId = R.string.search_title_albums;
+                    break;
+                case Playlist:
+                    resourceId = R.string.search_title_playlists;
+                    break;
+                case Song:
+                    resourceId = R.string.search_title_songs;
+                    break;
+            }
+            actionBar.setTitle(getString(resourceId, mFilterString).toUpperCase());
+            actionBar.setDisplayHomeAsUpEnabled(true);
+
+            // Set the prefix
+            mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
+
             // Prepare the loader. Either re-connect with an existing one,
             // or start a new one.
             getSupportLoaderManager().initLoader(0, null, this);
-
-            // Action bar subtitle
-            getActionBar().setSubtitle("\"" + mFilterString + "\"");
+        } else {
+            mTopLevelSearch = true;
         }
 
         // set the background on the root view
@@ -194,8 +242,16 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
      */
     @Override
     public Loader<SectionListContainer<SearchResult>> onCreateLoader(final int id, final Bundle args) {
-        IItemCompare<SearchResult> comparator = SectionCreatorUtils.createSearchResultComparison(this);
-        return new SectionCreator<SearchResult>(this, new SummarySearchLoader(this, mFilterString),
+        IItemCompare<SearchResult> comparator = null;
+
+        // if we are at the top level, create a comparator to separate the different types into
+        // their own sections (artists, albums, etc)
+        if (mTopLevelSearch) {
+            comparator = SectionCreatorUtils.createSearchResultComparison(this);
+        }
+
+        return new SectionCreator<SearchResult>(this,
+                new SummarySearchLoader(this, mFilterString, mSearchType),
                 comparator);
     }
 
@@ -204,6 +260,11 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
      */
     @Override
     public boolean onCreateOptionsMenu(final Menu menu) {
+        // if we are not a top level search view, we do not need to create the search fields
+        if (!mTopLevelSearch) {
+            return super.onCreateOptionsMenu(menu);
+        }
+
         // Search view
         getMenuInflater().inflate(R.menu.search, menu);
 
@@ -211,6 +272,17 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
         MenuItem searchItem = menu.findItem(R.id.menu_search);
         mSearchView = (SearchView)searchItem.getActionView();
         mSearchView.setOnQueryTextListener(this);
+        mSearchView.setQueryHint(getString(R.string.searchHint).toUpperCase());
+
+        // The SearchView has no way for you to customize or get access to the search icon in a
+        // normal fashion, so we need to manually look for the icon and change the
+        // layout params to hide it
+        mSearchView.setIconifiedByDefault(false);
+        mSearchView.setIconified(false);
+        int searchButtonId = getResources().getIdentifier("android:id/search_mag_icon", null, null);
+        ImageView searchIcon = (ImageView)mSearchView.findViewById(searchButtonId);
+        searchIcon.setLayoutParams(new LinearLayout.LayoutParams(0, 0));
+
         searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
             @Override
             public boolean onMenuItemActionExpand(MenuItem item) {
@@ -224,14 +296,8 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
             }
         });
 
-        if (mFilterString == null || mFilterString.isEmpty()) {
-            menu.findItem(R.id.menu_search).expandActionView();
-        }
+        menu.findItem(R.id.menu_search).expandActionView();
 
-        // Add voice search
-        final SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
-        final SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
-        mSearchView.setSearchableInfo(searchableInfo);
         return super.onCreateOptionsMenu(menu);
     }
 
@@ -289,9 +355,6 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
                                final SectionListContainer<SearchResult> data) {
         // Check for any errors
         if (data.mListResults.isEmpty()) {
-            // Set the empty text
-            mNoResultsContainer.setMainHighlightText("\"" + mFilterString + "\"");
-
             // clear the adapter
             mAdapter.clear();
             mListView.setVisibility(View.INVISIBLE);
@@ -335,14 +398,11 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
     public boolean onQueryTextSubmit(final String query) {
         if (TextUtils.isEmpty(query)) {
             mFilterString = "";
-            getActionBar().setSubtitle("");
             return false;
         }
 
         hideInputManager();
 
-        // Action bar subtitle
-        getActionBar().setSubtitle("\"" + mFilterString + "\"");
         return true;
     }
 
@@ -385,24 +445,34 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
     @Override
     public void onItemClick(final AdapterView<?> parent, final View view, final int position,
             final long id) {
-        SearchResult item = mAdapter.getTItem(position);
-        switch (item.mType) {
-            case Artist:
-                NavUtils.openArtistProfile(this, item.mArtist);
-                break;
-            case Album:
-                NavUtils.openAlbumProfile(this, item.mAlbum, item.mArtist, item.mId);
-                break;
-            case Playlist:
-                NavUtils.openPlaylist(this, item.mId, item.mTitle);
-                break;
-            case Song:
-                // If it's a song, play it and leave
-                final long[] list = new long[]{
-                        item.mId
-                };
-                MusicUtils.playAll(this, list, 0, false);
-                break;
+        if (mAdapter.isSectionFooter(position)) {
+            // since a footer should be after a list item by definition, let's look up the type
+            // of the previous item
+            SearchResult item = mAdapter.getTItem(position - 1);
+            Intent intent = new Intent(this, SearchActivity.class);
+            intent.putExtra(SearchManager.QUERY, mFilterString);
+            intent.putExtra(SearchManager.SEARCH_MODE, item.mType.ordinal());
+            startActivity(intent);
+        } else {
+            SearchResult item = mAdapter.getTItem(position);
+            switch (item.mType) {
+                case Artist:
+                    NavUtils.openArtistProfile(this, item.mArtist);
+                    break;
+                case Album:
+                    NavUtils.openAlbumProfile(this, item.mAlbum, item.mArtist, item.mId);
+                    break;
+                case Playlist:
+                    NavUtils.openPlaylist(this, item.mId, item.mTitle);
+                    break;
+                case Song:
+                    // If it's a song, play it and leave
+                    final long[] list = new long[]{
+                            item.mId
+                    };
+                    MusicUtils.playAll(this, list, 0, false);
+                    break;
+            }
         }
     }
 
@@ -426,23 +496,119 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
      * This class loads a search result summary of items
      */
     private static final class SummarySearchLoader extends SimpleListLoader<SearchResult> {
-        private static final int NUM_RESULTS_TO_GET = 3;
-
         private final String mQuery;
+        private final ResultType mSearchType;
 
-        public SummarySearchLoader(final Context context, final String query) {
+        public SummarySearchLoader(final Context context, final String query,
+                                   final ResultType searchType) {
             super(context);
             mQuery = query;
+            mSearchType = searchType;
+        }
+
+        /**
+         * This creates a search result given the data at the cursor position
+         * @param cursor at the position for the item
+         * @param type the type of item to create
+         * @return the search result
+         */
+        protected SearchResult createSearchResult(final Cursor cursor, ResultType type) {
+            SearchResult item = null;
+
+            switch (type) {
+                case Playlist:
+                    item = SearchResult.createPlaylistResult(cursor);
+                    item.mSongCount = MusicUtils.getSongCountForPlaylist(getContext(), item.mId);
+                    break;
+                case Song:
+                    item = SearchResult.createSearchResult(cursor);
+                    if (item != null) {
+                        AlbumArtistDetails details = MusicUtils.getAlbumArtDetails(getContext(), item.mId);
+                        if (details != null) {
+                            item.mArtist = details.mArtistName;
+                            item.mAlbum = details.mAlbumName;
+                            item.mAlbumId = details.mAlbumId;
+                        }
+                    }
+                    break;
+                case Album:
+                case Artist:
+                default:
+                    item = SearchResult.createSearchResult(cursor);
+                    break;
+            }
+
+            return item;
         }
 
         @Override
         public List<SearchResult> loadInBackground() {
+            // if we are doing a specific type search, run that one
+            if (mSearchType != null && mSearchType != ResultType.Unknown) {
+                return runSearchForType();
+            }
+
+            return runGenericSearch();
+        }
+
+        /**
+         * This creates a search for a specific type given a filter string.  This will return the
+         * full list of results that matches those two requirements
+         * @return the results for that search
+         */
+        protected List<SearchResult> runSearchForType() {
+            ArrayList<SearchResult> results = new ArrayList<SearchResult>();
+            Cursor cursor = null;
+            try {
+                if (mSearchType == ResultType.Playlist) {
+                    cursor = makePlaylistSearchCursor(getContext(), mQuery);
+                } else {
+                    cursor = ApolloUtils.createSearchQueryCursor(getContext(), mQuery);
+                }
+
+                // pre-cache this index
+                final int mimeTypeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE);
+
+                if (cursor != null && cursor.moveToFirst()) {
+                    do {
+                        boolean addResult = true;
+
+                        if (mSearchType != ResultType.Playlist) {
+                            // get the result type
+                            ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
+                            if (type != mSearchType) {
+                                addResult = false;
+                            }
+                        }
+
+                        if (addResult) {
+                            results.add(createSearchResult(cursor, mSearchType));
+                        }
+                    } while (cursor.moveToNext());
+                }
+
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                    cursor = null;
+                }
+            }
+
+            return results;
+        }
+
+        /**
+         * This will run a search given a filter string and return the top NUM_RESULTS_TO_GET per
+         * type
+         * @return the results for that search
+         */
+        public List<SearchResult> runGenericSearch() {
             ArrayList<SearchResult> results = new ArrayList<SearchResult>();
             // number of types to query for
             final int numTypes = ResultType.getNumTypes();
 
             // number of results we want
-            final int numResultsNeeded = NUM_RESULTS_TO_GET * numTypes;
+            final int numResultsNeeded = Config.SEARCH_NUM_RESULTS_TO_GET * numTypes;
 
             // current number of results we have
             int numResultsAdded = 0;
@@ -455,21 +621,17 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
             if (playlistCursor != null && playlistCursor.moveToFirst()) {
                 do {
                     // create the item
-                    SearchResult item = SearchResult.createPlaylistResult(playlistCursor);
-                    if (item != null) {
-                        // get the song count
-                        item.mSongCount = MusicUtils.getSongCountForPlaylist(getContext(), item.mId);
-
-                        /// add the results
-                        numResultsAdded++;
-                        results.add(item);
-                    }
-                } while (playlistCursor.moveToNext() && numResultsAdded < NUM_RESULTS_TO_GET);
+                    SearchResult item = createSearchResult(playlistCursor, ResultType.Playlist);
+                    /// add the results
+                    numResultsAdded++;
+                    results.add(item);
+                } while (playlistCursor.moveToNext()
+                        && numResultsAdded < Config.SEARCH_NUM_RESULTS_TO_GET);
 
                 // because we deal with playlists separately,
                 // just mark that we have the full # of playlists
                 // so that logic later can quit out early if full
-                numResultsAdded = NUM_RESULTS_TO_GET;
+                numResultsAdded = Config.SEARCH_NUM_RESULTS_TO_GET;
 
                 // close the cursor
                 playlistCursor.close();
@@ -489,20 +651,11 @@ public class SearchActivity extends FragmentActivity implements LoaderCallbacks<
                     ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
 
                     // if we still need this type
-                    if (numOfEachType[type.ordinal()] < NUM_RESULTS_TO_GET) {
+                    if (numOfEachType[type.ordinal()] < Config.SEARCH_NUM_RESULTS_TO_GET) {
                         // get the search result
-                        SearchResult item = SearchResult.createSearchResult(cursor);
-                        if (item != null) {
-                            if (item.mType == ResultType.Song) {
-                                // get the album art details
-                                AlbumArtistDetails details = MusicUtils.getAlbumArtDetails(getContext(), item.mId);
-                                if (details != null) {
-                                    item.mArtist = details.mArtistName;
-                                    item.mAlbum = details.mAlbumName;
-                                    item.mAlbumId = details.mAlbumId;
-                                }
-                            }
+                        SearchResult item = createSearchResult(cursor, type);
 
+                        if (item != null) {
                             // add it
                             results.add(item);
                             numOfEachType[type.ordinal()]++;
index 683c0f4..62e102a 100644 (file)
@@ -5,6 +5,7 @@ package com.cyngn.eleven.utils;
 
 import android.content.Context;
 
+import com.cyngn.eleven.Config;
 import com.cyngn.eleven.R;
 import com.cyngn.eleven.model.Album;
 import com.cyngn.eleven.model.Artist;
@@ -19,44 +20,95 @@ import java.util.TreeMap;
  * a section should be created
  */
 public class SectionCreatorUtils {
+    public enum SectionType {
+        Header,
+        Footer
+    }
+
+    public static class Section {
+        public SectionType mType;
+        public String mIdentifier;
+
+        public Section(final SectionType type, final String identifier) {
+            mType = type;
+            mIdentifier = identifier;
+        }
+    }
+
     /**
      * Interface to compare two items and create labels
      * @param <T> type of item to compare
      */
-    public static interface IItemCompare<T> {
+    public static class IItemCompare<T> {
         /**
          * Compares to items and returns a section divider T if there should
          * be a section divider between first and second
          * @param first the first element in the list.  If null, it is checking to see
          *              if we need a divider at the beginning of the list
          * @param second the second element in the list.
+         * @param items the source list of items that we are creating headers from
+         * @param firstIndex index of the first item we are looking at
          * @return String the expected separator label or null if none
          */
-        public String createSectionSeparator(T first, T second);
+        public String createSectionHeader(T first, T second, List<T> items, int firstIndex) {
+            return createSectionHeader(first, second);
+        }
+
+        public String createSectionHeader(T first, T second) {
+            return null;
+        }
+
+        /**
+         * Compares to items and returns a section divider T if there should
+         * be a section divider between first and second
+         * @param first the first element in the list.
+         * @param second the second element in the list. If null, it is checking to see if we need
+         *               a divider at the end of the list
+         * @param items the source list of items that we are creating footers from
+         * @param firstIndex index of the first item we are looking at
+         * @return String the expected separator label or null if none
+         */
+        public String createSectionFooter(T first, T second, List<T> items, int firstIndex) {
+            return createSectionFooter(first, second);
+        }
+
+        public String createSectionFooter(T first, T second) {
+            return null;
+        }
 
         /**
          * Returns the section label that corresponds to this item
          * @param item the item
          * @return the section label that this label falls under
          */
-        public String createLabel(T item);
-    }
+        public String createHeaderLabel(T item) {
+            return null;
+        }
 
+        /**
+         * Returns the section label that corresponds to this item
+         * @param item the item
+         * @return the section label that this label falls under
+         */
+        public String createFooterLabel(T item) {
+            return null;
+        }
+    }
 
     /**
      * A localized String comparison implementation of IItemCompare
      * @param <T> the type of item to compare
      */
-    public static abstract class LocalizedCompare<T> implements IItemCompare<T> {
+    public static abstract class LocalizedCompare<T> extends IItemCompare<T> {
         @Override
-        public String createSectionSeparator(T first, T second) {
-            String secondLabel = createLabel(second);
+        public String createSectionHeader(T first, T second) {
+            String secondLabel = createHeaderLabel(second);
             // if we can't determine a good label then don't bother creating a section
             if (secondLabel == null) {
                 return null;
             }
 
-            if (first == null || !secondLabel.equals(createLabel(first))) {
+            if (first == null || !secondLabel.equals(createHeaderLabel(first))) {
                 return secondLabel;
             }
 
@@ -64,7 +116,7 @@ public class SectionCreatorUtils {
         }
 
         @Override
-        public String createLabel(T item) {
+        public String createHeaderLabel(T item) {
             return MusicUtils.getLocalizedBucketLetter(getString(item), trimName());
         }
 
@@ -83,18 +135,18 @@ public class SectionCreatorUtils {
      * A simple int comparison implementation of IItemCompare
      * @param <T> the type of item to compare
      */
-    public static abstract class IntCompare<T> implements IItemCompare<T> {
+    public static abstract class IntCompare<T> extends IItemCompare<T> {
         @Override
-        public String createSectionSeparator(T first, T second) {
+        public String createSectionHeader(T first, T second) {
             if (first == null || getInt(first) != getInt(second)) {
-                return createLabel(second);
+                return createHeaderLabel(second);
             }
 
             return null;
         }
 
         @Override
-        public String createLabel(T item) {
+        public String createHeaderLabel(T item) {
             return String.valueOf(getInt(item));
         }
 
@@ -117,7 +169,7 @@ public class SectionCreatorUtils {
         protected abstract int getStringId(int value);
 
         @Override
-        public String createSectionSeparator(T first, T second) {
+        public String createSectionHeader(T first, T second) {
             int secondStringId = getStringId(getInt(second));
             if (first == null || getStringId(getInt(first)) != secondStringId) {
                 return createLabel(secondStringId, second);
@@ -131,7 +183,7 @@ public class SectionCreatorUtils {
         }
 
         @Override
-        public String createLabel(T item) {
+        public String createHeaderLabel(T item) {
             return createLabel(getStringId(getInt(item)), item);
         }
     }
@@ -217,7 +269,7 @@ public class SectionCreatorUtils {
         }
 
         @Override
-        public String createSectionSeparator(T first, T second) {
+        public String createSectionHeader(T first, T second) {
             boolean returnSeparator = false;
             if (first == null) {
                 returnSeparator = true;
@@ -226,14 +278,14 @@ public class SectionCreatorUtils {
                 // not greater than 5 albums
                 int firstInt = getInt(first);
                 int secondInt = getInt(second);
-                if (firstInt != secondInt && 
+                if (firstInt != secondInt &&
                         !(firstInt >= 5 && secondInt >= 5)) {
                     returnSeparator = true;
                 }
             }
 
             if (returnSeparator) {
-                return createLabel(second);
+                return createHeaderLabel(second);
             }
 
             return null;
@@ -256,22 +308,34 @@ public class SectionCreatorUtils {
      * @param <T> the type of item to compare
      * @return Creates a TreeMap of indices (if the headers were part of the list) to section labels
      */
-    public static <T> TreeMap<Integer, String> createSections(final List<T> list,
+    public static <T> TreeMap<Integer, Section> createSections(final List<T> list,
                                                               final IItemCompare<T> comparator) {
         if (list != null && list.size() > 0) {
-            TreeMap<Integer, String> sectionHeaders = new TreeMap<Integer, String>();
-            for (int i = 0; i < list.size(); i++) {
+            TreeMap<Integer, Section> sections = new TreeMap<Integer, Section>();
+            for (int i = 0; i < list.size() + 1; i++) {
                 T first = (i == 0 ? null : list.get(i - 1));
-                T second = list.get(i);
+                T second = (i == list.size() ? null : list.get(i));
+
+                // create the footer first because if we need both it should be footer,header,item
+                // not header,footer,item
+                if (first != null) {
+                    String footer = comparator.createSectionFooter(first, second, list, i - 1);
+                    if (footer != null) {
+                        // add sectionHeaders.size() to store the indices of the combined list
+                        sections.put(sections.size() + i, new Section(SectionType.Footer, footer));
+                    }
+                }
 
-                String separator = comparator.createSectionSeparator(first, second);
-                if (separator != null) {
-                    // add sectionHeaders.size() to store the indices of the combined list
-                    sectionHeaders.put(sectionHeaders.size() + i, separator);
+                if (second != null) {
+                    String header = comparator.createSectionHeader(first, second, list, i - 1);
+                    if (header != null) {
+                        // add sectionHeaders.size() to store the indices of the combined list
+                        sections.put(sections.size() + i, new Section(SectionType.Header, header));
+                    }
                 }
             }
 
-            return sectionHeaders;
+            return sections;
         }
 
         return null;
@@ -371,7 +435,7 @@ public class SectionCreatorUtils {
                 }
 
                 @Override
-                public String createLabel(Album item) {
+                public String createHeaderLabel(Album item) {
                     if (MusicUtils.isInvalidYear(getInt(item))) {
                         return context.getString(R.string.header_unknown_year);
                     }
@@ -438,14 +502,14 @@ public class SectionCreatorUtils {
                 }
 
                 @Override
-                public String createLabel(Song item) {
+                public String createHeaderLabel(Song item) {
                     // I have seen tracks in my library where it would return 0 or 2
                     // so have this check to return a more friendly label in that case
                     if (MusicUtils.isInvalidYear(item.mYear)) {
                         return context.getString(R.string.header_unknown_year);
                     }
 
-                    return super.createLabel(item);
+                    return super.createHeaderLabel(item);
                 }
             };
         }
@@ -462,16 +526,16 @@ public class SectionCreatorUtils {
         return new IItemCompare<SearchResult>() {
 
             @Override
-            public String createSectionSeparator(SearchResult first, SearchResult second) {
+            public String createSectionHeader(SearchResult first, SearchResult second) {
                 if (first == null || first.mType != second.mType) {
-                    return createLabel(second);
+                    return createHeaderLabel(second);
                 }
 
                 return null;
             }
 
             @Override
-            public String createLabel(SearchResult item) {
+            public String createHeaderLabel(SearchResult item) {
                 switch (item.mType) {
                     case Artist:
                         return context.getString(R.string.page_artists);
@@ -485,6 +549,45 @@ public class SectionCreatorUtils {
 
                 return null;
             }
+
+            @Override
+            public String createSectionFooter(SearchResult first, SearchResult second,
+                                              List<SearchResult> items, int firstIndex) {
+                if (second == null ||
+                        (first != null && first.mType != second.mType)) {
+                    // if we don't have SEARCH_NUM_RESULTS_TO_GET # of the same type of items
+                    // then we don't have enough to show the footer.  For example, if we show 5
+                    // items but only the last 2 items are artists, that means we only have 2
+                    // so there is no point in showing the "Show All" footer
+                    // We start from 1 because we don't need to count
+                    // the first item itself
+                    for (int i = 1; i < Config.SEARCH_NUM_RESULTS_TO_GET; i++) {
+                        if (firstIndex - i < 0 || items.get(firstIndex - i).mType != first.mType) {
+                            return null;
+                        }
+                    }
+
+                    return createFooterLabel(first);
+                }
+
+                return null;
+            }
+
+            @Override
+            public String createFooterLabel(SearchResult item) {
+                switch (item.mType) {
+                    case Artist:
+                        return context.getString(R.string.footer_search_artists);
+                    case Album:
+                        return context.getString(R.string.footer_search_albums);
+                    case Song:
+                        return context.getString(R.string.footer_search_songs);
+                    case Playlist:
+                        return context.getString(R.string.footer_search_playlists);
+                }
+
+                return null;
+            }
         };
     }
 }