1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.musicplayer;
18 
19 import android.app.PendingIntent;
20 import android.graphics.Bitmap;
21 import android.os.Looper;
22 import android.util.Log;
23 
24 import java.lang.reflect.Field;
25 import java.lang.reflect.Method;
26 
27 /**
28  * RemoteControlClient enables exposing information meant to be consumed by remote controls capable
29  * of displaying metadata, artwork and media transport control buttons. A remote control client
30  * object is associated with a media button event receiver. This event receiver must have been
31  * previously registered with
32  * {@link android.media.AudioManager#registerMediaButtonEventReceiver(android.content.ComponentName)}
33  * before the RemoteControlClient can be registered through
34  * {@link android.media.AudioManager#registerRemoteControlClient(android.media.RemoteControlClient)}.
35  */
36 @SuppressWarnings({"rawtypes", "unchecked"})
37 public class RemoteControlClientCompat {
38 
39     private static final String TAG = "RemoteControlCompat";
40 
41     private static Class sRemoteControlClientClass;
42 
43     // RCC short for RemoteControlClient
44     private static Method sRCCEditMetadataMethod;
45     private static Method sRCCSetPlayStateMethod;
46     private static Method sRCCSetTransportControlFlags;
47 
48     private static boolean sHasRemoteControlAPIs = false;
49 
50     static {
51         try {
52             ClassLoader classLoader = RemoteControlClientCompat.class.getClassLoader();
53             sRemoteControlClientClass = getActualRemoteControlClientClass(classLoader);
54             // dynamically populate the playstate and flag values in case they change
55             // in future versions.
56             for (Field field : RemoteControlClientCompat.class.getFields()) {
57                 try {
58                     Field realField = sRemoteControlClientClass.getField(field.getName());
59                     Object realValue = realField.get(null);
field.set(null, realValue)60                     field.set(null, realValue);
61                 } catch (NoSuchFieldException e) {
62                     Log.w(TAG, "Could not get real field: " + field.getName());
63                 } catch (IllegalArgumentException e) {
64                     Log.w(TAG, "Error trying to pull field value for: " + field.getName()
65                             + " " + e.getMessage());
66                 } catch (IllegalAccessException e) {
67                     Log.w(TAG, "Error trying to pull field value for: " + field.getName()
68                             + " " + e.getMessage());
69                 }
70             }
71 
72             // get the required public methods on RemoteControlClient
73             sRCCEditMetadataMethod = sRemoteControlClientClass.getMethod("editMetadata",
74                     boolean.class);
75             sRCCSetPlayStateMethod = sRemoteControlClientClass.getMethod("setPlaybackState",
76                     int.class);
77             sRCCSetTransportControlFlags = sRemoteControlClientClass.getMethod(
78                     "setTransportControlFlags", int.class);
79 
80             sHasRemoteControlAPIs = true;
81         } catch (ClassNotFoundException e) {
82             // Silently fail when running on an OS before ICS.
83         } catch (NoSuchMethodException e) {
84             // Silently fail when running on an OS before ICS.
85         } catch (IllegalArgumentException e) {
86             // Silently fail when running on an OS before ICS.
87         } catch (SecurityException e) {
88             // Silently fail when running on an OS before ICS.
89         }
90     }
91 
getActualRemoteControlClientClass(ClassLoader classLoader)92     public static Class getActualRemoteControlClientClass(ClassLoader classLoader)
93             throws ClassNotFoundException {
94         return classLoader.loadClass("android.media.RemoteControlClient");
95     }
96 
97     private Object mActualRemoteControlClient;
98 
RemoteControlClientCompat(PendingIntent pendingIntent)99     public RemoteControlClientCompat(PendingIntent pendingIntent) {
100         if (!sHasRemoteControlAPIs) {
101             return;
102         }
103         try {
104             mActualRemoteControlClient =
105                     sRemoteControlClientClass.getConstructor(PendingIntent.class)
106                             .newInstance(pendingIntent);
107         } catch (Exception e) {
108             throw new RuntimeException(e);
109         }
110     }
111 
RemoteControlClientCompat(PendingIntent pendingIntent, Looper looper)112     public RemoteControlClientCompat(PendingIntent pendingIntent, Looper looper) {
113         if (!sHasRemoteControlAPIs) {
114             return;
115         }
116 
117         try {
118             mActualRemoteControlClient =
119                     sRemoteControlClientClass.getConstructor(PendingIntent.class, Looper.class)
120                             .newInstance(pendingIntent, looper);
121         } catch (Exception e) {
122             Log.e(TAG, "Error creating new instance of " + sRemoteControlClientClass.getName(), e);
123         }
124     }
125 
126     /**
127      * Class used to modify metadata in a {@link android.media.RemoteControlClient} object. Use
128      * {@link android.media.RemoteControlClient#editMetadata(boolean)} to create an instance of an
129      * editor, on which you set the metadata for the RemoteControlClient instance. Once all the
130      * information has been set, use {@link #apply()} to make it the new metadata that should be
131      * displayed for the associated client. Once the metadata has been "applied", you cannot reuse
132      * this instance of the MetadataEditor.
133      */
134     public class MetadataEditorCompat {
135 
136         private Method mPutStringMethod;
137         private Method mPutBitmapMethod;
138         private Method mPutLongMethod;
139         private Method mClearMethod;
140         private Method mApplyMethod;
141 
142         private Object mActualMetadataEditor;
143 
144         /**
145          * The metadata key for the content artwork / album art.
146          */
147         public final static int METADATA_KEY_ARTWORK = 100;
148 
MetadataEditorCompat(Object actualMetadataEditor)149         private MetadataEditorCompat(Object actualMetadataEditor) {
150             if (sHasRemoteControlAPIs && actualMetadataEditor == null) {
151                 throw new IllegalArgumentException("Remote Control API's exist, " +
152                         "should not be given a null MetadataEditor");
153             }
154             if (sHasRemoteControlAPIs) {
155                 Class metadataEditorClass = actualMetadataEditor.getClass();
156 
157                 try {
158                     mPutStringMethod = metadataEditorClass.getMethod("putString",
159                             int.class, String.class);
160                     mPutBitmapMethod = metadataEditorClass.getMethod("putBitmap",
161                             int.class, Bitmap.class);
162                     mPutLongMethod = metadataEditorClass.getMethod("putLong",
163                             int.class, long.class);
164                     mClearMethod = metadataEditorClass.getMethod("clear", new Class[]{});
165                     mApplyMethod = metadataEditorClass.getMethod("apply", new Class[]{});
166                 } catch (Exception e) {
167                     throw new RuntimeException(e.getMessage(), e);
168                 }
169             }
170             mActualMetadataEditor = actualMetadataEditor;
171         }
172 
173         /**
174          * Adds textual information to be displayed.
175          * Note that none of the information added after {@link #apply()} has been called,
176          * will be displayed.
177          * @param key The identifier of a the metadata field to set. Valid values are
178          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUM},
179          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUMARTIST},
180          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE},
181          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_ARTIST},
182          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_AUTHOR},
183          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPILATION},
184          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPOSER},
185          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_DATE},
186          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_GENRE},
187          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE},
188          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_WRITER}.
189          * @param value The text for the given key, or {@code null} to signify there is no valid
190          *      information for the field.
191          * @return Returns a reference to the same MetadataEditor object, so you can chain put
192          *      calls together.
193          */
putString(int key, String value)194         public MetadataEditorCompat putString(int key, String value) {
195             if (sHasRemoteControlAPIs) {
196                 try {
197                     mPutStringMethod.invoke(mActualMetadataEditor, key, value);
198                 } catch (Exception e) {
199                     throw new RuntimeException(e.getMessage(), e);
200                 }
201             }
202             return this;
203         }
204 
205         /**
206          * Sets the album / artwork picture to be displayed on the remote control.
207          * @param key the identifier of the bitmap to set. The only valid value is
208          *      {@link #METADATA_KEY_ARTWORK}
209          * @param bitmap The bitmap for the artwork, or null if there isn't any.
210          * @return Returns a reference to the same MetadataEditor object, so you can chain put
211          *      calls together.
212          * @throws IllegalArgumentException
213          * @see android.graphics.Bitmap
214          */
putBitmap(int key, Bitmap bitmap)215         public MetadataEditorCompat putBitmap(int key, Bitmap bitmap) {
216             if (sHasRemoteControlAPIs) {
217                 try {
218                     mPutBitmapMethod.invoke(mActualMetadataEditor, key, bitmap);
219                 } catch (Exception e) {
220                     throw new RuntimeException(e.getMessage(), e);
221                 }
222             }
223             return this;
224         }
225 
226         /**
227          * Adds numerical information to be displayed.
228          * Note that none of the information added after {@link #apply()} has been called,
229          * will be displayed.
230          * @param key the identifier of a the metadata field to set. Valid values are
231          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_CD_TRACK_NUMBER},
232          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_DISC_NUMBER},
233          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_DURATION} (with a value
234          *      expressed in milliseconds),
235          *      {@link android.media.MediaMetadataRetriever#METADATA_KEY_YEAR}.
236          * @param value The long value for the given key
237          * @return Returns a reference to the same MetadataEditor object, so you can chain put
238          *      calls together.
239          * @throws IllegalArgumentException
240          */
putLong(int key, long value)241         public MetadataEditorCompat putLong(int key, long value) {
242             if (sHasRemoteControlAPIs) {
243                 try {
244                     mPutLongMethod.invoke(mActualMetadataEditor, key, value);
245                 } catch (Exception e) {
246                     throw new RuntimeException(e.getMessage(), e);
247                 }
248             }
249             return this;
250         }
251 
252         /**
253          * Clears all the metadata that has been set since the MetadataEditor instance was
254          * created with {@link android.media.RemoteControlClient#editMetadata(boolean)}.
255          */
clear()256         public void clear() {
257             if (sHasRemoteControlAPIs) {
258                 try {
259                     mClearMethod.invoke(mActualMetadataEditor, (Object[]) null);
260                 } catch (Exception e) {
261                     throw new RuntimeException(e.getMessage(), e);
262                 }
263             }
264         }
265 
266         /**
267          * Associates all the metadata that has been set since the MetadataEditor instance was
268          * created with {@link android.media.RemoteControlClient#editMetadata(boolean)}, or since
269          * {@link #clear()} was called, with the RemoteControlClient. Once "applied", this
270          * MetadataEditor cannot be reused to edit the RemoteControlClient's metadata.
271          */
apply()272         public void apply() {
273             if (sHasRemoteControlAPIs) {
274                 try {
275                     mApplyMethod.invoke(mActualMetadataEditor, (Object[]) null);
276                 } catch (Exception e) {
277                     throw new RuntimeException(e.getMessage(), e);
278                 }
279             }
280         }
281     }
282 
283     /**
284      * Creates a {@link android.media.RemoteControlClient.MetadataEditor}.
285      * @param startEmpty Set to false if you want the MetadataEditor to contain the metadata that
286      *     was previously applied to the RemoteControlClient, or true if it is to be created empty.
287      * @return a new MetadataEditor instance.
288      */
editMetadata(boolean startEmpty)289     public MetadataEditorCompat editMetadata(boolean startEmpty) {
290         Object metadataEditor;
291         if (sHasRemoteControlAPIs) {
292             try {
293                 metadataEditor = sRCCEditMetadataMethod.invoke(mActualRemoteControlClient,
294                         startEmpty);
295             } catch (Exception e) {
296                 throw new RuntimeException(e);
297             }
298         } else {
299             metadataEditor = null;
300         }
301         return new MetadataEditorCompat(metadataEditor);
302     }
303 
304     /**
305      * Sets the current playback state.
306      * @param state The current playback state, one of the following values:
307      *       {@link android.media.RemoteControlClient#PLAYSTATE_STOPPED},
308      *       {@link android.media.RemoteControlClient#PLAYSTATE_PAUSED},
309      *       {@link android.media.RemoteControlClient#PLAYSTATE_PLAYING},
310      *       {@link android.media.RemoteControlClient#PLAYSTATE_FAST_FORWARDING},
311      *       {@link android.media.RemoteControlClient#PLAYSTATE_REWINDING},
312      *       {@link android.media.RemoteControlClient#PLAYSTATE_SKIPPING_FORWARDS},
313      *       {@link android.media.RemoteControlClient#PLAYSTATE_SKIPPING_BACKWARDS},
314      *       {@link android.media.RemoteControlClient#PLAYSTATE_BUFFERING},
315      *       {@link android.media.RemoteControlClient#PLAYSTATE_ERROR}.
316      */
setPlaybackState(int state)317     public void setPlaybackState(int state) {
318         if (sHasRemoteControlAPIs) {
319             try {
320                 sRCCSetPlayStateMethod.invoke(mActualRemoteControlClient, state);
321             } catch (Exception e) {
322                 throw new RuntimeException(e);
323             }
324         }
325     }
326 
327     /**
328      * Sets the flags for the media transport control buttons that this client supports.
329      * @param transportControlFlags A combination of the following flags:
330      *      {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS},
331      *      {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND},
332      *      {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY},
333      *      {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE},
334      *      {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE},
335      *      {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP},
336      *      {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD},
337      *      {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT}
338      */
setTransportControlFlags(int transportControlFlags)339     public void setTransportControlFlags(int transportControlFlags) {
340         if (sHasRemoteControlAPIs) {
341             try {
342                 sRCCSetTransportControlFlags.invoke(mActualRemoteControlClient,
343                         transportControlFlags);
344             } catch (Exception e) {
345                 throw new RuntimeException(e);
346             }
347         }
348     }
349 
getActualRemoteControlClientObject()350     public final Object getActualRemoteControlClientObject() {
351         return mActualRemoteControlClient;
352     }
353 }
354