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); 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 92 public static Class getActualRemoteControlClientClass(ClassLoader classLoader) 93 throws ClassNotFoundException { 94 return classLoader.loadClass("android.media.RemoteControlClient"); 95 } 96 97 private Object mActualRemoteControlClient; 98 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 350 public final Object getActualRemoteControlClientObject() { 351 return mActualRemoteControlClient; 352 } 353 } 354