Merge "Support4Demo: Add sample usage of MediaBrowser pagination API"
diff --git a/samples/Support4Demos/res/layout/fragment_list.xml b/samples/Support4Demos/res/layout/fragment_list.xml
index c169fec..904ec1a 100644
--- a/samples/Support4Demos/res/layout/fragment_list.xml
+++ b/samples/Support4Demos/res/layout/fragment_list.xml
@@ -54,7 +54,6 @@
     <ListView
         android:id="@+id/list_view"
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
-    </ListView>
+        android:layout_height="match_parent"/>
 
-</LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java b/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java
index 2ee7622..765dc88 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java
@@ -26,6 +26,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.AbsListView;
 import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.ImageView;
@@ -44,7 +45,7 @@
  * <p/>
  * It uses a {@link MediaBrowserCompat} to connect to the {@link MediaBrowserServiceSupport}.
  * Once connected, the fragment subscribes to get all the children. All
- * {@link MediaBrowserCompat.MediaItem}'s that can be browsed are shown in a ListView.
+ * {@link MediaBrowserCompat.MediaItem} objects that can be browsed are shown in a ListView.
  */
 public class BrowseFragment extends Fragment {
 
@@ -52,27 +53,68 @@
 
     public static final String ARG_MEDIA_ID = "media_id";
 
+    // The number of media items per page.
+    private static final int PAGE_SIZE = 6;
+
     public static interface FragmentDataHelper {
         void onMediaItemSelected(MediaBrowserCompat.MediaItem item);
     }
 
     // The mediaId to be used for subscribing for children using the MediaBrowser.
     private String mMediaId;
+    private final List<MediaBrowserCompat.MediaItem> mMediaItems = new ArrayList<>();
 
+    private boolean mCanLoadNewPage;
     private MediaBrowserCompat mMediaBrowser;
     private BrowseAdapter mBrowserAdapter;
 
     private MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback =
             new MediaBrowserCompat.SubscriptionCallback() {
+        @Override
+        public void onChildrenLoaded(String parentId, List<MediaBrowserCompat.MediaItem> children,
+                Bundle options) {
+            int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
+            int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
+            if (page < 0 || pageSize != PAGE_SIZE || children == null
+                    || children.size() > PAGE_SIZE) {
+                return;
+            }
+
+            int itemIndex = page * PAGE_SIZE;
+            if (itemIndex >= mMediaItems.size()) {
+                if (children.size() == 0) {
+                    return;
+                }
+                // An additional page is loaded.
+                mMediaItems.addAll(children);
+            } else {
+                // An existing page is replaced by the newly loaded page.
+                for (MediaBrowserCompat.MediaItem item : children) {
+                    if (itemIndex < mMediaItems.size()) {
+                        mMediaItems.set(itemIndex, item);
+                    } else {
+                        mMediaItems.add(item);
+                    }
+                    itemIndex++;
+                }
+
+                // If the newly loaded page contains less than {PAGE_SIZE} items,
+                // then this page should be the last page.
+                if (children.size() < PAGE_SIZE) {
+                    while (mMediaItems.size() > itemIndex) {
+                        mMediaItems.remove(mMediaItems.size() - 1);
+                    }
+                }
+            }
+            mBrowserAdapter.notifyDataSetChanged();
+            mCanLoadNewPage = true;
+        }
 
         @Override
         public void onChildrenLoaded(String parentId, List<MediaBrowserCompat.MediaItem> children) {
-            Log.d(TAG, "onChildrenLoaded: " + parentId);
-            mBrowserAdapter.clear();
-            mBrowserAdapter.notifyDataSetInvalidated();
-            for (MediaBrowserCompat.MediaItem item : children) {
-                mBrowserAdapter.add(item);
-            }
+            Log.d(TAG, "onChildrenLoaded: parentId=" + parentId);
+            mMediaItems.clear();
+            mMediaItems.addAll(children);
             mBrowserAdapter.notifyDataSetChanged();
         }
 
@@ -89,10 +131,6 @@
         public void onConnected() {
             Log.d(TAG, "onConnected: session token " + mMediaBrowser.getSessionToken());
 
-            if (mMediaId == null) {
-                mMediaId = mMediaBrowser.getRoot();
-            }
-            mMediaBrowser.subscribe(mMediaId, mSubscriptionCallback);
             if (mMediaBrowser.getSessionToken() == null) {
                 throw new IllegalArgumentException("No Session token");
             }
@@ -104,6 +142,14 @@
                 Log.e(TAG, "Failed to create MediaController.", e);
             }
             ((MediaBrowserSupport) getActivity()).setMediaController(mediaController);
+
+            if (mMediaId == null) {
+                mMediaId = mMediaBrowser.getRoot();
+            }
+
+            if (mMediaItems.size() == 0) {
+                loadPage(0);
+            }
         }
 
         @Override
@@ -128,10 +174,10 @@
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
-                             Bundle savedInstanceState) {
+            Bundle savedInstanceState) {
         View rootView = inflater.inflate(R.layout.fragment_list, container, false);
 
-        mBrowserAdapter = new BrowseAdapter(getActivity());
+        mBrowserAdapter = new BrowseAdapter(getActivity(), mMediaItems);
 
         View controls = rootView.findViewById(R.id.controls);
         controls.setVisibility(View.GONE);
@@ -158,6 +204,22 @@
                 new ComponentName(getActivity(), MediaBrowserServiceSupport.class),
                 mConnectionCallback, null);
 
