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 android.media; 18 19 import android.app.PendingIntent; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.Paint; 26 import android.graphics.RectF; 27 import android.media.MediaMetadataRetriever; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.os.IBinder; 31 import android.os.Looper; 32 import android.os.Message; 33 import android.os.RemoteException; 34 import android.os.ServiceManager; 35 import android.os.SystemClock; 36 import android.util.Log; 37 38 import java.lang.IllegalArgumentException; 39 40 /** 41 * RemoteControlClient enables exposing information meant to be consumed by remote controls 42 * capable of displaying metadata, artwork and media transport control buttons. 43 * 44 * <p>A remote control client object is associated with a media button event receiver. This 45 * event receiver must have been previously registered with 46 * {@link AudioManager#registerMediaButtonEventReceiver(ComponentName)} before the 47 * RemoteControlClient can be registered through 48 * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}. 49 * 50 * <p>Here is an example of creating a RemoteControlClient instance after registering a media 51 * button event receiver: 52 * <pre>ComponentName myEventReceiver = new ComponentName(getPackageName(), MyRemoteControlEventReceiver.class.getName()); 53 * AudioManager myAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 54 * myAudioManager.registerMediaButtonEventReceiver(myEventReceiver); 55 * // build the PendingIntent for the remote control client 56 * Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); 57 * mediaButtonIntent.setComponent(myEventReceiver); 58 * PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0); 59 * // create and register the remote control client 60 * RemoteControlClient myRemoteControlClient = new RemoteControlClient(mediaPendingIntent); 61 * myAudioManager.registerRemoteControlClient(myRemoteControlClient);</pre> 62 */ 63 public class RemoteControlClient 64 { 65 private final static String TAG = "RemoteControlClient"; 66 67 /** 68 * Playback state of a RemoteControlClient which is stopped. 69 * 70 * @see #setPlaybackState(int) 71 */ 72 public final static int PLAYSTATE_STOPPED = 1; 73 /** 74 * Playback state of a RemoteControlClient which is paused. 75 * 76 * @see #setPlaybackState(int) 77 */ 78 public final static int PLAYSTATE_PAUSED = 2; 79 /** 80 * Playback state of a RemoteControlClient which is playing media. 81 * 82 * @see #setPlaybackState(int) 83 */ 84 public final static int PLAYSTATE_PLAYING = 3; 85 /** 86 * Playback state of a RemoteControlClient which is fast forwarding in the media 87 * it is currently playing. 88 * 89 * @see #setPlaybackState(int) 90 */ 91 public final static int PLAYSTATE_FAST_FORWARDING = 4; 92 /** 93 * Playback state of a RemoteControlClient which is fast rewinding in the media 94 * it is currently playing. 95 * 96 * @see #setPlaybackState(int) 97 */ 98 public final static int PLAYSTATE_REWINDING = 5; 99 /** 100 * Playback state of a RemoteControlClient which is skipping to the next 101 * logical chapter (such as a song in a playlist) in the media it is currently playing. 102 * 103 * @see #setPlaybackState(int) 104 */ 105 public final static int PLAYSTATE_SKIPPING_FORWARDS = 6; 106 /** 107 * Playback state of a RemoteControlClient which is skipping back to the previous 108 * logical chapter (such as a song in a playlist) in the media it is currently playing. 109 * 110 * @see #setPlaybackState(int) 111 */ 112 public final static int PLAYSTATE_SKIPPING_BACKWARDS = 7; 113 /** 114 * Playback state of a RemoteControlClient which is buffering data to play before it can 115 * start or resume playback. 116 * 117 * @see #setPlaybackState(int) 118 */ 119 public final static int PLAYSTATE_BUFFERING = 8; 120 /** 121 * Playback state of a RemoteControlClient which cannot perform any playback related 122 * operation because of an internal error. Examples of such situations are no network 123 * connectivity when attempting to stream data from a server, or expired user credentials 124 * when trying to play subscription-based content. 125 * 126 * @see #setPlaybackState(int) 127 */ 128 public final static int PLAYSTATE_ERROR = 9; 129 /** 130 * @hide 131 * The value of a playback state when none has been declared. 132 * Intentionally hidden as an application shouldn't set such a playback state value. 133 */ 134 public final static int PLAYSTATE_NONE = 0; 135 136 /** 137 * @hide 138 * The default playback type, "local", indicating the presentation of the media is happening on 139 * the same device (e.g. a phone, a tablet) as where it is controlled from. 140 */ 141 public final static int PLAYBACK_TYPE_LOCAL = 0; 142 /** 143 * @hide 144 * A playback type indicating the presentation of the media is happening on 145 * a different device (i.e. the remote device) than where it is controlled from. 146 */ 147 public final static int PLAYBACK_TYPE_REMOTE = 1; 148 private final static int PLAYBACK_TYPE_MIN = PLAYBACK_TYPE_LOCAL; 149 private final static int PLAYBACK_TYPE_MAX = PLAYBACK_TYPE_REMOTE; 150 /** 151 * @hide 152 * Playback information indicating the playback volume is fixed, i.e. it cannot be controlled 153 * from this object. An example of fixed playback volume is a remote player, playing over HDMI 154 * where the user prefer to control the volume on the HDMI sink, rather than attenuate at the 155 * source. 156 * @see #PLAYBACKINFO_VOLUME_HANDLING. 157 */ 158 public final static int PLAYBACK_VOLUME_FIXED = 0; 159 /** 160 * @hide 161 * Playback information indicating the playback volume is variable and can be controlled from 162 * this object. 163 * @see #PLAYBACKINFO_VOLUME_HANDLING. 164 */ 165 public final static int PLAYBACK_VOLUME_VARIABLE = 1; 166 /** 167 * @hide (to be un-hidden) 168 * The playback information value indicating the value of a given information type is invalid. 169 * @see #PLAYBACKINFO_VOLUME_HANDLING. 170 */ 171 public final static int PLAYBACKINFO_INVALID_VALUE = Integer.MIN_VALUE; 172 173 //========================================== 174 // Public keys for playback information 175 /** 176 * @hide 177 * Playback information that defines the type of playback associated with this 178 * RemoteControlClient. See {@link #PLAYBACK_TYPE_LOCAL} and {@link #PLAYBACK_TYPE_REMOTE}. 179 */ 180 public final static int PLAYBACKINFO_PLAYBACK_TYPE = 1; 181 /** 182 * @hide 183 * Playback information that defines at what volume the playback associated with this 184 * RemoteControlClient is performed. This information is only used when the playback type is not 185 * local (see {@link #PLAYBACKINFO_PLAYBACK_TYPE}). 186 */ 187 public final static int PLAYBACKINFO_VOLUME = 2; 188 /** 189 * @hide 190 * Playback information that defines the maximum volume volume value that is supported 191 * by the playback associated with this RemoteControlClient. This information is only used 192 * when the playback type is not local (see {@link #PLAYBACKINFO_PLAYBACK_TYPE}). 193 */ 194 public final static int PLAYBACKINFO_VOLUME_MAX = 3; 195 /** 196 * @hide 197 * Playback information that defines how volume is handled for the presentation of the media. 198 * @see #PLAYBACK_VOLUME_FIXED 199 * @see #PLAYBACK_VOLUME_VARIABLE 200 */ 201 public final static int PLAYBACKINFO_VOLUME_HANDLING = 4; 202 /** 203 * @hide 204 * Playback information that defines over what stream type the media is presented. 205 */ 206 public final static int PLAYBACKINFO_USES_STREAM = 5; 207 208 //========================================== 209 // Private keys for playback information 210 /** 211 * @hide 212 * Used internally to relay playback state (set by the application with 213 * {@link #setPlaybackState(int)}) to AudioService 214 */ 215 public final static int PLAYBACKINFO_PLAYSTATE = 255; 216 217 218 /** 219 * Flag indicating a RemoteControlClient makes use of the "previous" media key. 220 * 221 * @see #setTransportControlFlags(int) 222 * @see android.view.KeyEvent#KEYCODE_MEDIA_PREVIOUS 223 */ 224 public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0; 225 /** 226 * Flag indicating a RemoteControlClient makes use of the "rewind" media key. 227 * 228 * @see #setTransportControlFlags(int) 229 * @see android.view.KeyEvent#KEYCODE_MEDIA_REWIND 230 */ 231 public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1; 232 /** 233 * Flag indicating a RemoteControlClient makes use of the "play" media key. 234 * 235 * @see #setTransportControlFlags(int) 236 * @see android.view.KeyEvent#KEYCODE_MEDIA_PLAY 237 */ 238 public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2; 239 /** 240 * Flag indicating a RemoteControlClient makes use of the "play/pause" media key. 241 * 242 * @see #setTransportControlFlags(int) 243 * @see android.view.KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE 244 */ 245 public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3; 246 /** 247 * Flag indicating a RemoteControlClient makes use of the "pause" media key. 248 * 249 * @see #setTransportControlFlags(int) 250 * @see android.view.KeyEvent#KEYCODE_MEDIA_PAUSE 251 */ 252 public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4; 253 /** 254 * Flag indicating a RemoteControlClient makes use of the "stop" media key. 255 * 256 * @see #setTransportControlFlags(int) 257 * @see android.view.KeyEvent#KEYCODE_MEDIA_STOP 258 */ 259 public final static int FLAG_KEY_MEDIA_STOP = 1 << 5; 260 /** 261 * Flag indicating a RemoteControlClient makes use of the "fast forward" media key. 262 * 263 * @see #setTransportControlFlags(int) 264 * @see android.view.KeyEvent#KEYCODE_MEDIA_FAST_FORWARD 265 */ 266 public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6; 267 /** 268 * Flag indicating a RemoteControlClient makes use of the "next" media key. 269 * 270 * @see #setTransportControlFlags(int) 271 * @see android.view.KeyEvent#KEYCODE_MEDIA_NEXT 272 */ 273 public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7; 274 275 /** 276 * @hide 277 * The flags for when no media keys are declared supported. 278 * Intentionally hidden as an application shouldn't set the transport control flags 279 * to this value. 280 */ 281 public final static int FLAGS_KEY_MEDIA_NONE = 0; 282 283 /** 284 * @hide 285 * Flag used to signal some type of metadata exposed by the RemoteControlClient is requested. 286 */ 287 public final static int FLAG_INFORMATION_REQUEST_METADATA = 1 << 0; 288 /** 289 * @hide 290 * Flag used to signal that the transport control buttons supported by the 291 * RemoteControlClient are requested. 292 * This can for instance happen when playback is at the end of a playlist, and the "next" 293 * operation is not supported anymore. 294 */ 295 public final static int FLAG_INFORMATION_REQUEST_KEY_MEDIA = 1 << 1; 296 /** 297 * @hide 298 * Flag used to signal that the playback state of the RemoteControlClient is requested. 299 */ 300 public final static int FLAG_INFORMATION_REQUEST_PLAYSTATE = 1 << 2; 301 /** 302 * @hide 303 * Flag used to signal that the album art for the RemoteControlClient is requested. 304 */ 305 public final static int FLAG_INFORMATION_REQUEST_ALBUM_ART = 1 << 3; 306 307 /** 308 * Class constructor. 309 * @param mediaButtonIntent The intent that will be sent for the media button events sent 310 * by remote controls. 311 * This intent needs to have been constructed with the {@link Intent#ACTION_MEDIA_BUTTON} 312 * action, and have a component that will handle the intent (set with 313 * {@link Intent#setComponent(ComponentName)}) registered with 314 * {@link AudioManager#registerMediaButtonEventReceiver(ComponentName)} 315 * before this new RemoteControlClient can itself be registered with 316 * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}. 317 * @see AudioManager#registerMediaButtonEventReceiver(ComponentName) 318 * @see AudioManager#registerRemoteControlClient(RemoteControlClient) 319 */ RemoteControlClient(PendingIntent mediaButtonIntent)320 public RemoteControlClient(PendingIntent mediaButtonIntent) { 321 mRcMediaIntent = mediaButtonIntent; 322 323 Looper looper; 324 if ((looper = Looper.myLooper()) != null) { 325 mEventHandler = new EventHandler(this, looper); 326 } else if ((looper = Looper.getMainLooper()) != null) { 327 mEventHandler = new EventHandler(this, looper); 328 } else { 329 mEventHandler = null; 330 Log.e(TAG, "RemoteControlClient() couldn't find main application thread"); 331 } 332 } 333 334 /** 335 * Class constructor for a remote control client whose internal event handling 336 * happens on a user-provided Looper. 337 * @param mediaButtonIntent The intent that will be sent for the media button events sent 338 * by remote controls. 339 * This intent needs to have been constructed with the {@link Intent#ACTION_MEDIA_BUTTON} 340 * action, and have a component that will handle the intent (set with 341 * {@link Intent#setComponent(ComponentName)}) registered with 342 * {@link AudioManager#registerMediaButtonEventReceiver(ComponentName)} 343 * before this new RemoteControlClient can itself be registered with 344 * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}. 345 * @param looper The Looper running the event loop. 346 * @see AudioManager#registerMediaButtonEventReceiver(ComponentName) 347 * @see AudioManager#registerRemoteControlClient(RemoteControlClient) 348 */ RemoteControlClient(PendingIntent mediaButtonIntent, Looper looper)349 public RemoteControlClient(PendingIntent mediaButtonIntent, Looper looper) { 350 mRcMediaIntent = mediaButtonIntent; 351 352 mEventHandler = new EventHandler(this, looper); 353 } 354 355 private static final int[] METADATA_KEYS_TYPE_STRING = { 356 MediaMetadataRetriever.METADATA_KEY_ALBUM, 357 MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, 358 MediaMetadataRetriever.METADATA_KEY_TITLE, 359 MediaMetadataRetriever.METADATA_KEY_ARTIST, 360 MediaMetadataRetriever.METADATA_KEY_AUTHOR, 361 MediaMetadataRetriever.METADATA_KEY_COMPILATION, 362 MediaMetadataRetriever.METADATA_KEY_COMPOSER, 363 MediaMetadataRetriever.METADATA_KEY_DATE, 364 MediaMetadataRetriever.METADATA_KEY_GENRE, 365 MediaMetadataRetriever.METADATA_KEY_TITLE, 366 MediaMetadataRetriever.METADATA_KEY_WRITER }; 367 private static final int[] METADATA_KEYS_TYPE_LONG = { 368 MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, 369 MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER, 370 MediaMetadataRetriever.METADATA_KEY_DURATION }; 371 372 /** 373 * Class used to modify metadata in a {@link RemoteControlClient} object. 374 * Use {@link RemoteControlClient#editMetadata(boolean)} to create an instance of an editor, 375 * on which you set the metadata for the RemoteControlClient instance. Once all the information 376 * has been set, use {@link #apply()} to make it the new metadata that should be displayed 377 * for the associated client. Once the metadata has been "applied", you cannot reuse this 378 * instance of the MetadataEditor. 379 */ 380 public class MetadataEditor { 381 /** 382 * @hide 383 */ 384 protected boolean mMetadataChanged; 385 /** 386 * @hide 387 */ 388 protected boolean mArtworkChanged; 389 /** 390 * @hide 391 */ 392 protected Bitmap mEditorArtwork; 393 /** 394 * @hide 395 */ 396 protected Bundle mEditorMetadata; 397 private boolean mApplied = false; 398 399 // only use RemoteControlClient.editMetadata() to get a MetadataEditor instance MetadataEditor()400 private MetadataEditor() { } 401 /** 402 * @hide 403 */ clone()404 public Object clone() throws CloneNotSupportedException { 405 throw new CloneNotSupportedException(); 406 } 407 408 /** 409 * The metadata key for the content artwork / album art. 410 */ 411 public final static int BITMAP_KEY_ARTWORK = 100; 412 /** 413 * @hide 414 * TODO(jmtrivi) have lockscreen and music move to the new key name 415 */ 416 public final static int METADATA_KEY_ARTWORK = BITMAP_KEY_ARTWORK; 417 418 /** 419 * Adds textual information to be displayed. 420 * Note that none of the information added after {@link #apply()} has been called, 421 * will be displayed. 422 * @param key The identifier of a the metadata field to set. Valid values are 423 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUM}, 424 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUMARTIST}, 425 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE}, 426 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ARTIST}, 427 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_AUTHOR}, 428 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPILATION}, 429 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPOSER}, 430 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DATE}, 431 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_GENRE}, 432 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE}, 433 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_WRITER}. 434 * @param value The text for the given key, or {@code null} to signify there is no valid 435 * information for the field. 436 * @return Returns a reference to the same MetadataEditor object, so you can chain put 437 * calls together. 438 */ putString(int key, String value)439 public synchronized MetadataEditor putString(int key, String value) 440 throws IllegalArgumentException { 441 if (mApplied) { 442 Log.e(TAG, "Can't edit a previously applied MetadataEditor"); 443 return this; 444 } 445 if (!validTypeForKey(key, METADATA_KEYS_TYPE_STRING)) { 446 throw(new IllegalArgumentException("Invalid type 'String' for key "+ key)); 447 } 448 mEditorMetadata.putString(String.valueOf(key), value); 449 mMetadataChanged = true; 450 return this; 451 } 452 453 /** 454 * Adds numerical information to be displayed. 455 * Note that none of the information added after {@link #apply()} has been called, 456 * will be displayed. 457 * @param key the identifier of a the metadata field to set. Valid values are 458 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_CD_TRACK_NUMBER}, 459 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DISC_NUMBER}, 460 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DURATION} (with a value 461 * expressed in milliseconds), 462 * {@link android.media.MediaMetadataRetriever#METADATA_KEY_YEAR}. 463 * @param value The long value for the given key 464 * @return Returns a reference to the same MetadataEditor object, so you can chain put 465 * calls together. 466 * @throws IllegalArgumentException 467 */ putLong(int key, long value)468 public synchronized MetadataEditor putLong(int key, long value) 469 throws IllegalArgumentException { 470 if (mApplied) { 471 Log.e(TAG, "Can't edit a previously applied MetadataEditor"); 472 return this; 473 } 474 if (!validTypeForKey(key, METADATA_KEYS_TYPE_LONG)) { 475 throw(new IllegalArgumentException("Invalid type 'long' for key "+ key)); 476 } 477 mEditorMetadata.putLong(String.valueOf(key), value); 478 mMetadataChanged = true; 479 return this; 480 } 481 482 /** 483 * Sets the album / artwork picture to be displayed on the remote control. 484 * @param key the identifier of the bitmap to set. The only valid value is 485 * {@link #BITMAP_KEY_ARTWORK} 486 * @param bitmap The bitmap for the artwork, or null if there isn't any. 487 * @return Returns a reference to the same MetadataEditor object, so you can chain put 488 * calls together. 489 * @throws IllegalArgumentException 490 * @see android.graphics.Bitmap 491 */ putBitmap(int key, Bitmap bitmap)492 public synchronized MetadataEditor putBitmap(int key, Bitmap bitmap) 493 throws IllegalArgumentException { 494 if (mApplied) { 495 Log.e(TAG, "Can't edit a previously applied MetadataEditor"); 496 return this; 497 } 498 if (key != BITMAP_KEY_ARTWORK) { 499 throw(new IllegalArgumentException("Invalid type 'Bitmap' for key "+ key)); 500 } 501 if ((mArtworkExpectedWidth > 0) && (mArtworkExpectedHeight > 0)) { 502 mEditorArtwork = scaleBitmapIfTooBig(bitmap, 503 mArtworkExpectedWidth, mArtworkExpectedHeight); 504 } else { 505 // no valid resize dimensions, store as is 506 mEditorArtwork = bitmap; 507 } 508 mArtworkChanged = true; 509 return this; 510 } 511 512 /** 513 * Clears all the metadata that has been set since the MetadataEditor instance was 514 * created with {@link RemoteControlClient#editMetadata(boolean)}. 515 */ clear()516 public synchronized void clear() { 517 if (mApplied) { 518 Log.e(TAG, "Can't clear a previously applied MetadataEditor"); 519 return; 520 } 521 mEditorMetadata.clear(); 522 mEditorArtwork = null; 523 } 524 525 /** 526 * Associates all the metadata that has been set since the MetadataEditor instance was 527 * created with {@link RemoteControlClient#editMetadata(boolean)}, or since 528 * {@link #clear()} was called, with the RemoteControlClient. Once "applied", 529 * this MetadataEditor cannot be reused to edit the RemoteControlClient's metadata. 530 */ apply()531 public synchronized void apply() { 532 if (mApplied) { 533 Log.e(TAG, "Can't apply a previously applied MetadataEditor"); 534 return; 535 } 536 synchronized(mCacheLock) { 537 // assign the edited data 538 mMetadata = new Bundle(mEditorMetadata); 539 if ((mArtwork != null) && (!mArtwork.equals(mEditorArtwork))) { 540 mArtwork.recycle(); 541 } 542 mArtwork = mEditorArtwork; 543 mEditorArtwork = null; 544 if (mMetadataChanged & mArtworkChanged) { 545 // send to remote control display if conditions are met 546 sendMetadataWithArtwork_syncCacheLock(); 547 } else if (mMetadataChanged) { 548 // send to remote control display if conditions are met 549 sendMetadata_syncCacheLock(); 550 } else if (mArtworkChanged) { 551 // send to remote control display if conditions are met 552 sendArtwork_syncCacheLock(); 553 } 554 mApplied = true; 555 } 556 } 557 } 558 559 /** 560 * Creates a {@link MetadataEditor}. 561 * @param startEmpty Set to false if you want the MetadataEditor to contain the metadata that 562 * was previously applied to the RemoteControlClient, or true if it is to be created empty. 563 * @return a new MetadataEditor instance. 564 */ editMetadata(boolean startEmpty)565 public MetadataEditor editMetadata(boolean startEmpty) { 566 MetadataEditor editor = new MetadataEditor(); 567 if (startEmpty) { 568 editor.mEditorMetadata = new Bundle(); 569 editor.mEditorArtwork = null; 570 editor.mMetadataChanged = true; 571 editor.mArtworkChanged = true; 572 } else { 573 editor.mEditorMetadata = new Bundle(mMetadata); 574 editor.mEditorArtwork = mArtwork; 575 editor.mMetadataChanged = false; 576 editor.mArtworkChanged = false; 577 } 578 return editor; 579 } 580 581 /** 582 * Sets the current playback state. 583 * @param state The current playback state, one of the following values: 584 * {@link #PLAYSTATE_STOPPED}, 585 * {@link #PLAYSTATE_PAUSED}, 586 * {@link #PLAYSTATE_PLAYING}, 587 * {@link #PLAYSTATE_FAST_FORWARDING}, 588 * {@link #PLAYSTATE_REWINDING}, 589 * {@link #PLAYSTATE_SKIPPING_FORWARDS}, 590 * {@link #PLAYSTATE_SKIPPING_BACKWARDS}, 591 * {@link #PLAYSTATE_BUFFERING}, 592 * {@link #PLAYSTATE_ERROR}. 593 */ setPlaybackState(int state)594 public void setPlaybackState(int state) { 595 synchronized(mCacheLock) { 596 if (mPlaybackState != state) { 597 // store locally 598 mPlaybackState = state; 599 // keep track of when the state change occurred 600 mPlaybackStateChangeTimeMs = SystemClock.elapsedRealtime(); 601 602 // send to remote control display if conditions are met 603 sendPlaybackState_syncCacheLock(); 604 // update AudioService 605 sendAudioServiceNewPlaybackInfo_syncCacheLock(PLAYBACKINFO_PLAYSTATE, state); 606 } 607 } 608 } 609 610 /** 611 * Sets the flags for the media transport control buttons that this client supports. 612 * @param transportControlFlags A combination of the following flags: 613 * {@link #FLAG_KEY_MEDIA_PREVIOUS}, 614 * {@link #FLAG_KEY_MEDIA_REWIND}, 615 * {@link #FLAG_KEY_MEDIA_PLAY}, 616 * {@link #FLAG_KEY_MEDIA_PLAY_PAUSE}, 617 * {@link #FLAG_KEY_MEDIA_PAUSE}, 618 * {@link #FLAG_KEY_MEDIA_STOP}, 619 * {@link #FLAG_KEY_MEDIA_FAST_FORWARD}, 620 * {@link #FLAG_KEY_MEDIA_NEXT} 621 */ setTransportControlFlags(int transportControlFlags)622 public void setTransportControlFlags(int transportControlFlags) { 623 synchronized(mCacheLock) { 624 // store locally 625 mTransportControlFlags = transportControlFlags; 626 627 // send to remote control display if conditions are met 628 sendTransportControlFlags_syncCacheLock(); 629 } 630 } 631 632 /** @hide */ 633 public final static int DEFAULT_PLAYBACK_VOLUME_HANDLING = PLAYBACK_VOLUME_VARIABLE; 634 /** @hide */ 635 // hard-coded to the same number of steps as AudioService.MAX_STREAM_VOLUME[STREAM_MUSIC] 636 public final static int DEFAULT_PLAYBACK_VOLUME = 15; 637 638 private int mPlaybackType = PLAYBACK_TYPE_LOCAL; 639 private int mPlaybackVolumeMax = DEFAULT_PLAYBACK_VOLUME; 640 private int mPlaybackVolume = DEFAULT_PLAYBACK_VOLUME; 641 private int mPlaybackVolumeHandling = DEFAULT_PLAYBACK_VOLUME_HANDLING; 642 private int mPlaybackStream = AudioManager.STREAM_MUSIC; 643 644 /** 645 * @hide 646 * Set information describing information related to the playback of media so the system 647 * can implement additional behavior to handle non-local playback usecases. 648 * @param what a key to specify the type of information to set. Valid keys are 649 * {@link #PLAYBACKINFO_PLAYBACK_TYPE}, 650 * {@link #PLAYBACKINFO_USES_STREAM}, 651 * {@link #PLAYBACKINFO_VOLUME}, 652 * {@link #PLAYBACKINFO_VOLUME_MAX}, 653 * and {@link #PLAYBACKINFO_VOLUME_HANDLING}. 654 * @param value the value for the supplied information to set. 655 */ setPlaybackInformation(int what, int value)656 public void setPlaybackInformation(int what, int value) { 657 synchronized(mCacheLock) { 658 switch (what) { 659 case PLAYBACKINFO_PLAYBACK_TYPE: 660 if ((value >= PLAYBACK_TYPE_MIN) && (value <= PLAYBACK_TYPE_MAX)) { 661 if (mPlaybackType != value) { 662 mPlaybackType = value; 663 sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value); 664 } 665 } else { 666 Log.w(TAG, "using invalid value for PLAYBACKINFO_PLAYBACK_TYPE"); 667 } 668 break; 669 case PLAYBACKINFO_VOLUME: 670 if ((value > -1) && (value <= mPlaybackVolumeMax)) { 671 if (mPlaybackVolume != value) { 672 mPlaybackVolume = value; 673 sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value); 674 } 675 } else { 676 Log.w(TAG, "using invalid value for PLAYBACKINFO_VOLUME"); 677 } 678 break; 679 case PLAYBACKINFO_VOLUME_MAX: 680 if (value > 0) { 681 if (mPlaybackVolumeMax != value) { 682 mPlaybackVolumeMax = value; 683 sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value); 684 } 685 } else { 686 Log.w(TAG, "using invalid value for PLAYBACKINFO_VOLUME_MAX"); 687 } 688 break; 689 case PLAYBACKINFO_USES_STREAM: 690 if ((value >= 0) && (value < AudioSystem.getNumStreamTypes())) { 691 mPlaybackStream = value; 692 } else { 693 Log.w(TAG, "using invalid value for PLAYBACKINFO_USES_STREAM"); 694 } 695 break; 696 case PLAYBACKINFO_VOLUME_HANDLING: 697 if ((value >= PLAYBACK_VOLUME_FIXED) && (value <= PLAYBACK_VOLUME_VARIABLE)) { 698 if (mPlaybackVolumeHandling != value) { 699 mPlaybackVolumeHandling = value; 700 sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value); 701 } 702 } else { 703 Log.w(TAG, "using invalid value for PLAYBACKINFO_VOLUME_HANDLING"); 704 } 705 break; 706 default: 707 // not throwing an exception or returning an error if more keys are to be 708 // supported in the future 709 Log.w(TAG, "setPlaybackInformation() ignoring unknown key " + what); 710 break; 711 } 712 } 713 } 714 715 /** 716 * @hide 717 * Return playback information represented as an integer value. 718 * @param what a key to specify the type of information to retrieve. Valid keys are 719 * {@link #PLAYBACKINFO_PLAYBACK_TYPE}, 720 * {@link #PLAYBACKINFO_USES_STREAM}, 721 * {@link #PLAYBACKINFO_VOLUME}, 722 * {@link #PLAYBACKINFO_VOLUME_MAX}, 723 * and {@link #PLAYBACKINFO_VOLUME_HANDLING}. 724 * @return the current value for the given information type, or 725 * {@link #PLAYBACKINFO_INVALID_VALUE} if an error occurred or the request is invalid, or 726 * the value is unknown. 727 */ getIntPlaybackInformation(int what)728 public int getIntPlaybackInformation(int what) { 729 synchronized(mCacheLock) { 730 switch (what) { 731 case PLAYBACKINFO_PLAYBACK_TYPE: 732 return mPlaybackType; 733 case PLAYBACKINFO_VOLUME: 734 return mPlaybackVolume; 735 case PLAYBACKINFO_VOLUME_MAX: 736 return mPlaybackVolumeMax; 737 case PLAYBACKINFO_USES_STREAM: 738 return mPlaybackStream; 739 case PLAYBACKINFO_VOLUME_HANDLING: 740 return mPlaybackVolumeHandling; 741 default: 742 Log.e(TAG, "getIntPlaybackInformation() unknown key " + what); 743 return PLAYBACKINFO_INVALID_VALUE; 744 } 745 } 746 } 747 748 /** 749 * Lock for all cached data 750 */ 751 private final Object mCacheLock = new Object(); 752 /** 753 * Cache for the playback state. 754 * Access synchronized on mCacheLock 755 */ 756 private int mPlaybackState = PLAYSTATE_NONE; 757 /** 758 * Time of last play state change 759 * Access synchronized on mCacheLock 760 */ 761 private long mPlaybackStateChangeTimeMs = 0; 762 /** 763 * Cache for the artwork bitmap. 764 * Access synchronized on mCacheLock 765 * Artwork and metadata are not kept in one Bundle because the bitmap sometimes needs to be 766 * accessed to be resized, in which case a copy will be made. This would add overhead in 767 * Bundle operations. 768 */ 769 private Bitmap mArtwork; 770 private final int ARTWORK_DEFAULT_SIZE = 256; 771 private final int ARTWORK_INVALID_SIZE = -1; 772 private int mArtworkExpectedWidth = ARTWORK_DEFAULT_SIZE; 773 private int mArtworkExpectedHeight = ARTWORK_DEFAULT_SIZE; 774 /** 775 * Cache for the transport control mask. 776 * Access synchronized on mCacheLock 777 */ 778 private int mTransportControlFlags = FLAGS_KEY_MEDIA_NONE; 779 /** 780 * Cache for the metadata strings. 781 * Access synchronized on mCacheLock 782 * This is re-initialized in apply() and so cannot be final. 783 */ 784 private Bundle mMetadata = new Bundle(); 785 786 /** 787 * The current remote control client generation ID across the system 788 */ 789 private int mCurrentClientGenId = -1; 790 /** 791 * The remote control client generation ID, the last time it was told it was the current RC. 792 * If (mCurrentClientGenId == mInternalClientGenId) is true, it means that this remote control 793 * client is the "focused" one, and that whenever this client's info is updated, it needs to 794 * send it to the known IRemoteControlDisplay interfaces. 795 */ 796 private int mInternalClientGenId = -2; 797 798 /** 799 * The media button intent description associated with this remote control client 800 * (can / should include target component for intent handling) 801 */ 802 private final PendingIntent mRcMediaIntent; 803 804 /** 805 * The remote control display to which this client will send information. 806 * NOTE: Only one IRemoteControlDisplay supported in this implementation 807 */ 808 private IRemoteControlDisplay mRcDisplay; 809 810 /** 811 * @hide 812 * Accessor to media button intent description (includes target component) 813 */ getRcMediaIntent()814 public PendingIntent getRcMediaIntent() { 815 return mRcMediaIntent; 816 } 817 /** 818 * @hide 819 * Accessor to IRemoteControlClient 820 */ getIRemoteControlClient()821 public IRemoteControlClient getIRemoteControlClient() { 822 return mIRCC; 823 } 824 825 /** 826 * The IRemoteControlClient implementation 827 */ 828 private final IRemoteControlClient mIRCC = new IRemoteControlClient.Stub() { 829 830 public void onInformationRequested(int clientGeneration, int infoFlags, 831 int artWidth, int artHeight) { 832 // only post messages, we can't block here 833 if (mEventHandler != null) { 834 // signal new client 835 mEventHandler.removeMessages(MSG_NEW_INTERNAL_CLIENT_GEN); 836 mEventHandler.dispatchMessage( 837 mEventHandler.obtainMessage( 838 MSG_NEW_INTERNAL_CLIENT_GEN, 839 artWidth, artHeight, 840 new Integer(clientGeneration))); 841 // send the information 842 mEventHandler.removeMessages(MSG_REQUEST_PLAYBACK_STATE); 843 mEventHandler.removeMessages(MSG_REQUEST_METADATA); 844 mEventHandler.removeMessages(MSG_REQUEST_TRANSPORTCONTROL); 845 mEventHandler.removeMessages(MSG_REQUEST_ARTWORK); 846 mEventHandler.dispatchMessage( 847 mEventHandler.obtainMessage(MSG_REQUEST_PLAYBACK_STATE)); 848 mEventHandler.dispatchMessage( 849 mEventHandler.obtainMessage(MSG_REQUEST_TRANSPORTCONTROL)); 850 mEventHandler.dispatchMessage(mEventHandler.obtainMessage(MSG_REQUEST_METADATA)); 851 mEventHandler.dispatchMessage(mEventHandler.obtainMessage(MSG_REQUEST_ARTWORK)); 852 } 853 } 854 855 public void setCurrentClientGenerationId(int clientGeneration) { 856 // only post messages, we can't block here 857 if (mEventHandler != null) { 858 mEventHandler.removeMessages(MSG_NEW_CURRENT_CLIENT_GEN); 859 mEventHandler.dispatchMessage(mEventHandler.obtainMessage( 860 MSG_NEW_CURRENT_CLIENT_GEN, clientGeneration, 0/*ignored*/)); 861 } 862 } 863 864 public void plugRemoteControlDisplay(IRemoteControlDisplay rcd) { 865 // only post messages, we can't block here 866 if (mEventHandler != null) { 867 mEventHandler.dispatchMessage(mEventHandler.obtainMessage( 868 MSG_PLUG_DISPLAY, rcd)); 869 } 870 } 871 872 public void unplugRemoteControlDisplay(IRemoteControlDisplay rcd) { 873 // only post messages, we can't block here 874 if (mEventHandler != null) { 875 mEventHandler.dispatchMessage(mEventHandler.obtainMessage( 876 MSG_UNPLUG_DISPLAY, rcd)); 877 } 878 } 879 }; 880 881 /** 882 * @hide 883 * Default value for the unique identifier 884 */ 885 public final static int RCSE_ID_UNREGISTERED = -1; 886 /** 887 * Unique identifier of the RemoteControlStackEntry in AudioService with which 888 * this RemoteControlClient is associated. 889 */ 890 private int mRcseId = RCSE_ID_UNREGISTERED; 891 /** 892 * @hide 893 * To be only used by AudioManager after it has received the unique id from 894 * IAudioService.registerRemoteControlClient() 895 * @param id the unique identifier of the RemoteControlStackEntry in AudioService with which 896 * this RemoteControlClient is associated. 897 */ setRcseId(int id)898 public void setRcseId(int id) { 899 mRcseId = id; 900 } 901 902 /** 903 * @hide 904 */ getRcseId()905 public int getRcseId() { 906 return mRcseId; 907 } 908 909 private EventHandler mEventHandler; 910 private final static int MSG_REQUEST_PLAYBACK_STATE = 1; 911 private final static int MSG_REQUEST_METADATA = 2; 912 private final static int MSG_REQUEST_TRANSPORTCONTROL = 3; 913 private final static int MSG_REQUEST_ARTWORK = 4; 914 private final static int MSG_NEW_INTERNAL_CLIENT_GEN = 5; 915 private final static int MSG_NEW_CURRENT_CLIENT_GEN = 6; 916 private final static int MSG_PLUG_DISPLAY = 7; 917 private final static int MSG_UNPLUG_DISPLAY = 8; 918 919 private class EventHandler extends Handler { EventHandler(RemoteControlClient rcc, Looper looper)920 public EventHandler(RemoteControlClient rcc, Looper looper) { 921 super(looper); 922 } 923 924 @Override handleMessage(Message msg)925 public void handleMessage(Message msg) { 926 switch(msg.what) { 927 case MSG_REQUEST_PLAYBACK_STATE: 928 synchronized (mCacheLock) { 929 sendPlaybackState_syncCacheLock(); 930 } 931 break; 932 case MSG_REQUEST_METADATA: 933 synchronized (mCacheLock) { 934 sendMetadata_syncCacheLock(); 935 } 936 break; 937 case MSG_REQUEST_TRANSPORTCONTROL: 938 synchronized (mCacheLock) { 939 sendTransportControlFlags_syncCacheLock(); 940 } 941 break; 942 case MSG_REQUEST_ARTWORK: 943 synchronized (mCacheLock) { 944 sendArtwork_syncCacheLock(); 945 } 946 break; 947 case MSG_NEW_INTERNAL_CLIENT_GEN: 948 onNewInternalClientGen((Integer)msg.obj, msg.arg1, msg.arg2); 949 break; 950 case MSG_NEW_CURRENT_CLIENT_GEN: 951 onNewCurrentClientGen(msg.arg1); 952 break; 953 case MSG_PLUG_DISPLAY: 954 onPlugDisplay((IRemoteControlDisplay)msg.obj); 955 break; 956 case MSG_UNPLUG_DISPLAY: 957 onUnplugDisplay((IRemoteControlDisplay)msg.obj); 958 break; 959 default: 960 Log.e(TAG, "Unknown event " + msg.what + " in RemoteControlClient handler"); 961 } 962 } 963 } 964 965 //=========================================================== 966 // Communication with IRemoteControlDisplay 967 detachFromDisplay_syncCacheLock()968 private void detachFromDisplay_syncCacheLock() { 969 mRcDisplay = null; 970 mArtworkExpectedWidth = ARTWORK_INVALID_SIZE; 971 mArtworkExpectedHeight = ARTWORK_INVALID_SIZE; 972 } 973 sendPlaybackState_syncCacheLock()974 private void sendPlaybackState_syncCacheLock() { 975 if ((mCurrentClientGenId == mInternalClientGenId) && (mRcDisplay != null)) { 976 try { 977 mRcDisplay.setPlaybackState(mInternalClientGenId, mPlaybackState, 978 mPlaybackStateChangeTimeMs); 979 } catch (RemoteException e) { 980 Log.e(TAG, "Error in setPlaybackState(), dead display "+e); 981 detachFromDisplay_syncCacheLock(); 982 } 983 } 984 } 985 sendMetadata_syncCacheLock()986 private void sendMetadata_syncCacheLock() { 987 if ((mCurrentClientGenId == mInternalClientGenId) && (mRcDisplay != null)) { 988 try { 989 mRcDisplay.setMetadata(mInternalClientGenId, mMetadata); 990 } catch (RemoteException e) { 991 Log.e(TAG, "Error in sendPlaybackState(), dead display "+e); 992 detachFromDisplay_syncCacheLock(); 993 } 994 } 995 } 996 sendTransportControlFlags_syncCacheLock()997 private void sendTransportControlFlags_syncCacheLock() { 998 if ((mCurrentClientGenId == mInternalClientGenId) && (mRcDisplay != null)) { 999 try { 1000 mRcDisplay.setTransportControlFlags(mInternalClientGenId, 1001 mTransportControlFlags); 1002 } catch (RemoteException e) { 1003 Log.e(TAG, "Error in sendTransportControlFlags(), dead display "+e); 1004 detachFromDisplay_syncCacheLock(); 1005 } 1006 } 1007 } 1008 sendArtwork_syncCacheLock()1009 private void sendArtwork_syncCacheLock() { 1010 if ((mCurrentClientGenId == mInternalClientGenId) && (mRcDisplay != null)) { 1011 // even though we have already scaled in setArtwork(), when this client needs to 1012 // send the bitmap, there might be newer and smaller expected dimensions, so we have 1013 // to check again. 1014 mArtwork = scaleBitmapIfTooBig(mArtwork, mArtworkExpectedWidth, mArtworkExpectedHeight); 1015 try { 1016 mRcDisplay.setArtwork(mInternalClientGenId, mArtwork); 1017 } catch (RemoteException e) { 1018 Log.e(TAG, "Error in sendArtwork(), dead display "+e); 1019 detachFromDisplay_syncCacheLock(); 1020 } 1021 } 1022 } 1023 sendMetadataWithArtwork_syncCacheLock()1024 private void sendMetadataWithArtwork_syncCacheLock() { 1025 if ((mCurrentClientGenId == mInternalClientGenId) && (mRcDisplay != null)) { 1026 // even though we have already scaled in setArtwork(), when this client needs to 1027 // send the bitmap, there might be newer and smaller expected dimensions, so we have 1028 // to check again. 1029 mArtwork = scaleBitmapIfTooBig(mArtwork, mArtworkExpectedWidth, mArtworkExpectedHeight); 1030 try { 1031 mRcDisplay.setAllMetadata(mInternalClientGenId, mMetadata, mArtwork); 1032 } catch (RemoteException e) { 1033 Log.e(TAG, "Error in setAllMetadata(), dead display "+e); 1034 detachFromDisplay_syncCacheLock(); 1035 } 1036 } 1037 } 1038 1039 //=========================================================== 1040 // Communication with AudioService 1041 1042 private static IAudioService sService; 1043 getService()1044 private static IAudioService getService() 1045 { 1046 if (sService != null) { 1047 return sService; 1048 } 1049 IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); 1050 sService = IAudioService.Stub.asInterface(b); 1051 return sService; 1052 } 1053 sendAudioServiceNewPlaybackInfo_syncCacheLock(int what, int value)1054 private void sendAudioServiceNewPlaybackInfo_syncCacheLock(int what, int value) { 1055 if (mRcseId == RCSE_ID_UNREGISTERED) { 1056 return; 1057 } 1058 //Log.d(TAG, "sending to AudioService key=" + what + ", value=" + value); 1059 IAudioService service = getService(); 1060 try { 1061 service.setPlaybackInfoForRcc(mRcseId, what, value); 1062 } catch (RemoteException e) { 1063 Log.e(TAG, "Dead object in sendAudioServiceNewPlaybackInfo_syncCacheLock", e); 1064 } 1065 } 1066 1067 //=========================================================== 1068 // Message handlers 1069 onNewInternalClientGen(Integer clientGeneration, int artWidth, int artHeight)1070 private void onNewInternalClientGen(Integer clientGeneration, int artWidth, int artHeight) { 1071 synchronized (mCacheLock) { 1072 // this remote control client is told it is the "focused" one: 1073 // it implies that now (mCurrentClientGenId == mInternalClientGenId) is true 1074 mInternalClientGenId = clientGeneration.intValue(); 1075 if (artWidth > 0) { 1076 mArtworkExpectedWidth = artWidth; 1077 mArtworkExpectedHeight = artHeight; 1078 } 1079 } 1080 } 1081 onNewCurrentClientGen(int clientGeneration)1082 private void onNewCurrentClientGen(int clientGeneration) { 1083 synchronized (mCacheLock) { 1084 mCurrentClientGenId = clientGeneration; 1085 } 1086 } 1087 onPlugDisplay(IRemoteControlDisplay rcd)1088 private void onPlugDisplay(IRemoteControlDisplay rcd) { 1089 synchronized(mCacheLock) { 1090 mRcDisplay = rcd; 1091 } 1092 } 1093 onUnplugDisplay(IRemoteControlDisplay rcd)1094 private void onUnplugDisplay(IRemoteControlDisplay rcd) { 1095 synchronized(mCacheLock) { 1096 if ((mRcDisplay != null) && (mRcDisplay.asBinder().equals(rcd.asBinder()))) { 1097 mRcDisplay = null; 1098 mArtworkExpectedWidth = ARTWORK_DEFAULT_SIZE; 1099 mArtworkExpectedHeight = ARTWORK_DEFAULT_SIZE; 1100 } 1101 } 1102 } 1103 1104 //=========================================================== 1105 // Internal utilities 1106 1107 /** 1108 * Scale a bitmap to fit the smallest dimension by uniformly scaling the incoming bitmap. 1109 * If the bitmap fits, then do nothing and return the original. 1110 * 1111 * @param bitmap 1112 * @param maxWidth 1113 * @param maxHeight 1114 * @return 1115 */ 1116 scaleBitmapIfTooBig(Bitmap bitmap, int maxWidth, int maxHeight)1117 private Bitmap scaleBitmapIfTooBig(Bitmap bitmap, int maxWidth, int maxHeight) { 1118 if (bitmap != null) { 1119 final int width = bitmap.getWidth(); 1120 final int height = bitmap.getHeight(); 1121 if (width > maxWidth || height > maxHeight) { 1122 float scale = Math.min((float) maxWidth / width, (float) maxHeight / height); 1123 int newWidth = Math.round(scale * width); 1124 int newHeight = Math.round(scale * height); 1125 Bitmap.Config newConfig = bitmap.getConfig(); 1126 if (newConfig == null) { 1127 newConfig = Bitmap.Config.ARGB_8888; 1128 } 1129 Bitmap outBitmap = Bitmap.createBitmap(newWidth, newHeight, newConfig); 1130 Canvas canvas = new Canvas(outBitmap); 1131 Paint paint = new Paint(); 1132 paint.setAntiAlias(true); 1133 paint.setFilterBitmap(true); 1134 canvas.drawBitmap(bitmap, null, 1135 new RectF(0, 0, outBitmap.getWidth(), outBitmap.getHeight()), paint); 1136 bitmap = outBitmap; 1137 } 1138 } 1139 return bitmap; 1140 } 1141 1142 /** 1143 * Fast routine to go through an array of allowed keys and return whether the key is part 1144 * of that array 1145 * @param key the key value 1146 * @param validKeys the array of valid keys for a given type 1147 * @return true if the key is part of the array, false otherwise 1148 */ validTypeForKey(int key, int[] validKeys)1149 private static boolean validTypeForKey(int key, int[] validKeys) { 1150 try { 1151 for (int i = 0 ; ; i++) { 1152 if (key == validKeys[i]) { 1153 return true; 1154 } 1155 } 1156 } catch (ArrayIndexOutOfBoundsException e) { 1157 return false; 1158 } 1159 } 1160 } 1161