diff options
author | Jean-Michel Trivi <jmtrivi@google.com> | 2013-09-01 18:06:45 -0700 |
---|---|---|
committer | Jean-Michel Trivi <jmtrivi@google.com> | 2013-09-18 01:47:25 +0000 |
commit | 7ddd226e7c6e759feaf2747a90be1cc06acf37a3 (patch) | |
tree | 9d4f59a1a6bcfdd87e88a5156b72e1763248d788 /media/java | |
parent | 9b6459841e52b9d44ec8ec57af5eb8007841f93d (diff) | |
download | frameworks_base-7ddd226e7c6e759feaf2747a90be1cc06acf37a3.zip frameworks_base-7ddd226e7c6e759feaf2747a90be1cc06acf37a3.tar.gz frameworks_base-7ddd226e7c6e759feaf2747a90be1cc06acf37a3.tar.bz2 |
RemoteController class to expose IRemoteControlDisplay features
Wrap all the features of IRemoteControlDisplay.aidl in a
new class, RemoteController, that implements the
IRemoteControlDisplay interface.
The API functions to expose in the SDK are tagged with
"CANDIDATE FOR API"
Bug 8209392
Change-Id: I597bcd503ac93e73889c9ae8b47b16c4fcb363bc
Diffstat (limited to 'media/java')
-rw-r--r-- | media/java/android/media/AudioManager.java | 64 | ||||
-rw-r--r-- | media/java/android/media/AudioService.java | 15 | ||||
-rw-r--r-- | media/java/android/media/IAudioService.aidl | 8 | ||||
-rw-r--r-- | media/java/android/media/MediaFocusControl.java | 43 | ||||
-rw-r--r-- | media/java/android/media/Rating.java | 10 | ||||
-rw-r--r-- | media/java/android/media/RemoteControlClient.java | 2 | ||||
-rw-r--r-- | media/java/android/media/RemoteController.java | 796 |
7 files changed, 905 insertions, 33 deletions
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 4542643..a4009cf 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -444,6 +444,19 @@ public class AudioManager { /** * @hide + * @param KeyEvent + */ + protected void dispatchMediaKeyEvent(KeyEvent keyEvent) { + IAudioService service = getService(); + try { + service.dispatchMediaKeyEvent(keyEvent); + } catch (RemoteException e) { + Log.e(TAG, "dispatchMediaKeyEvent threw exception ", e); + } + } + + /** + * @hide */ public void preDispatchKeyEvent(KeyEvent event, int stream) { /* @@ -2235,6 +2248,49 @@ public class AudioManager { /** * @hide + * CANDIDATE FOR PUBLIC API + * @param rctlr + * @return true if the {@link RemoteController} was successfully registered, false if an + * error occurred, due to an internal system error, or insufficient permissions. + */ + public boolean registerRemoteController(RemoteController rctlr) { + if (rctlr == null) { + return false; + } + IAudioService service = getService(); + try { + boolean reg = service.registerRemoteControlDisplay(rctlr.getRcDisplay(), + // passing a negative value for art work width and height + // as they are still unknown at this stage + /*w*/-1, /*h*/ -1); + rctlr.setIsRegistered(reg); + return reg; + } catch (RemoteException e) { + Log.e(TAG, "Dead object in registerRemoteControlDisplay " + e); + return false; + } + } + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * @param rctlr + */ + public void unregisterRemoteController(RemoteController rctlr) { + if (rctlr == null) { + return; + } + IAudioService service = getService(); + try { + service.unregisterRemoteControlDisplay(rctlr.getRcDisplay()); + rctlr.setIsRegistered(false); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in unregisterRemoteControlDisplay " + e); + } + } + + /** + * @hide * Registers a remote control display that will be sent information by remote control clients. * Use this method if your IRemoteControlDisplay is not going to display artwork, otherwise * use {@link #registerRemoteControlDisplay(IRemoteControlDisplay, int, int)} to pass the @@ -2263,8 +2319,6 @@ public class AudioManager { } IAudioService service = getService(); try { - // passing a negative value for art work width and height as they are unknown at - // this stage service.registerRemoteControlDisplay(rcd, w, h); } catch (RemoteException e) { Log.e(TAG, "Dead object in registerRemoteControlDisplay " + e); @@ -2357,13 +2411,15 @@ public class AudioManager { /** * @hide - * Notify the user of a RemoteControlClient that it should update its metadata + * Notify the user of a RemoteControlClient that it should update its metadata with the + * new value for the given key. * @param generationId the RemoteControlClient generation counter for which this request is * issued. Requests for an older generation than current one will be ignored. * @param key the metadata key for which a new value exists * @param value the new metadata value */ - public void updateRemoteControlClientMetadata(int generationId, int key, long value) { + public void updateRemoteControlClientMetadata(int generationId, int key, + Rating value) { IAudioService service = getService(); try { service.updateRemoteControlClientMetadata(generationId, key, value); diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java index 07f0858..3425c91 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -4143,8 +4143,17 @@ public class AudioService extends IAudioService.Stub { //========================================================================================== // RemoteControlDisplay / RemoteControlClient / Remote info //========================================================================================== - public void registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) { - mMediaFocusControl.registerRemoteControlDisplay(rcd, w, h); + public boolean registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) { + if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( + android.Manifest.permission.MEDIA_CONTENT_CONTROL)) { + mMediaFocusControl.registerRemoteControlDisplay(rcd, w, h); + return true; + } else { + Log.w(TAG, "Access denied to process: " + Binder.getCallingPid() + + ", must have permission " + android.Manifest.permission.MEDIA_CONTENT_CONTROL + + " to register IRemoteControlDisplay"); + return false; + } } public void unregisterRemoteControlDisplay(IRemoteControlDisplay rcd) { @@ -4190,7 +4199,7 @@ public class AudioService extends IAudioService.Stub { mMediaFocusControl.setRemoteControlClientPlaybackPosition(generationId, timeMs); } - public void updateRemoteControlClientMetadata(int generationId, int key, long value) { + public void updateRemoteControlClientMetadata(int generationId, int key, Rating value) { mMediaFocusControl.updateRemoteControlClientMetadata(generationId, key, value); } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index e08ecbf..e3b87dd 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -26,6 +26,7 @@ import android.media.IRemoteControlClient; import android.media.IRemoteControlDisplay; import android.media.IRemoteVolumeObserver; import android.media.IRingtonePlayer; +import android.media.Rating; import android.net.Uri; import android.view.KeyEvent; @@ -140,7 +141,7 @@ interface IAudioService { * @param h the maximum height of the expected bitmap. Negative or zero values indicate this * display doesn't need to receive artwork. */ - oneway void registerRemoteControlDisplay(in IRemoteControlDisplay rcd, int w, int h); + boolean registerRemoteControlDisplay(in IRemoteControlDisplay rcd, int w, int h); /** * Unregister an IRemoteControlDisplay. * No effect if the IRemoteControlDisplay hasn't been successfully registered. @@ -178,13 +179,14 @@ interface IAudioService { */ void setRemoteControlClientPlaybackPosition(int generationId, long timeMs); /** - * Notify the user of a RemoteControlClient that it should update its metadata + * Notify the user of a RemoteControlClient that it should update its metadata with the + * new value for the given key. * @param generationId the RemoteControlClient generation counter for which this request is * issued. Requests for an older generation than current one will be ignored. * @param key the metadata key for which a new value exists * @param value the new metadata value */ - void updateRemoteControlClientMetadata(int generationId, int key, long value); + void updateRemoteControlClientMetadata(int generationId, int key, in Rating value); /** * Do not use directly, use instead diff --git a/media/java/android/media/MediaFocusControl.java b/media/java/android/media/MediaFocusControl.java index 34dc580..143ddf2 100644 --- a/media/java/android/media/MediaFocusControl.java +++ b/media/java/android/media/MediaFocusControl.java @@ -138,7 +138,7 @@ public class MediaFocusControl implements OnFinished { private static final int MSG_PROMOTE_RCC = 6; private static final int MSG_RCC_NEW_PLAYBACK_STATE = 7; private static final int MSG_RCC_SEEK_REQUEST = 8; - private static final int MSG_RCC_UPDATE_METADATA_LONG = 9; + private static final int MSG_RCC_UPDATE_METADATA = 9; // sendMsg() flags /** If the msg is already queued, replace it with this one. */ @@ -206,9 +206,9 @@ public class MediaFocusControl implements OnFinished { msg.arg1 /* generationId */, ((Long)msg.obj).longValue() /* timeMs */); break; - case MSG_RCC_UPDATE_METADATA_LONG: - onUpdateRemoteControlClientMetadataLong(msg.arg1 /*genId*/, msg.arg2 /*key*/, - ((Long)msg.obj).longValue() /* value */); + case MSG_RCC_UPDATE_METADATA: + onUpdateRemoteControlClientMetadata(msg.arg1 /*genId*/, msg.arg2 /*key*/, + (Rating) msg.obj /* value */); break; case MSG_PROMOTE_RCC: @@ -720,11 +720,7 @@ public class MediaFocusControl implements OnFinished { } } - private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) { - if (keyEvent == null) { - return false; - } - final int keyCode = keyEvent.getKeyCode(); + protected static boolean isMediaKeyCode(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_MUTE: case KeyEvent.KEYCODE_HEADSETHOOK: @@ -740,11 +736,17 @@ public class MediaFocusControl implements OnFinished { case KeyEvent.KEYCODE_MEDIA_CLOSE: case KeyEvent.KEYCODE_MEDIA_EJECT: case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: - break; + return true; default: return false; } - return true; + } + + private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) { + if (keyEvent == null) { + return false; + } + return MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode()); } /** @@ -2080,24 +2082,21 @@ public class MediaFocusControl implements OnFinished { } } - protected void updateRemoteControlClientMetadata(int genId, int key, long value) { - sendMsg(mEventHandler, MSG_RCC_UPDATE_METADATA_LONG, SENDMSG_QUEUE, - genId /* arg1 */, key /* arg2 */, Long.valueOf(value) /* obj */, 0 /* delay */); + protected void updateRemoteControlClientMetadata(int genId, int key, Rating value) { + sendMsg(mEventHandler, MSG_RCC_UPDATE_METADATA, SENDMSG_QUEUE, + genId /* arg1 */, key /* arg2 */, value /* obj */, 0 /* delay */); } - private void onUpdateRemoteControlClientMetadataLong(int genId, int key, long value) { - if(DEBUG_RC) Log.d(TAG, "onUpdateRemoteControlClientMetadataLong(genId=" + genId + - ", what=" + key + ",val=" + value + ")"); + private void onUpdateRemoteControlClientMetadata(int genId, int key, Rating value) { + if(DEBUG_RC) Log.d(TAG, "onUpdateRemoteControlClientMetadata(genId=" + genId + + ", what=" + key + ",rating=" + value + ")"); synchronized(mRCStack) { synchronized(mCurrentRcLock) { if ((mCurrentRcClient != null) && (mCurrentRcClientGen == genId)) { try { switch (key) { - case RemoteControlClient.MetadataEditor.RATING_KEY_BY_USER: - // TODO handle rating update, placeholder code here that sends - // an unrated percent-based rating - mCurrentRcClient.updateMetadata(genId, key, - Rating.newUnratedRating(Rating.RATING_PERCENTAGE)); + case MediaMetadataEditor.RATING_KEY_BY_USER: + mCurrentRcClient.updateMetadata(genId, key, value); break; default: Log.e(TAG, "unhandled metadata key " + key + " update for RCC " diff --git a/media/java/android/media/Rating.java b/media/java/android/media/Rating.java index 48443ff..82c0392 100644 --- a/media/java/android/media/Rating.java +++ b/media/java/android/media/Rating.java @@ -75,6 +75,16 @@ public final class Rating implements Parcelable { mRatingValue = rating; } + + /** + * @hide + */ + @Override + public String toString () { + return "Rating:style=" + mRatingStyle + " rating=" + + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue)); + } + @Override public int describeContents() { return mRatingStyle; diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java index f8faf3a..4b5a1ca 100644 --- a/media/java/android/media/RemoteControlClient.java +++ b/media/java/android/media/RemoteControlClient.java @@ -715,7 +715,7 @@ public class RemoteControlClient * Implement this interface to receive metadata updates after registering your listener * through {@link RemoteControlClient#setMetadataUpdateListener(OnMetadataUpdateListener)}. */ - public interface OnMetadataUpdateListener { + public interface OnMetadataUpdateListener { /** * Called on the implementer to notify that the metadata field for the given key has * been updated to the new value. diff --git a/media/java/android/media/RemoteController.java b/media/java/android/media/RemoteController.java new file mode 100644 index 0000000..6266160 --- /dev/null +++ b/media/java/android/media/RemoteController.java @@ -0,0 +1,796 @@ +/* + * Copyright (C) 2013 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.media; + +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.media.IRemoteControlDisplay; +import android.media.MediaMetadataEditor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; +import android.view.KeyEvent; + +/** + * @hide + * CANDIDATE FOR PUBLIC API + * The RemoteController class is used to control media playback, display and update media metadata + * and playback status, published by applications using the {@link RemoteControlClient} class. + * <p> + * A RemoteController shall be registered through + * {@link AudioManager#registerRemoteController(RemoteController)} in order for the system to send + * media event updates to the listener set in + * {@link #setOnClientUpdateListener(OnClientUpdateListener)}. This listener is a subclass of + * the {@link OnClientUpdateListener} abstract class. Override its methods to receive the + * information published by the active {@link RemoteControlClient} instances. + * By default an {@link OnClientUpdateListener} implementation will not receive bitmaps for album + * art. Use {@link #setBitmapConfiguration(boolean, int, int)} to receive images as well. + * <p> + * A RemoteController can also be used without being registered, when it is only meant to send + * media key events (for play or stop events for instance), + * with {@link #sendMediaKeyEvent(KeyEvent)}. + */ +public class RemoteController +{ + private final static int MAX_BITMAP_DIMENSION = 512; + private final static int TRANSPORT_UNKNOWN = 0; + private RcDisplay mRcd; + private final static String TAG = "RemoteController"; + private final static boolean DEBUG = false; + private final static Object mGenLock = new Object(); + private final static Object mInfoLock = new Object(); + private Context mContext; + private AudioManager mAudioManager; + private MetadataEditor mMetadataEditor; + + /** + * Synchronized on mGenLock + */ + private int mClientGenerationIdCurrent = 0; + + /** + * Synchronized on mInfoLock + */ + private boolean mIsRegistered = false; + private PendingIntent mClientPendingIntentCurrent; + private OnClientUpdateListener mOnClientUpdateListener; + private PlaybackInfo mLastPlaybackInfo; + private int mLastTransportControlFlags = TRANSPORT_UNKNOWN; + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * @param ctxt non-null {@link Context} + * @throws java.lang.IllegalArgumentException + */ + public RemoteController(Context ctxt) throws IllegalArgumentException { + if (ctxt == null) { + throw new IllegalArgumentException("Invalid null Context"); + } + Looper looper; + if ((looper = Looper.myLooper()) != null) { + mEventHandler = new EventHandler(this, looper); + } else if ((looper = Looper.getMainLooper()) != null) { + mEventHandler = new EventHandler(this, looper); + } else { + mEventHandler = null; + Log.e(TAG, "RemoteController() couldn't find main application thread"); + } + mContext = ctxt; + mRcd = new RcDisplay(); + mAudioManager = (AudioManager) ctxt.getSystemService(Context.AUDIO_SERVICE); + } + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * @param looper + * @param ctxt non-null {@link Context} + * @throws java.lang.IllegalArgumentException + */ + public RemoteController(Looper looper, Context ctxt) throws IllegalArgumentException { + if (ctxt == null) { + throw new IllegalArgumentException("Invalid null Context"); + } + mEventHandler = new EventHandler(this, looper); + mContext = ctxt; + mRcd = new RcDisplay(); + mAudioManager = (AudioManager) ctxt.getSystemService(Context.AUDIO_SERVICE); + } + + + /** + * @hide + * CANDIDATE FOR PUBLIC API + */ + public static abstract class OnClientUpdateListener { + /** + * @hide + * CANDIDATE FOR PUBLIC API + * @param clearing + */ + public void onClientReset(boolean clearing) { } + /** + * @hide + * CANDIDATE FOR PUBLIC API + * @param state + */ + public void onClientPlaybackStateUpdate(int state) { } + /** + * @hide + * CANDIDATE FOR PUBLIC API + * @param state + * @param stateChangeTimeMs + * @param currentPosMs + * @param speed + */ + public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, + long currentPosMs, float speed) { } + /** + * @hide + * CANDIDATE FOR PUBLIC API + * @param transportControlFlags + * @param posCapabilities + */ + public void onClientTransportControlUpdate(int transportControlFlags) { } + /** + * @hide + * CANDIDATE FOR PUBLIC API + * @param metadataEditor + */ + public void onClientMetadataUpdate(MetadataEditor metadataEditor) { } + }; + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * @param l + */ + public void setOnClientUpdateListener(OnClientUpdateListener l) { + synchronized(mInfoLock) { + mOnClientUpdateListener = l; + if (!mIsRegistered) { + // since the object is not registered, it hasn't received any information from + // RemoteControlClients yet, so we can exit here. + return; + } + if (mLastPlaybackInfo != null) { + sendMsg(mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE, + mClientGenerationIdCurrent /*arg1*/, 0, + mLastPlaybackInfo /*obj*/, 0 /*delay*/); + } + if (mLastTransportControlFlags != TRANSPORT_UNKNOWN) { + sendMsg(mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE, + mClientGenerationIdCurrent /*arg1*/, mLastTransportControlFlags /*arg2*/, + null /*obj*/, 0 /*delay*/); + } + if (mMetadataEditor != null) { + sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, + mClientGenerationIdCurrent /*arg1*/, 0 /*arg2*/, + mMetadataEditor /*obj*/, 0 /*delay*/); + } + } + } + + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * Send a simulated key event for a media button. + * May be used without registering the RemoteController + * with {@link AudioManager#registerRemoteController(RemoteController)}. To simulate a key + * press, you must first send a KeyEvent built with a {@link KeyEvent#ACTION_DOWN} action, then + * another event with the {@link KeyEvent#ACTION_UP} action. + * <p> When used from a registered RemoteController, the key event will be sent to the + * application currently promoted to publish its media metadata and playback state (there may be + * none under some circumstances). With an unregistered RemoteController, the key event will be + * sent to the current media key event consumer + * (see {@link AudioManager#registerMediaButtonEventReceiver(PendingIntent)}). + * @param keyEvent a {@link KeyEvent} instance whose key code is one of + * {@link KeyEvent.KEYCODE_MUTE}, + * {@link KeyEvent.KEYCODE_HEADSETHOOK}, + * {@link KeyEvent.KEYCODE_MEDIA_PLAY}, + * {@link KeyEvent.KEYCODE_MEDIA_PAUSE}, + * {@link KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE}, + * {@link KeyEvent.KEYCODE_MEDIA_STOP}, + * {@link KeyEvent.KEYCODE_MEDIA_NEXT}, + * {@link KeyEvent.KEYCODE_MEDIA_PREVIOUS}, + * {@link KeyEvent.KEYCODE_MEDIA_REWIND}, + * {@link KeyEvent.KEYCODE_MEDIA_RECORD}, + * {@link KeyEvent.KEYCODE_MEDIA_FAST_FORWARD}, + * {@link KeyEvent.KEYCODE_MEDIA_CLOSE}, + * {@link KeyEvent.KEYCODE_MEDIA_EJECT}, + * or {@link KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK}. + */ + public int sendMediaKeyEvent(KeyEvent keyEvent) { + if (!MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode())) { + Log.e(TAG, "Cannot use sendMediaKeyEvent() for a non-media key event"); + return ERROR_BAD_VALUE; + } + boolean registered = false; + final PendingIntent pi; + synchronized(mInfoLock) { + registered = mIsRegistered; + pi = mClientPendingIntentCurrent; + } + if (registered) { + if (pi != null) { + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); + try { + pi.send(mContext, 0, intent); + } catch (CanceledException e) { + Log.e(TAG, "Error sending intent for media button down: ", e); + return ERROR; + } + } else { + Log.i(TAG, "No-op when sending key click, no receiver right now"); + return ERROR; + } + } else { + mAudioManager.dispatchMediaKeyEvent(keyEvent); + } + return SUCCESS; + } + + + // Error codes + /** + * @hide + * CANDIDATE FOR PUBLIC API + * Successful operation. + */ + public static final int SUCCESS = 0; + /** + * @hide + * CANDIDATE FOR PUBLIC API + * Unspecified error. + */ + public static final int ERROR = -1; + /** + * @hide + * CANDIDATE FOR PUBLIC API + * Operation failed due to bad parameter value. + */ + public static final int ERROR_BAD_VALUE = -2; + + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * @param timeMs + * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE} + */ + public int seekTo(long timeMs) { + if (timeMs < 0) { + return ERROR_BAD_VALUE; + } + final int genId; + synchronized (mGenLock) { + genId = mClientGenerationIdCurrent; + } + mAudioManager.setRemoteControlClientPlaybackPosition(genId, timeMs); + return SUCCESS; + } + + + /** + * @hide + * must be called on a registered RemoteController + * @param wantBitmap + * @param width + * @param height + * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE} + */ + public int setBitmapConfiguration(boolean wantBitmap, int width, int height) { + synchronized (mInfoLock) { + if (!mIsRegistered) { + Log.e(TAG, "Cannot specify bitmap configuration on unregistered RemoteController"); + return ERROR; + } + } + if (wantBitmap) { + if ((width > 0) && (height > 0)) { + if (width > MAX_BITMAP_DIMENSION) { width = MAX_BITMAP_DIMENSION; } + if (height > MAX_BITMAP_DIMENSION) { height = MAX_BITMAP_DIMENSION; } + mAudioManager.remoteControlDisplayUsesBitmapSize(mRcd, width, height); + } else { + Log.e(TAG, "Invalid dimensions"); + return ERROR_BAD_VALUE; + } + } else { + mAudioManager.remoteControlDisplayUsesBitmapSize(mRcd, -1, -1); + } + return SUCCESS; + } + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * must be called on a registered RemoteController + * @param width + * @param height + * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE} + */ + public int setBitmapConfiguration(int width, int height) { + return setBitmapConfiguration(true, width, height); + } + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * must be called on a registered RemoteController + * @return {@link #SUCCESS}, {@link #ERROR} + */ + public int setBitmapConfigurationNone() { + return setBitmapConfiguration(false, -1, -1); + } + + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * Default playback position synchronization mode where the RemoteControlClient is not + * asked regularly for its playback position to see if it has drifted from the estimated + * position. + */ + public static final int POSITION_SYNCHRONIZATION_NONE = 0; + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * The playback position synchronization mode where the RemoteControlClient instances which + * expose their playback position to the framework, will be regularly polled to check + * whether any drift has been noticed between their estimated position and the one they report. + * Note that this mode should only ever be used when needing to display very accurate playback + * position, as regularly polling a RemoteControlClient for its position may have an impact + * on battery life (if applicable) when this query will trigger network transactions in the + * case of remote playback. + */ + public static final int POSITION_SYNCHRONIZATION_CHECK = 1; + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * Set the playback position synchronization mode. + * Must be called on a registered RemoteController. + * @param sync {@link #POSITION_SYNCHRONIZATION_NONE} or {@link #POSITION_SYNCHRONIZATION_CHECK} + * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE} + */ + public int setSynchronizationMode(int sync) { + if ((sync != POSITION_SYNCHRONIZATION_NONE) || (sync != POSITION_SYNCHRONIZATION_CHECK)) { + Log.e(TAG, "Unknown synchronization mode"); + return ERROR_BAD_VALUE; + } + if (!mIsRegistered) { + Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController"); + return ERROR; + } + mAudioManager.remoteControlDisplayWantsPlaybackPositionSync(mRcd, + POSITION_SYNCHRONIZATION_CHECK == sync); + return SUCCESS; + } + + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * Creates a {@link MetadataEditor} for updating metadata values of the editable keys of + * the current {@link RemoteControlClient}. + * @return a new MetadataEditor instance. + */ + public MetadataEditor editMetadata() { + MetadataEditor editor = new MetadataEditor(); + editor.mEditorMetadata = new Bundle(); + editor.mEditorArtwork = null; + editor.mMetadataChanged = true; + editor.mArtworkChanged = true; + editor.mEditableKeys = 0; + return editor; + } + + + /** + * @hide + * CANDIDATE FOR PUBLIC API + * Used to read the metadata published by a {@link RemoteControlClient}, or send a + * {@link RemoteControlClient} new values for keys that can be edited. + */ + public class MetadataEditor extends MediaMetadataEditor { + /** + * @hide + */ + protected MetadataEditor() { } + + /** + * @hide + */ + protected MetadataEditor(Bundle metadata, long editableKeys) { + mEditorMetadata = metadata; + mEditableKeys = editableKeys; + mEditorArtwork = null; + mMetadataChanged = true; + mArtworkChanged = true; + mApplied = false; + } + + /** + * @hide + * CANDIDATE FOR PUBLIC API + */ + public synchronized void apply() { + // "applying" a metadata bundle in RemoteController is only for sending edited + // key values back to the RemoteControlClient, so here we only care about the only + // editable key we support: RATING_KEY_BY_USER + if (!mMetadataChanged) { + return; + } + final int genId; + synchronized(mGenLock) { + genId = mClientGenerationIdCurrent; + } + synchronized(mInfoLock) { + if (mEditorMetadata.containsKey( + String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) { + Rating rating = (Rating) getObject( + MediaMetadataEditor.RATING_KEY_BY_USER, null); + mAudioManager.updateRemoteControlClientMetadata(genId, + MediaMetadataEditor.RATING_KEY_BY_USER, + rating); + } else { + Log.e(TAG, "no metadata to apply"); + } + // NOT setting mApplied to true as this type of MetadataEditor will be applied + // multiple times, whenever the user of a RemoteController needs to change the + // metadata (e.g. user changes the rating of a song more than once during playback) + mApplied = false; + } + } + + } + + + //================================================== + // Implementation of IRemoteControlDisplay interface + private class RcDisplay extends IRemoteControlDisplay.Stub { + /** + * @hide + */ + public void setCurrentClientId(int genId, PendingIntent clientMediaIntent, + boolean clearing) { + boolean isNew = false; + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + mClientGenerationIdCurrent = genId; + isNew = true; + } + } + if (clientMediaIntent != null) { + sendMsg(mEventHandler, MSG_NEW_PENDING_INTENT, SENDMSG_REPLACE, + genId /*arg1*/, 0, clientMediaIntent /*obj*/, 0 /*delay*/); + } + if (isNew || clearing) { + sendMsg(mEventHandler, MSG_CLIENT_RESET, SENDMSG_REPLACE, + genId /*arg1*/, clearing ? 1 : 0, null /*obj*/, 0 /*delay*/); + } + } + + /** + * @hide + */ + public void setPlaybackState(int genId, int state, + long stateChangeTimeMs, long currentPosMs, float speed) { + if (DEBUG) { + Log.d(TAG, "> new playback state: genId="+genId + + " state="+ state + + " changeTime="+ stateChangeTimeMs + + " pos=" + currentPosMs + + "ms speed=" + speed); + } + + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + return; + } + } + final PlaybackInfo playbackInfo = + new PlaybackInfo(state, stateChangeTimeMs, currentPosMs, speed); + sendMsg(mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE, + genId /*arg1*/, 0, playbackInfo /*obj*/, 0 /*delay*/); + + } + + /** + * @hide + */ + public void setTransportControlInfo(int genId, int transportControlFlags, + int posCapabilities) { + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + return; + } + } + sendMsg(mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE, + genId /*arg1*/, transportControlFlags /*arg2*/, + null /*obj*/, 0 /*delay*/); + } + + /** + * @hide + */ + public void setMetadata(int genId, Bundle metadata) { + if (DEBUG) { Log.e(TAG, "setMetadata("+genId+")"); } + if (metadata == null) { + return; + } + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + return; + } + } + sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, + genId /*arg1*/, 0 /*arg2*/, + metadata /*obj*/, 0 /*delay*/); + } + + /** + * @hide + */ + public void setArtwork(int genId, Bitmap artwork) { + if (DEBUG) { Log.v(TAG, "setArtwork("+genId+")"); } + if (artwork == null) { + return; + } + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + return; + } + } + Bundle metadata = new Bundle(1); + metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK), artwork); + sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, + genId /*arg1*/, 0 /*arg2*/, + metadata /*obj*/, 0 /*delay*/); + } + + /** + * @hide + */ + public void setAllMetadata(int genId, Bundle metadata, Bitmap artwork) { + if (DEBUG) { Log.e(TAG, "setAllMetadata("+genId+")"); } + if ((metadata == null) && (artwork == null)) { + return; + } + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + return; + } + } + if (metadata == null) { + metadata = new Bundle(1); + } + if (artwork != null) { + metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK), + artwork); + } + sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, + genId /*arg1*/, 0 /*arg2*/, + metadata /*obj*/, 0 /*delay*/); + } + } + + //================================================== + // Event handling + private EventHandler mEventHandler; + private final static int MSG_NEW_PENDING_INTENT = 0; + private final static int MSG_NEW_PLAYBACK_INFO = 1; + private final static int MSG_NEW_TRANSPORT_INFO = 2; + private final static int MSG_NEW_METADATA = 3; // msg always has non-null obj parameter + private final static int MSG_CLIENT_RESET = 4; + + private class EventHandler extends Handler { + + public EventHandler(RemoteController rc, Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_NEW_PENDING_INTENT: + onNewPendingIntent(msg.arg1, (PendingIntent) msg.obj); + break; + case MSG_NEW_PLAYBACK_INFO: + onNewPlaybackInfo(msg.arg1, (PlaybackInfo) msg.obj); + break; + case MSG_NEW_TRANSPORT_INFO: + onNewTransportInfo(msg.arg1, msg.arg2); + break; + case MSG_NEW_METADATA: + onNewMetadata(msg.arg1, (Bundle)msg.obj); + break; + case MSG_CLIENT_RESET: + onClientReset(msg.arg1, msg.arg2 == 1); + break; + default: + Log.e(TAG, "unknown event " + msg.what); + } + } + } + + /** If the msg is already queued, replace it with this one. */ + private static final int SENDMSG_REPLACE = 0; + /** If the msg is already queued, ignore this one and leave the old. */ + private static final int SENDMSG_NOOP = 1; + /** If the msg is already queued, queue this one and leave the old. */ + private static final int SENDMSG_QUEUE = 2; + + private static void sendMsg(Handler handler, int msg, int existingMsgPolicy, + int arg1, int arg2, Object obj, int delayMs) { + if (handler == null) { + Log.e(TAG, "null event handler, will not deliver message " + msg); + return; + } + if (existingMsgPolicy == SENDMSG_REPLACE) { + handler.removeMessages(msg); + } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) { + return; + } + handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delayMs); + } + + private void onNewPendingIntent(int genId, PendingIntent pi) { + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + return; + } + } + synchronized(mInfoLock) { + mClientPendingIntentCurrent = pi; + } + } + + private void onNewPlaybackInfo(int genId, PlaybackInfo pi) { + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + return; + } + } + final OnClientUpdateListener l; + synchronized(mInfoLock) { + l = this.mOnClientUpdateListener; + mLastPlaybackInfo = pi; + } + if (l != null) { + if (pi.mCurrentPosMs == RemoteControlClient.PLAYBACK_POSITION_ALWAYS_UNKNOWN) { + l.onClientPlaybackStateUpdate(pi.mState); + } else { + l.onClientPlaybackStateUpdate(pi.mState, pi.mStateChangeTimeMs, pi.mCurrentPosMs, + pi.mSpeed); + } + } + } + + private void onNewTransportInfo(int genId, int transportControlFlags) { + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + return; + } + } + final OnClientUpdateListener l; + synchronized(mInfoLock) { + l = mOnClientUpdateListener; + mLastTransportControlFlags = transportControlFlags; + } + if (l != null) { + l.onClientTransportControlUpdate(transportControlFlags); + } + } + + /** + * @param genId + * @param metadata guaranteed to be always non-null + */ + private void onNewMetadata(int genId, Bundle metadata) { + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + return; + } + } + final OnClientUpdateListener l; + final MetadataEditor metadataEditor; + // prepare the received Bundle to be used inside a MetadataEditor + final long editableKeys = metadata.getLong( + String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK), 0); + if (editableKeys != 0) { + metadata.remove(String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK)); + } + synchronized(mInfoLock) { + l = mOnClientUpdateListener; + if ((mMetadataEditor != null) && (mMetadataEditor.mEditorMetadata != null)) { + if (mMetadataEditor.mEditorMetadata != metadata) { + // existing metadata, merge existing and new + mMetadataEditor.mEditorMetadata.putAll(metadata); + } + } else { + mMetadataEditor = new MetadataEditor(metadata, editableKeys); + } + metadataEditor = mMetadataEditor; + } + if (l != null) { + l.onClientMetadataUpdate(metadataEditor); + } + } + + private void onClientReset(int genId, boolean clearing) { + synchronized(mGenLock) { + if (mClientGenerationIdCurrent != genId) { + return; + } + } + final OnClientUpdateListener l; + synchronized(mInfoLock) { + l = mOnClientUpdateListener; + } + if (l != null) { + l.onClientReset(clearing); + } + } + + + //================================================== + private static class PlaybackInfo { + int mState; + long mStateChangeTimeMs; + long mCurrentPosMs; + float mSpeed; + + PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed) { + mState = state; + mStateChangeTimeMs = stateChangeTimeMs; + mCurrentPosMs = currentPosMs; + mSpeed = speed; + } + } + + /** + * @hide + * Used by AudioManager to mark this instance as registered. + * @param registered + */ + protected void setIsRegistered(boolean registered) { + synchronized (mInfoLock) { + mIsRegistered = registered; + } + } + + /** + * @hide + * Used by AudioManager to access binder to be registered/unregistered inside MediaFocusControl + * @return + */ + protected RcDisplay getRcDisplay() { + return mRcd; + } +} |