+        listView.setOnScrollListener(new AbsListView.OnScrollListener() {
+            @Override
+            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+                    int totalItemCount) {
+                if (mCanLoadNewPage && firstVisibleItem + visibleItemCount == totalItemCount) {
+                    mCanLoadNewPage = false;
+                    loadPage((mMediaItems.size() + PAGE_SIZE - 1) / PAGE_SIZE);
+                }
+            }
+
+            @Override
+            public void onScrollStateChanged(AbsListView view, int scrollState) {
+                // Do nothing
+            }
+        });
+
         return rootView;
     }
 
@@ -173,11 +235,18 @@
         mMediaBrowser.disconnect();
     }
 
-    // An adapter for showing the list of browsed MediaItem's
+    private void loadPage(int page) {
+        Bundle options = new Bundle();
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, PAGE_SIZE);
+        mMediaBrowser.subscribe(mMediaId, options, mSubscriptionCallback);
+    }
+
+    // An adapter for showing the list of browsed MediaItem objects
     private static class BrowseAdapter extends ArrayAdapter<MediaBrowserCompat.MediaItem> {
 
-        public BrowseAdapter(Context context) {
-            super(context, R.layout.media_list_item, new ArrayList<MediaBrowserCompat.MediaItem>());
+        public BrowseAdapter(Context context, List<MediaBrowserCompat.MediaItem> mediaItems) {
+            super(context, R.layout.media_list_item, mediaItems);
         }
 
         static class ViewHolder {
@@ -211,8 +280,10 @@
                 holder.mImageView.setImageDrawable(getContext().getResources()
                         .getDrawable(R.drawable.ic_play_arrow_white_24dp));
                 holder.mImageView.setVisibility(View.VISIBLE);
+            } else {
+                holder.mImageView.setVisibility(View.GONE);
             }
             return convertView;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/media/MediaBrowserServiceSupport.java b/samples/Support4Demos/src/com/example/android/supportv4/media/MediaBrowserServiceSupport.java
index 74ddd3f..a1d5bfa 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/media/MediaBrowserServiceSupport.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/media/MediaBrowserServiceSupport.java
@@ -25,6 +25,7 @@
 import android.os.Handler;
 import android.os.Message;
 import android.os.SystemClock;
+import android.support.v4.media.MediaBrowserCompat;
 import android.support.v4.media.MediaDescriptionCompat;
 import android.support.v4.media.MediaMetadataCompat;
 import android.support.v4.media.MediaBrowserCompat.MediaItem;
@@ -241,6 +242,12 @@
 
     @Override
     public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
+        onLoadChildren(parentMediaId, result, null);
+    }
+
+    @Override
+    public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result,
+            final Bundle options) {
         if (!mMusicProvider.isInitialized()) {
             // Use result.detach to allow calling result.sendResult from another thread:
             result.detach();
@@ -249,17 +256,16 @@
                 @Override
                 public void onMusicCatalogReady(boolean success) {
                     if (success) {
-                        loadChildrenImpl(parentMediaId, result);
+                        loadChildrenImpl(parentMediaId, result, options);
                     } else {
                         updatePlaybackState(getString(R.string.error_no_metadata));
                         result.sendResult(Collections.<MediaItem>emptyList());
                     }
                 }
             });
-
         } else {
             // If our music catalog is already loaded/cached, load them into result immediately
-            loadChildrenImpl(parentMediaId, result);
+            loadChildrenImpl(parentMediaId, result, options);
         }
     }
 
