MediaBrowserCompat: Support pagination on N devices

Bug: 23289404, Bug: 25564520
Change-Id: Ia129b551bac7c6e1448e1468fd78f233eea2b92d
diff --git a/v4/api21/android/support/v4/media/ServiceCallbacksAdapterApi21.java b/v4/api21/android/support/v4/media/ServiceCallbacksAdapterApi21.java
index a9136b1..4bd7dbb 100644
--- a/v4/api21/android/support/v4/media/ServiceCallbacksAdapterApi21.java
+++ b/v4/api21/android/support/v4/media/ServiceCallbacksAdapterApi21.java
@@ -17,6 +17,7 @@
 package android.support.v4.media;
 
 import android.media.session.MediaSession;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
diff --git a/v4/api24/android/support/v4/media/MediaBrowserCompatApi24.java b/v4/api24/android/support/v4/media/MediaBrowserCompatApi24.java
new file mode 100644
index 0000000..0b4f34b
--- /dev/null
+++ b/v4/api24/android/support/v4/media/MediaBrowserCompatApi24.java
@@ -0,0 +1,86 @@
+/*
+ * 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 android.support.v4.media;
+
+import android.media.browse.MediaBrowser;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.support.annotation.NonNull;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+
+class MediaBrowserCompatApi24 {
+    // TODO: Remove reflections once N released.
+    private static Method sSubscribeMethod;
+    private static Method sUnsubscribeMethod;
+    static {
+        try {
+            sSubscribeMethod = MediaBrowser.class.getMethod("subscribe", new Class[] {
+                    String.class, Bundle.class, MediaBrowser.SubscriptionCallback.class});
+            sUnsubscribeMethod = MediaBrowser.class.getMethod("unsubscribe",
+                    new Class[] {String.class, Bundle.class});
+        } catch (NoSuchMethodException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static Object createSubscriptionCallback(SubscriptionCallback callback) {
+        return new SubscriptionCallbackProxy<>(callback);
+    }
+
+    public static void subscribe(
+            Object browserObj, String parentId, Bundle options, Object subscriptionCallbackObj) {
+        try {
+            sSubscribeMethod.invoke(browserObj, parentId, options, subscriptionCallbackObj);
+        } catch (IllegalAccessException | InvocationTargetException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static void unsubscribe(Object browserObj, String parentId, Bundle options) {
+        try {
+            sUnsubscribeMethod.invoke(browserObj, parentId, options);
+        } catch (IllegalAccessException | InvocationTargetException e) {
+            e.printStackTrace();
+        }
+    }
+
+    interface SubscriptionCallback extends MediaBrowserCompatApi21.SubscriptionCallback {
+        void onChildrenLoaded(@NonNull String parentId, List<Parcel> children,
+                @NonNull Bundle options);
+        void onError(@NonNull String parentId, @NonNull  Bundle options);
+    }
+
+    static class SubscriptionCallbackProxy<T extends SubscriptionCallback>
+            extends MediaBrowserCompatApi21.SubscriptionCallbackProxy<T> {
+        public SubscriptionCallbackProxy(T callback) {
+            super(callback);
+        }
+
+        public void onChildrenLoaded(@NonNull String parentId,
+                List<MediaBrowser.MediaItem> children, @NonNull Bundle options) {
+            mSubscriptionCallback.onChildrenLoaded(
+                    parentId, itemListToParcelList(children), options);
+        }
+
+        public void onError(@NonNull String parentId, @NonNull Bundle options) {
+            mSubscriptionCallback.onError(parentId, options);
+        }
+    }
+}
diff --git a/v4/api24/android/support/v4/media/MediaBrowserServiceCompatApi24.java b/v4/api24/android/support/v4/media/MediaBrowserServiceCompatApi24.java
new file mode 100644
index 0000000..543776e
--- /dev/null
+++ b/v4/api24/android/support/v4/media/MediaBrowserServiceCompatApi24.java
@@ -0,0 +1,66 @@
+/*
+ * 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 android.support.v4.media;
+
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.RemoteException;
+
+import java.util.List;
+
+class MediaBrowserServiceCompatApi24 extends MediaBrowserServiceCompatApi23 {
+    public static Object createService() {
+        return new MediaBrowserServiceAdaptorApi24();
+    }
+
+    public interface ServiceImplApi24 extends ServiceImplApi23 {
+        void connect(String pkg, Bundle rootHints, ServiceCallbacksApi24 callbacks);
+        void addSubscription(String id, Bundle options, ServiceCallbacksApi24 callbacks);
+        void removeSubscription(String id, Bundle options, ServiceCallbacksApi24 callbacks);
+    }
+
+    public interface ServiceCallbacksApi24 extends ServiceCallbacksApi21 {
+        void onLoadChildren(String mediaId, List<Parcel> list, Bundle options)
+                throws RemoteException;
+    }
+
+    public static class ServiceCallbacksImplApi24 extends ServiceCallbacksImplApi21
+            implements ServiceCallbacksApi24 {
+        ServiceCallbacksImplApi24(Object callbacksObj) {
+            super(callbacksObj);
+        }
+
+        @Override
+        public void onLoadChildren(String mediaId, List<Parcel> list, Bundle options)
+                throws RemoteException {
+            ((ServiceCallbacksAdapterApi24)mCallbacks).onLoadChildrenWithOptions(
+                    mediaId, parcelListToParceledListSliceObject(list), options);
+        }
+
+        @Override
+        ServiceCallbacksAdapterApi24 createCallbacks(Object callbacksObj) {
+            return new ServiceCallbacksAdapterApi24(callbacksObj);
+        }
+    }
+
+    static class MediaBrowserServiceAdaptorApi24 extends MediaBrowserServiceAdaptorApi23 {
+        protected Binder createServiceBinder(ServiceImplApi21 serviceImpl) {
+            return new ServiceBinderAdapterApi24((ServiceImplApi24) serviceImpl);
+        }
+    }
+}
diff --git a/v4/api24/android/support/v4/media/ServiceBinderAdapterApi24.java b/v4/api24/android/support/v4/media/ServiceBinderAdapterApi24.java
new file mode 100644
index 0000000..1a6fef8
--- /dev/null
+++ b/v4/api24/android/support/v4/media/ServiceBinderAdapterApi24.java
@@ -0,0 +1,97 @@
+/*
+ * 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 android.support.v4.media;
+
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.RemoteException;
+
+/**
+ * A class for presenting a service binder for API 24.
+ *
+ * MediaBrowserServiceCompat should inherit a private class, MediaBrowserService.ServiceBinder,
+ * which extends IMediaBrowserService.Stub. As a way to inherit the private class,
+ * ServiceBinderAdapterApi21 implements all necessary functionalities including the codes in the
+ * auto generated hidden class, IMediaBrowserService.Stub for API 24.
+ */
+class ServiceBinderAdapterApi24 extends ServiceBinderAdapterApi23 {
+    // Following TRANSACTION_XXX values are synchronized with the auto generated java file
+    // from IMediaBrowserService.aidl
+    private static final int TRANSACTION_addSubscriptionWithOptions =
+            IBinder.FIRST_CALL_TRANSACTION + 5;
+    private static final int TRANSACTION_removeSubscriptionWithOptions =
+            IBinder.FIRST_CALL_TRANSACTION + 6;
+
+    final MediaBrowserServiceCompatApi24.ServiceImplApi24 mServiceImpl;
+
+    public ServiceBinderAdapterApi24(
+            MediaBrowserServiceCompatApi24.ServiceImplApi24 serviceImpl) {
+        super(serviceImpl);
+        mServiceImpl = serviceImpl;
+    }
+
+    @Override
+    public IBinder asBinder() {
+        return this;
+    }
+
+    @Override
+    public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+            throws RemoteException {
+        switch (code) {
+            case TRANSACTION_addSubscriptionWithOptions: {
+                data.enforceInterface(DESCRIPTOR);
+                String arg0 = data.readString();
+                Bundle arg1 = (data.readInt() == 0)
+                        ? null : Bundle.CREATOR.createFromParcel(data);
+                Object arg2 = ServiceCallbacksAdapterApi24.Stub.asInterface(
+                        data.readStrongBinder());
+                addSubscription(arg0, arg1, arg2);
+                return true;
+            }
+            case TRANSACTION_removeSubscriptionWithOptions: {
+                data.enforceInterface(DESCRIPTOR);
+                String arg0 = data.readString();
+                Bundle arg1 = (data.readInt() == 0)
+                        ? null : Bundle.CREATOR.createFromParcel(data);
+                Object arg2 = ServiceCallbacksAdapterApi24.Stub.asInterface(
+                        data.readStrongBinder());
+                removeSubscription(arg0, arg1, arg2);
+                return true;
+            }
+        }
+        return super.onTransact(code, data, reply, flags);
+    }
+
+    @Override
+    void connect(String pkg, Bundle rootHints, Object callbacks) {
+        mServiceImpl.connect(pkg, rootHints,
+                new MediaBrowserServiceCompatApi24.ServiceCallbacksImplApi24(callbacks));
+    }
+
+    void addSubscription(String id, Bundle options, Object callbacks) {
+        mServiceImpl.addSubscription(id, options,
+                new MediaBrowserServiceCompatApi24.ServiceCallbacksImplApi24(callbacks));
+    }
+
+    void removeSubscription(String id, Bundle options, Object callbacks) {
+        mServiceImpl.removeSubscription(id, options,
+                new MediaBrowserServiceCompatApi24.ServiceCallbacksImplApi24(callbacks));
+    }
+}
+
diff --git a/v4/api24/android/support/v4/media/ServiceCallbacksAdapterApi24.java b/v4/api24/android/support/v4/media/ServiceCallbacksAdapterApi24.java
new file mode 100644
index 0000000..741cabe
--- /dev/null
+++ b/v4/api24/android/support/v4/media/ServiceCallbacksAdapterApi24.java
@@ -0,0 +1,53 @@
+/*
+ * 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 android.support.v4.media;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * An adapter class for accessing the hidden framework classes, IMediaBrowserServiceCallbacks and
+ * IMediaBrowserServiceCallbacks.Stub using reflection.
+ */
+class ServiceCallbacksAdapterApi24 extends ServiceCallbacksAdapterApi21 {
+    private Method mOnLoadChildrenMethodWithOptionsMethod;
+
+    ServiceCallbacksAdapterApi24(Object callbackObject) {
+        super(callbackObject);
+        try {
+            Class theClass = Class.forName("android.service.media.IMediaBrowserServiceCallbacks");
+            Class parceledListSliceClass = Class.forName("android.content.pm.ParceledListSlice");
+            mOnLoadChildrenMethodWithOptionsMethod = theClass.getMethod("onLoadChildrenWithOptions",
+                    new Class[] { String.class, parceledListSliceClass, Bundle.class });
+        } catch (ClassNotFoundException | NoSuchMethodException e) {
+            e.printStackTrace();
+        }
+    }
+
+    void onLoadChildrenWithOptions(String mediaId, Object parceledListSliceObj, Bundle options)
+            throws RemoteException {
+        try {
+            mOnLoadChildrenMethodWithOptionsMethod.invoke(
+                    mCallbackObject, mediaId, parceledListSliceObj, options);
+        } catch (IllegalAccessException | InvocationTargetException | NullPointerException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/v4/java/android/support/v4/media/MediaBrowserCompat.java b/v4/java/android/support/v4/media/MediaBrowserCompat.java
index 90232cc..8d3c87e 100644
--- a/v4/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/v4/java/android/support/v4/media/MediaBrowserCompat.java
@@ -93,7 +93,9 @@
      */
     public MediaBrowserCompat(Context context, ComponentName serviceComponent,
             ConnectionCallback callback, Bundle rootHints) {
-        if (Build.VERSION.SDK_INT >= 23) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            mImpl = new MediaBrowserImplApi24(context, serviceComponent, callback, rootHints);
+        } else if (Build.VERSION.SDK_INT >= 23) {
             mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints);
         } else if (Build.VERSION.SDK_INT >= 21) {
             mImpl = new MediaBrowserImplApi21(context, serviceComponent, callback, rootHints);
@@ -534,8 +536,13 @@
         public SubscriptionCallbackApi21(SubscriptionCallback callback, Bundle options) {
             mSubscriptionCallback = callback;
             mOptions = options;
-            mSubscriptionCallbackObj =
-                    MediaBrowserCompatApi21.createSubscriptionCallback(new StubApi21());
+            if (Build.VERSION.SDK_INT >= 24) {
+                mSubscriptionCallbackObj =
+                        MediaBrowserCompatApi24.createSubscriptionCallback(new StubApi24());
+            } else  {
+                mSubscriptionCallbackObj =
+                        MediaBrowserCompatApi21.createSubscriptionCallback(new StubApi21());
+            }
         }
 
         /**
@@ -629,6 +636,21 @@
                 return items;
             }
         }
+
+        private class StubApi24 extends StubApi21
+                implements MediaBrowserCompatApi24.SubscriptionCallback {
+            @Override
+            public void onChildrenLoaded(@NonNull String parentId, @NonNull List<Parcel> children,
+                    @NonNull Bundle options) {
+                SubscriptionCallbackApi21.this.onChildrenLoaded(
+                        parentId, parcelListToItemList(children), mOptions);
+            }
+
+            @Override
+            public void onError(@NonNull String parentId, @NonNull Bundle options) {
+                SubscriptionCallbackApi21.this.onError(parentId, mOptions);
+            }
+        }
     }
 
     /**
@@ -1486,6 +1508,35 @@
         }
     }
 
+    static class MediaBrowserImplApi24 extends MediaBrowserImplApi23 {
+        public MediaBrowserImplApi24(Context context, ComponentName serviceComponent,
+                ConnectionCallback callback, Bundle rootHints) {
+            super(context, serviceComponent, callback, rootHints);
+        }
+
+        @Override
+        public void subscribe(@NonNull String parentId, @NonNull Bundle options,
+                @NonNull SubscriptionCallback callback) {
+            SubscriptionCallbackApi21 cb21 = new SubscriptionCallbackApi21(callback, options);
+            if (options == null) {
+                MediaBrowserCompatApi21.subscribe(
+                        mBrowserObj, parentId, cb21.mSubscriptionCallbackObj);
+            } else {
+                MediaBrowserCompatApi24.subscribe(
+                        mBrowserObj, parentId, options, cb21.mSubscriptionCallbackObj);
+            }
+        }
+
+        @Override
+        public void unsubscribe(@NonNull String parentId, Bundle options) {
+            if (options == null) {
+                MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
+            } else {
+                MediaBrowserCompatApi24.unsubscribe(mBrowserObj, parentId, options);
+            }
+        }
+    }
+
     private static class Subscription {
         private final List<SubscriptionCallback> mCallbacks;
         private final List<Bundle> mOptionsList;
diff --git a/v4/java/android/support/v4/media/MediaBrowserServiceCompat.java b/v4/java/android/support/v4/media/MediaBrowserServiceCompat.java
index 8acaf75..61e87b7 100644
--- a/v4/java/android/support/v4/media/MediaBrowserServiceCompat.java
+++ b/v4/java/android/support/v4/media/MediaBrowserServiceCompat.java
@@ -151,6 +151,21 @@
         }
     }
 
+    class MediaBrowserServiceImplApi24 implements MediaBrowserServiceImpl {
+        private Object mServiceObj;
+
+        @Override
+        public void onCreate() {
+            mServiceObj = MediaBrowserServiceCompatApi24.createService();
+            MediaBrowserServiceCompatApi24.onCreate(mServiceObj, new ServiceImplApi24());
+        }
+
+        @Override
+        public IBinder onBind(Intent intent) {
+            return MediaBrowserServiceCompatApi23.onBind(mServiceObj, intent);
+        }
+    }
+
     private final class ServiceHandler extends Handler {
         private final ServiceImpl mServiceImpl = new ServiceImpl();
 
@@ -481,6 +496,27 @@
         }
     }
 
+    private class ServiceImplApi24 extends ServiceImplApi23
+            implements MediaBrowserServiceCompatApi24.ServiceImplApi24 {
+        @Override
+        public void connect(String pkg, Bundle rootHints,
+                MediaBrowserServiceCompatApi24.ServiceCallbacksApi24 callbacks) {
+            mServiceImpl.connect(pkg, Binder.getCallingUid(), rootHints,
+                    new ServiceCallbacksApi24(callbacks));
+        }
+
+        @Override
+        public void addSubscription(String id, Bundle options,
+                MediaBrowserServiceCompatApi24.ServiceCallbacksApi24 callbacks) {
+            mServiceImpl.addSubscription(id, options, new ServiceCallbacksApi24(callbacks));
+        }
+
+        public void removeSubscription(String id, Bundle options,
+                MediaBrowserServiceCompatApi24.ServiceCallbacksApi24 callbacks) {
+            mServiceImpl.removeSubscription(id, options, new ServiceCallbacksApi24(callbacks));
+        }
+    }
+
     private interface ServiceCallbacks {
         IBinder asBinder();
         void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
@@ -581,10 +617,39 @@
         }
     }
 
+    private class ServiceCallbacksApi24 extends ServiceCallbacksApi21 {
+        final MediaBrowserServiceCompatApi24.ServiceCallbacksApi24 mCallbacks;
+
+        ServiceCallbacksApi24(MediaBrowserServiceCompatApi24.ServiceCallbacksApi24 callbacks) {
+            super(callbacks);
+            mCallbacks = callbacks;
+        }
+
+        public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
+                Bundle options) throws RemoteException {
+            List<Parcel> parcelList = null;
+            if (list != null) {
+                parcelList = new ArrayList<>();
+                for (MediaBrowserCompat.MediaItem item : list) {
+                    Parcel parcel = Parcel.obtain();
+                    item.writeToParcel(parcel, 0);
+                    parcelList.add(parcel);
+                }
+            }
+            if (options == null) {
+                mCallbacks.onLoadChildren(mediaId, parcelList);
+            } else {
+                mCallbacks.onLoadChildren(mediaId, parcelList, options);
+            }
+        }
+    }
+
     @Override
     public void onCreate() {
         super.onCreate();
-        if (Build.VERSION.SDK_INT >= 23) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            mImpl = new MediaBrowserServiceImplApi24();
+        } else if (Build.VERSION.SDK_INT >= 23) {
             mImpl = new MediaBrowserServiceImplApi23();
         } else if (Build.VERSION.SDK_INT >= 21) {
             mImpl = new MediaBrowserServiceImplApi21();