@@ -268,26 +274,49 @@
      * initialized.
      */
     private void loadChildrenImpl(final String parentMediaId,
-            final Result<List<MediaItem>> result) {
-        Log.d(TAG, "OnLoadChildren: parentMediaId=" + parentMediaId);
+            final Result<List<MediaItem>> result, final Bundle options) {
+        Log.d(TAG, "OnLoadChildren: parentMediaId=" + parentMediaId + ", options=" + options);
+
+        int page = -1;
+        int pageSize = -1;
+
+        if (options != null && (options.containsKey(MediaBrowserCompat.EXTRA_PAGE)
+                || options.containsKey(MediaBrowserCompat.EXTRA_PAGE_SIZE))) {
+            page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
+            pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
+
+            if (page < 0 || pageSize < 1) {
+                result.sendResult(new ArrayList<>());
+                return;
+            }
+        }
+
+        int fromIndex = page == -1 ? 0 : page * pageSize;
+        int toIndex = 0;
 
         List<MediaItem> mediaItems = new ArrayList<>();
 
         if (MEDIA_ID_ROOT.equals(parentMediaId)) {
             Log.d(TAG, "OnLoadChildren.ROOT");
-            mediaItems.add(new MediaItem(
-                    new MediaDescriptionCompat.Builder()
-                            .setMediaId(MEDIA_ID_MUSICS_BY_GENRE)
-                            .setTitle(getString(R.string.browse_genres))
-                            .setIconUri(Uri.parse("android.resource://" +
-                                    "com.example.android.supportv4.media/drawable/ic_by_genre"))
-                            .setSubtitle(getString(R.string.browse_genre_subtitle))
-                            .build(), MediaItem.FLAG_BROWSABLE
-            ));
+            if (page <= 0) {
+                mediaItems.add(new MediaItem(
+                        new MediaDescriptionCompat.Builder()
+                                .setMediaId(MEDIA_ID_MUSICS_BY_GENRE)
+                                .setTitle(getString(R.string.browse_genres))
+                                .setIconUri(Uri.parse("android.resource://" +
+                                        "com.example.android.supportv4.media/drawable/ic_by_genre"))
+                                .setSubtitle(getString(R.string.browse_genre_subtitle))
+                                .build(), MediaItem.FLAG_BROWSABLE));
+            }
 
         } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) {
             Log.d(TAG, "OnLoadChildren.GENRES");
-            for (String genre : mMusicProvider.getGenres()) {
+
+            List<String> genres = mMusicProvider.getGenres();
+            toIndex = page == -1 ? genres.size() : Math.min(fromIndex + pageSize, genres.size());
+
+            for (int i = fromIndex; i < toIndex; i++) {
+                String genre = genres.get(i);
                 MediaItem item = new MediaItem(
                         new MediaDescriptionCompat.Builder()
                                 .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE,
@@ -303,7 +332,13 @@
         } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) {
             String genre = MediaIDHelper.getHierarchy(parentMediaId)[1];
             Log.d(TAG, "OnLoadChildren.SONGS_BY_GENRE  genre=" + genre);
-            for (MediaMetadataCompat track : mMusicProvider.getMusicsByGenre(genre)) {
+
+            List<MediaMetadataCompat> tracks = mMusicProvider.getMusicsByGenre(genre);
+            toIndex = page == -1 ? tracks.size() : Math.min(fromIndex + pageSize, tracks.size());
+
+            for (int i = fromIndex; i < toIndex; i++) {
+                MediaMetadataCompat track = tracks.get(i);
+
                 // Since mediaMetadata fields are immutable, we need to create a copy, so we
                 // can set a hierarchy-aware mediaID. We will need to know the media hierarchy
                 // when we get a onPlayFromMusicID call, so we can create the proper queue based
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/media/model/MusicProvider.java b/samples/Support4Demos/src/com/example/android/supportv4/media/model/MusicProvider.java
index 777ca8d..a6eff2c 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/media/model/MusicProvider.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/media/model/MusicProvider.java
@@ -64,6 +64,7 @@
 
     // Categorized caches for music track data:
     private ConcurrentMap<String, List<MediaMetadataCompat>> mMusicListByGenre;
+    private List<String> mMusicGenres;
     private final ConcurrentMap<String, MutableMediaMetadata> mMusicListById;
 
     private final Set<String> mFavoriteTracks;
@@ -82,25 +83,25 @@
         mMusicListByGenre = new ConcurrentHashMap<>();
         mMusicListById = new ConcurrentHashMap<>();
         mFavoriteTracks = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
+        mMusicGenres = new ArrayList<>();
     }
 
     /**
-     * Get an iterator over the list of genres
+     * Get the list of genres
      *
      * @return genres
      */
-    public Iterable<String> getGenres() {
+    public List<String> getGenres() {
         if (mCurrentState != State.INITIALIZED) {
             return Collections.emptyList();
         }
-        return mMusicListByGenre.keySet();
+        return mMusicGenres;
     }
 
     /**
      * Get music tracks of the given genre
-     *
      */
-    public Iterable<MediaMetadataCompat> getMusicsByGenre(String genre) {
+    public List<MediaMetadataCompat> getMusicsByGenre(String genre) {
         if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) {
             return Collections.emptyList();
         }
@@ -199,7 +200,8 @@
     }
 
     private synchronized void buildListsByGenre() {
-        ConcurrentMap<String, List<MediaMetadataCompat>> newMusicListByGenre = new ConcurrentHashMap<>();
+        ConcurrentMap<String, List<MediaMetadataCompat>> newMusicListByGenre
+                = new ConcurrentHashMap<>();
 
         for (MutableMediaMetadata m : mMusicListById.values()) {
             String genre = m.metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE);
@@ -211,6 +213,7 @@
             list.add(m.metadata);
         }
         mMusicListByGenre = newMusicListByGenre;
+        mMusicGenres = new ArrayList<>(mMusicListByGenre.keySet());
     }
 
     private synchronized void retrieveMedia() {