diff options
author | Jean-Michel Trivi <jmtrivi@google.com> | 2013-10-07 17:04:20 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2013-10-07 17:04:20 +0000 |
commit | bacb5422bc4670d2b8905d9f58d068a97836561f (patch) | |
tree | 50c74719b3ec38cc68516f148b70662cda9b4196 /media/java | |
parent | d5102f19653f6b00ce872435c5efb5cb630deed7 (diff) | |
parent | f108cdd9ee5efe354d87edd02a07b323298c116c (diff) | |
download | frameworks_base-bacb5422bc4670d2b8905d9f58d068a97836561f.zip frameworks_base-bacb5422bc4670d2b8905d9f58d068a97836561f.tar.gz frameworks_base-bacb5422bc4670d2b8905d9f58d068a97836561f.tar.bz2 |
Merge "Also rely on enabled notification listeners for RemoteController registration" into klp-dev
Diffstat (limited to 'media/java')
-rw-r--r-- | media/java/android/media/AudioManager.java | 16 | ||||
-rw-r--r-- | media/java/android/media/AudioService.java | 18 | ||||
-rw-r--r-- | media/java/android/media/IAudioService.aidl | 12 | ||||
-rw-r--r-- | media/java/android/media/IRemoteControlClient.aidl | 1 | ||||
-rw-r--r-- | media/java/android/media/IRemoteControlDisplay.aidl | 6 | ||||
-rw-r--r-- | media/java/android/media/MediaFocusControl.java | 222 | ||||
-rw-r--r-- | media/java/android/media/RemoteControlClient.java | 102 | ||||
-rw-r--r-- | media/java/android/media/RemoteController.java | 203 |
8 files changed, 427 insertions, 153 deletions
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index c8ee5ad..680e73a 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -24,6 +24,7 @@ import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.media.RemoteController.OnClientUpdateListener; import android.os.Binder; import android.os.Build; import android.os.Handler; @@ -2270,7 +2271,9 @@ public class AudioManager { * Registers a {@link RemoteController} instance for it to receive media metadata updates * and playback state information from applications using {@link RemoteControlClient}, and * control their playback. - * <p>Registration requires the {@link Manifest.permission#MEDIA_CONTENT_CONTROL} permission. + * <p>Registration requires the {@link OnClientUpdateListener} listener to be one of the + * enabled notification listeners (see + * {@link android.service.notification.NotificationListenerService}). * @param rctlr the object to register. * @return true if the {@link RemoteController} was successfully registered, false if an * error occurred, due to an internal system error, or insufficient permissions. @@ -2280,14 +2283,17 @@ public class AudioManager { return false; } IAudioService service = getService(); + final RemoteController.OnClientUpdateListener l = rctlr.getUpdateListener(); + final ComponentName listenerComponent = new ComponentName(mContext, l.getClass()); try { int[] artworkDimensions = rctlr.getArtworkSize(); - boolean reg = service.registerRemoteControlDisplay(rctlr.getRcDisplay(), - artworkDimensions[0]/*w*/, artworkDimensions[1]/*h*/); + boolean reg = service.registerRemoteController(rctlr.getRcDisplay(), + artworkDimensions[0]/*w*/, artworkDimensions[1]/*h*/, + listenerComponent); rctlr.setIsRegistered(reg); return reg; } catch (RemoteException e) { - Log.e(TAG, "Dead object in registerRemoteControlDisplay " + e); + Log.e(TAG, "Dead object in registerRemoteController " + e); return false; } } @@ -2318,6 +2324,7 @@ public class AudioManager { * artwork size directly, or * {@link #remoteControlDisplayUsesBitmapSize(IRemoteControlDisplay, int, int)} later if artwork * is not yet needed. + * <p>Registration requires the {@link Manifest.permission#MEDIA_CONTENT_CONTROL} permission. * @param rcd the IRemoteControlDisplay */ public void registerRemoteControlDisplay(IRemoteControlDisplay rcd) { @@ -2328,6 +2335,7 @@ public class AudioManager { /** * @hide * Registers a remote control display that will be sent information by remote control clients. + * <p>Registration requires the {@link Manifest.permission#MEDIA_CONTENT_CONTROL} permission. * @param rcd * @param w the maximum width of the expected bitmap. Negative values indicate it is * useless to send artwork. diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java index 49c838c..79814d1 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -48,6 +48,7 @@ import android.content.res.XmlResourceParser; import android.database.ContentObserver; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; +import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; @@ -87,6 +88,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.concurrent.ConcurrentHashMap; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -4162,17 +4164,13 @@ public class AudioService extends IAudioService.Stub { //========================================================================================== // RemoteControlDisplay / RemoteControlClient / Remote info //========================================================================================== + public boolean registerRemoteController(IRemoteControlDisplay rcd, int w, int h, + ComponentName listenerComp) { + return mMediaFocusControl.registerRemoteController(rcd, w, h, listenerComp); + } + 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; - } + return mMediaFocusControl.registerRemoteControlDisplay(rcd, w, h); } public void unregisterRemoteControlDisplay(IRemoteControlDisplay rcd) { diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index e3b87dd..8c05089 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -133,6 +133,8 @@ interface IAudioService { /** * Register an IRemoteControlDisplay. + * Success of registration is subject to a check on + * the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission. * Notify all IRemoteControlClient of the new display and cause the RemoteControlClient * at the top of the stack to update the new display with its information. * @param rcd the IRemoteControlDisplay to register. No effect if null. @@ -142,6 +144,16 @@ interface IAudioService { * display doesn't need to receive artwork. */ boolean registerRemoteControlDisplay(in IRemoteControlDisplay rcd, int w, int h); + + /** + * Like registerRemoteControlDisplay, but with success being subject to a check on + * the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission, and if it fails, + * success is subject to listenerComp being one of the ENABLED_NOTIFICATION_LISTENERS + * components. + */ + boolean registerRemoteController(in IRemoteControlDisplay rcd, int w, int h, + in ComponentName listenerComp); + /** * Unregister an IRemoteControlDisplay. * No effect if the IRemoteControlDisplay hasn't been successfully registered. diff --git a/media/java/android/media/IRemoteControlClient.aidl b/media/java/android/media/IRemoteControlClient.aidl index 999b8ba..aa142d6 100644 --- a/media/java/android/media/IRemoteControlClient.aidl +++ b/media/java/android/media/IRemoteControlClient.aidl @@ -56,6 +56,7 @@ oneway interface IRemoteControlClient void unplugRemoteControlDisplay(IRemoteControlDisplay rcd); void setBitmapSizeForDisplay(IRemoteControlDisplay rcd, int w, int h); void setWantsSyncForDisplay(IRemoteControlDisplay rcd, boolean wantsSync); + void enableRemoteControlDisplay(IRemoteControlDisplay rcd, boolean enabled); void seekTo(int clientGeneration, long timeMs); void updateMetadata(int clientGeneration, int key, in Rating value); }
\ No newline at end of file diff --git a/media/java/android/media/IRemoteControlDisplay.aidl b/media/java/android/media/IRemoteControlDisplay.aidl index 583f436..1609030 100644 --- a/media/java/android/media/IRemoteControlDisplay.aidl +++ b/media/java/android/media/IRemoteControlDisplay.aidl @@ -41,6 +41,12 @@ oneway interface IRemoteControlDisplay boolean clearing); /** + * Sets whether the controls of this display are enabled + * @param if false, the display shouldn't any commands + */ + void setEnabled(boolean enabled); + + /** * Sets the playback information (state, position and speed) of a client. * @param generationId the current generation ID as known by this client * @param state the current playback state, one of the following values: diff --git a/media/java/android/media/MediaFocusControl.java b/media/java/android/media/MediaFocusControl.java index 2a7a731..d185321 100644 --- a/media/java/android/media/MediaFocusControl.java +++ b/media/java/android/media/MediaFocusControl.java @@ -17,6 +17,7 @@ package android.media; import android.app.Activity; +import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.KeyguardManager; import android.app.PendingIntent; @@ -30,6 +31,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -45,6 +48,7 @@ import android.speech.RecognizerIntent; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; +import android.util.Slog; import android.view.KeyEvent; import java.io.FileDescriptor; @@ -78,6 +82,7 @@ public class MediaFocusControl implements OnFinished { private final AppOpsManager mAppOps; private final KeyguardManager mKeyguardManager; private final AudioService mAudioService; + private final NotificationListenerObserver mNotifListenerObserver; protected MediaFocusControl(Looper looper, Context cntxt, VolumeController volumeCtrl, AudioService as) { @@ -111,6 +116,7 @@ public class MediaFocusControl implements OnFinished { mAppOps = (AppOpsManager)mContext.getSystemService(Context.APP_OPS_SERVICE); mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); + mNotifListenerObserver = new NotificationListenerObserver(); mHasRemotePlayback = false; mMainRemoteIsActive = false; @@ -125,6 +131,182 @@ public class MediaFocusControl implements OnFinished { } //========================================================================================== + // Management of RemoteControlDisplay registration permissions + //========================================================================================== + private final static Uri ENABLED_NOTIFICATION_LISTENERS_URI = + Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); + + private class NotificationListenerObserver extends ContentObserver { + + NotificationListenerObserver() { + super(mEventHandler); + mContentResolver.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_LISTENERS), false, this); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (!ENABLED_NOTIFICATION_LISTENERS_URI.equals(uri) || selfChange) { + return; + } + if (DEBUG_RC) { Log.d(TAG, "NotificationListenerObserver.onChange()"); } + postReevaluateRemoteControlDisplays(); + } + } + + private final static int RCD_REG_FAILURE = 0; + private final static int RCD_REG_SUCCESS_PERMISSION = 1; + private final static int RCD_REG_SUCCESS_ENABLED_NOTIF = 2; + + /** + * Checks a caller's authorization to register an IRemoteControlDisplay. + * Authorization is granted if one of the following is true: + * <ul> + * <li>the caller has android.Manifest.permission.MEDIA_CONTENT_CONTROL permission</li> + * <li>the caller's listener is one of the enabled notification listeners</li> + * </ul> + * @return RCD_REG_FAILURE if it's not safe to proceed with the IRemoteControlDisplay + * registration. + */ + private int checkRcdRegistrationAuthorization(ComponentName listenerComp) { + // MEDIA_CONTENT_CONTROL permission check + if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( + android.Manifest.permission.MEDIA_CONTENT_CONTROL)) { + if (DEBUG_RC) { Log.d(TAG, "ok to register Rcd: has MEDIA_CONTENT_CONTROL permission");} + return RCD_REG_SUCCESS_PERMISSION; + } + + // ENABLED_NOTIFICATION_LISTENERS settings check + if (listenerComp != null) { + // this call is coming from an app, can't use its identity to read secure settings + final long ident = Binder.clearCallingIdentity(); + try { + final int currentUser = ActivityManager.getCurrentUser(); + final String enabledNotifListeners = Settings.Secure.getStringForUser( + mContext.getContentResolver(), + Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, + currentUser); + if (enabledNotifListeners != null) { + final String[] components = enabledNotifListeners.split(":"); + for (int i=0; i<components.length; i++) { + final ComponentName component = + ComponentName.unflattenFromString(components[i]); + if (component != null) { + if (listenerComp.equals(component)) { + if (DEBUG_RC) { Log.d(TAG, "ok to register RCC: " + component + + " is authorized notification listener"); } + return RCD_REG_SUCCESS_ENABLED_NOTIF; + } + } + } + } + if (DEBUG_RC) { Log.d(TAG, "not ok to register RCD, " + listenerComp + + " is not in list of ENABLED_NOTIFICATION_LISTENERS"); } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + return RCD_REG_FAILURE; + } + + protected boolean registerRemoteController(IRemoteControlDisplay rcd, int w, int h, + ComponentName listenerComp) { + int reg = checkRcdRegistrationAuthorization(listenerComp); + if (reg != RCD_REG_FAILURE) { + registerRemoteControlDisplay_int(rcd, w, h, listenerComp); + return true; + } else { + Slog.w(TAG, "Access denied to process: " + Binder.getCallingPid() + + ", must have permission " + android.Manifest.permission.MEDIA_CONTENT_CONTROL + + " or be an enabled NotificationListenerService for registerRemoteController"); + return false; + } + } + + protected boolean registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) { + int reg = checkRcdRegistrationAuthorization(null); + if (reg != RCD_REG_FAILURE) { + registerRemoteControlDisplay_int(rcd, w, h, null); + return true; + } else { + Slog.w(TAG, "Access denied to process: " + Binder.getCallingPid() + + ", must have permission " + android.Manifest.permission.MEDIA_CONTENT_CONTROL + + " to register IRemoteControlDisplay"); + return false; + } + } + + private void postReevaluateRemoteControlDisplays() { + sendMsg(mEventHandler, MSG_REEVALUATE_RCD, SENDMSG_QUEUE, 0, 0, null, 0); + } + + private void onReevaluateRemoteControlDisplays() { + if (DEBUG_RC) { Log.d(TAG, "onReevaluateRemoteControlDisplays()"); } + // read which components are enabled notification listeners + final int currentUser = ActivityManager.getCurrentUser(); + final String enabledNotifListeners = Settings.Secure.getStringForUser( + mContext.getContentResolver(), + Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, + currentUser); + if (DEBUG_RC) { Log.d(TAG, " > enabled list: " + enabledNotifListeners); } + synchronized(mAudioFocusLock) { + synchronized(mRCStack) { + // check whether the "enable" status of each RCD with a notification listener + // has changed + final String[] enabledComponents; + if (enabledNotifListeners == null) { + enabledComponents = null; + } else { + enabledComponents = enabledNotifListeners.split(":"); + } + final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator(); + while (displayIterator.hasNext()) { + final DisplayInfoForServer di = + (DisplayInfoForServer) displayIterator.next(); + if (di.mClientNotifListComp != null) { + boolean wasEnabled = di.mEnabled; + di.mEnabled = isComponentInStringArray(di.mClientNotifListComp, + enabledComponents); + if (wasEnabled != di.mEnabled){ + try { + // tell the RCD whether it's enabled + di.mRcDisplay.setEnabled(di.mEnabled); + // tell the RCCs about the change for this RCD + enableRemoteControlDisplayForClient_syncRcStack( + di.mRcDisplay, di.mEnabled); + } catch (RemoteException e) { + Log.e(TAG, "Error en/disabling RCD: ", e); + } + } + } + } + } + } + } + + /** + * @param comp a non-null ComponentName + * @param enabledArray may be null + * @return + */ + private boolean isComponentInStringArray(ComponentName comp, String[] enabledArray) { + if (enabledArray == null || enabledArray.length == 0) { + if (DEBUG_RC) { Log.d(TAG, " > " + comp + " is NOT enabled"); } + return false; + } + final String compString = comp.flattenToString(); + for (int i=0; i<enabledArray.length; i++) { + if (compString.equals(enabledArray[i])) { + if (DEBUG_RC) { Log.d(TAG, " > " + compString + " is enabled"); } + return true; + } + } + if (DEBUG_RC) { Log.d(TAG, " > " + compString + " is NOT enabled"); } + return false; + } + + //========================================================================================== // Internal event handling //========================================================================================== @@ -140,6 +322,7 @@ public class MediaFocusControl implements OnFinished { private static final int MSG_RCC_SEEK_REQUEST = 8; private static final int MSG_RCC_UPDATE_METADATA = 9; private static final int MSG_RCDISPLAY_INIT_INFO = 10; + private static final int MSG_REEVALUATE_RCD = 11; // sendMsg() flags /** If the msg is already queued, replace it with this one. */ @@ -221,6 +404,10 @@ public class MediaFocusControl implements OnFinished { onRcDisplayInitInfo((IRemoteControlDisplay)msg.obj /*newRcd*/, msg.arg1/*w*/, msg.arg2/*h*/); break; + + case MSG_REEVALUATE_RCD: + onReevaluateRemoteControlDisplays(); + break; } } } @@ -1193,8 +1380,9 @@ public class MediaFocusControl implements OnFinished { final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next(); pw.println(" IRCD: " + di.mRcDisplay + " -- w:" + di.mArtworkExpectedWidth + - " -- h:" + di.mArtworkExpectedHeight+ - " -- wantsPosSync:" + di.mWantsPositionSync); + " -- h:" + di.mArtworkExpectedHeight + + " -- wantsPosSync:" + di.mWantsPositionSync + + " -- " + (di.mEnabled ? "enabled" : "disabled")); } } } @@ -1834,11 +2022,13 @@ public class MediaFocusControl implements OnFinished { */ private class DisplayInfoForServer implements IBinder.DeathRecipient { /** may never be null */ - private IRemoteControlDisplay mRcDisplay; - private IBinder mRcDisplayBinder; + private final IRemoteControlDisplay mRcDisplay; + private final IBinder mRcDisplayBinder; private int mArtworkExpectedWidth = -1; private int mArtworkExpectedHeight = -1; private boolean mWantsPositionSync = false; + private ComponentName mClientNotifListComp; + private boolean mEnabled = true; public DisplayInfoForServer(IRemoteControlDisplay rcd, int w, int h) { if (DEBUG_RC) Log.i(TAG, "new DisplayInfoForServer for " + rcd + " w=" + w + " h=" + h); @@ -1911,6 +2101,23 @@ public class MediaFocusControl implements OnFinished { } } + private void enableRemoteControlDisplayForClient_syncRcStack(IRemoteControlDisplay rcd, + boolean enabled) { + // let all the remote control clients know whether the given display is enabled + // (so the remote control stack traversal order doesn't matter). + final Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator(); + while(stackIterator.hasNext()) { + RemoteControlStackEntry rcse = stackIterator.next(); + if(rcse.mRcClient != null) { + try { + rcse.mRcClient.enableRemoteControlDisplay(rcd, enabled); + } catch (RemoteException e) { + Log.e(TAG, "Error connecting RCD to client: ", e); + } + } + } + } + /** * Is the remote control display interface already registered * @param rcd @@ -1937,8 +2144,11 @@ public class MediaFocusControl implements OnFinished { * display doesn't need to receive artwork. * @param h the maximum height of the expected bitmap. Negative or zero values indicate this * display doesn't need to receive artwork. + * @param listenerComp the component for the listener interface, may be null if it's not needed + * to verify it belongs to one of the enabled notification listeners */ - protected void registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) { + private void registerRemoteControlDisplay_int(IRemoteControlDisplay rcd, int w, int h, + ComponentName listenerComp) { if (DEBUG_RC) Log.d(TAG, ">>> registerRemoteControlDisplay("+rcd+")"); synchronized(mAudioFocusLock) { synchronized(mRCStack) { @@ -1946,6 +2156,8 @@ public class MediaFocusControl implements OnFinished { return; } DisplayInfoForServer di = new DisplayInfoForServer(rcd, w, h); + di.mEnabled = true; + di.mClientNotifListComp = listenerComp; if (!di.init()) { if (DEBUG_RC) Log.e(TAG, " error registering RCD"); return; diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java index 497b8b3..0c00aba 100644 --- a/media/java/android/media/RemoteControlClient.java +++ b/media/java/android/media/RemoteControlClient.java @@ -1049,6 +1049,7 @@ public class RemoteControlClient private int mArtworkExpectedWidth; private int mArtworkExpectedHeight; private boolean mWantsPositionSync = false; + private boolean mEnabled = true; DisplayInfoForClient(IRemoteControlDisplay rcd, int w, int h) { mRcDisplay = rcd; @@ -1165,6 +1166,14 @@ public class RemoteControlClient } } + public void enableRemoteControlDisplay(IRemoteControlDisplay rcd, boolean enabled) { + // only post messages, we can't block here + if ((mEventHandler != null) && (rcd != null)) { + mEventHandler.sendMessage(mEventHandler.obtainMessage( + MSG_DISPLAY_ENABLE, enabled ? 1 : 0, 0/*arg2 ignored*/, rcd)); + } + } + public void seekTo(int generationId, long timeMs) { // only post messages, we can't block here if (mEventHandler != null) { @@ -1227,6 +1236,7 @@ public class RemoteControlClient private final static int MSG_DISPLAY_WANTS_POS_SYNC = 12; private final static int MSG_UPDATE_METADATA = 13; private final static int MSG_REQUEST_METADATA_ARTWORK = 14; + private final static int MSG_DISPLAY_ENABLE = 15; private class EventHandler extends Handler { public EventHandler(RemoteControlClient rcc, Looper looper) { @@ -1290,6 +1300,9 @@ public class RemoteControlClient case MSG_UPDATE_METADATA: onUpdateMetadata(msg.arg1, msg.arg2, msg.obj); break; + case MSG_DISPLAY_ENABLE: + onDisplayEnable((IRemoteControlDisplay)msg.obj, msg.arg1 == 1); + break; default: Log.e(TAG, "Unknown event " + msg.what + " in RemoteControlClient handler"); } @@ -1315,13 +1328,15 @@ public class RemoteControlClient final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); while (displayIterator.hasNext()) { final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next(); - try { - di.mRcDisplay.setPlaybackState(mInternalClientGenId, - mPlaybackState, mPlaybackStateChangeTimeMs, mPlaybackPositionMs, - mPlaybackSpeed); - } catch (RemoteException e) { - Log.e(TAG, "Error in setPlaybackState(), dead display " + di.mRcDisplay, e); - displayIterator.remove(); + if (di.mEnabled) { + try { + di.mRcDisplay.setPlaybackState(mInternalClientGenId, + mPlaybackState, mPlaybackStateChangeTimeMs, mPlaybackPositionMs, + mPlaybackSpeed); + } catch (RemoteException e) { + Log.e(TAG, "Error in setPlaybackState(), dead display " + di.mRcDisplay, e); + displayIterator.remove(); + } } } } @@ -1341,11 +1356,13 @@ public class RemoteControlClient final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); while (displayIterator.hasNext()) { final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next(); - try { - di.mRcDisplay.setMetadata(mInternalClientGenId, mMetadata); - } catch (RemoteException e) { - Log.e(TAG, "Error in setMetadata(), dead display " + di.mRcDisplay, e); - displayIterator.remove(); + if (di.mEnabled) { + try { + di.mRcDisplay.setMetadata(mInternalClientGenId, mMetadata); + } catch (RemoteException e) { + Log.e(TAG, "Error in setMetadata(), dead display " + di.mRcDisplay, e); + displayIterator.remove(); + } } } } @@ -1367,13 +1384,15 @@ public class RemoteControlClient final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); while (displayIterator.hasNext()) { final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next(); - try { - di.mRcDisplay.setTransportControlInfo(mInternalClientGenId, - mTransportControlFlags, mPlaybackPositionCapabilities); - } catch (RemoteException e) { - Log.e(TAG, "Error in setTransportControlFlags(), dead display " + di.mRcDisplay, - e); - displayIterator.remove(); + if (di.mEnabled) { + try { + di.mRcDisplay.setTransportControlInfo(mInternalClientGenId, + mTransportControlFlags, mPlaybackPositionCapabilities); + } catch (RemoteException e) { + Log.e(TAG, "Error in setTransportControlFlags(), dead display " + di.mRcDisplay, + e); + displayIterator.remove(); + } } } } @@ -1438,12 +1457,14 @@ public class RemoteControlClient while (displayIterator.hasNext()) { final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next(); try { - if ((di.mArtworkExpectedWidth > 0) && (di.mArtworkExpectedHeight > 0)) { - Bitmap artwork = scaleBitmapIfTooBig(mOriginalArtwork, - di.mArtworkExpectedWidth, di.mArtworkExpectedHeight); - di.mRcDisplay.setAllMetadata(mInternalClientGenId, mMetadata, artwork); - } else { - di.mRcDisplay.setMetadata(mInternalClientGenId, mMetadata); + if (di.mEnabled) { + if ((di.mArtworkExpectedWidth > 0) && (di.mArtworkExpectedHeight > 0)) { + Bitmap artwork = scaleBitmapIfTooBig(mOriginalArtwork, + di.mArtworkExpectedWidth, di.mArtworkExpectedHeight); + di.mRcDisplay.setAllMetadata(mInternalClientGenId, mMetadata, artwork); + } else { + di.mRcDisplay.setMetadata(mInternalClientGenId, mMetadata); + } } } catch (RemoteException e) { Log.e(TAG, "Error when setting metadata, dead display " + di.mRcDisplay, e); @@ -1578,8 +1599,10 @@ public class RemoteControlClient ((di.mArtworkExpectedWidth != w) || (di.mArtworkExpectedHeight != h))) { di.mArtworkExpectedWidth = w; di.mArtworkExpectedHeight = h; - if (!sendArtworkToDisplay(di)) { - displayIterator.remove(); + if (di.mEnabled) { + if (!sendArtworkToDisplay(di)) { + displayIterator.remove(); + } } break; } @@ -1597,11 +1620,13 @@ public class RemoteControlClient // that gets upated, and whether the list has one entry that wants position sync while (displayIterator.hasNext()) { final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next(); - if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) { - di.mWantsPositionSync = wantsSync; - } - if (di.mWantsPositionSync) { - newNeedsPositionSync = true; + if (di.mEnabled) { + if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) { + di.mWantsPositionSync = wantsSync; + } + if (di.mWantsPositionSync) { + newNeedsPositionSync = true; + } } } mNeedsPositionSync = newNeedsPositionSync; @@ -1612,6 +1637,19 @@ public class RemoteControlClient } } + /** pre-condition rcd != null */ + private void onDisplayEnable(IRemoteControlDisplay rcd, boolean enable) { + synchronized(mCacheLock) { + final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); + while (displayIterator.hasNext()) { + final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next(); + if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) { + di.mEnabled = enable; + } + } + } + } + private void onSeekTo(int generationId, long timeMs) { synchronized (mCacheLock) { if ((mCurrentClientGenId == generationId) && (mPositionUpdateListener != null)) { diff --git a/media/java/android/media/RemoteController.java b/media/java/android/media/RemoteController.java index 22f6343..c6d6296 100644 --- a/media/java/android/media/RemoteController.java +++ b/media/java/android/media/RemoteController.java @@ -42,14 +42,14 @@ import android.view.KeyEvent; * <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 #setArtworkConfiguration(int, int)} to receive images as well. + * media event updates to the {@link OnClientUpdateListener} listener set in the class constructor. + * Implement the methods of the interface to receive the information published by the active + * {@link RemoteControlClient} instances. + * <br>By default an {@link OnClientUpdateListener} implementation will not receive bitmaps for + * album art. Use {@link #setArtworkConfiguration(int, int)} to receive images as well. * <p> - * Registration requires the {@link Manifest.permission#MEDIA_CONTENT_CONTROL} permission. + * Registration requires the {@link OnClientUpdateListener} listener to be one of the enabled + * notification listeners (see {@link android.service.notification.NotificationListenerService}). */ public final class RemoteController { @@ -77,30 +77,39 @@ public final class RemoteController private PendingIntent mClientPendingIntentCurrent; private OnClientUpdateListener mOnClientUpdateListener; private PlaybackInfo mLastPlaybackInfo; - private int mLastTransportControlFlags = TRANSPORT_UNKNOWN; private int mArtworkWidth = -1; private int mArtworkHeight = -1; + private boolean mEnabled = true; /** * Class constructor. - * @param context non-null the {@link Context}, must be non-null - * @throws java.lang.IllegalArgumentException + * @param context the {@link Context}, must be non-null. + * @param updateListener the listener to be called whenever new client information is available, + * must be non-null. + * @throws IllegalArgumentException */ - public RemoteController(Context context) throws IllegalArgumentException { - this(context, null); + public RemoteController(Context context, OnClientUpdateListener updateListener) + throws IllegalArgumentException { + this(context, updateListener, null); } /** * Class constructor. + * @param context the {@link Context}, must be non-null. + * @param updateListener the listener to be called whenever new client information is available, + * must be non-null. * @param looper the {@link Looper} on which to run the event loop, * or null to use the current thread's looper. - * @param context the {@link Context}, must be non-null * @throws java.lang.IllegalArgumentException */ - public RemoteController(Context context, Looper looper) throws IllegalArgumentException { + public RemoteController(Context context, OnClientUpdateListener updateListener, Looper looper) + throws IllegalArgumentException { if (context == null) { throw new IllegalArgumentException("Invalid null Context"); } + if (updateListener == null) { + throw new IllegalArgumentException("Invalid null OnClientUpdateListener"); + } if (looper != null) { mEventHandler = new EventHandler(this, looper); } else { @@ -111,6 +120,7 @@ public final class RemoteController throw new IllegalArgumentException("Calling thread not associated with a looper"); } } + mOnClientUpdateListener = updateListener; mContext = context; mRcd = new RcDisplay(); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); @@ -125,31 +135,31 @@ public final class RemoteController /** - * An abstract class definition for the callbacks to be invoked whenever media events, metadata + * Interface definition for the callbacks to be invoked whenever media events, metadata * and playback status are available. */ - public static abstract class OnClientUpdateListener { + public interface OnClientUpdateListener { /** - * The method called whenever all information previously received through the other + * Called whenever all information, previously received through the other * methods of the listener, is no longer valid and is about to be refreshed. * This is typically called whenever a new {@link RemoteControlClient} has been selected * by the system to have its media information published. * @param clearing true if there is no selected RemoteControlClient and no information * is available. */ - public void onClientChange(boolean clearing) { } + public void onClientChange(boolean clearing); /** - * The method called whenever the playback state has changed. + * Called whenever the playback state has changed. * It is called when no information is known about the playback progress in the media and * the playback speed. * @param state one of the playback states authorized * in {@link RemoteControlClient#setPlaybackState(int)}. */ - public void onClientPlaybackStateUpdate(int state) { } + public void onClientPlaybackStateUpdate(int state); /** - * The method called whenever the playback state has changed, and playback position and - * speed are known. + * Called whenever the playback state has changed, and playback position + * and speed are known. * @param state one of the playback states authorized * in {@link RemoteControlClient#setPlaybackState(int)}. * @param stateChangeTimeMs the system time at which the state change was reported, @@ -161,15 +171,15 @@ public final class RemoteController * playing (e.g. when state is {@link RemoteControlClient#PLAYSTATE_ERROR}). */ public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, - long currentPosMs, float speed) { } + long currentPosMs, float speed); /** - * The method called whenever the transport control flags have changed. + * Called whenever the transport control flags have changed. * @param transportControlFlags one of the flags authorized * in {@link RemoteControlClient#setTransportControlFlags(int)}. */ - public void onClientTransportControlUpdate(int transportControlFlags) { } + public void onClientTransportControlUpdate(int transportControlFlags); /** - * The method called whenever new metadata is available. + * Called whenever new metadata is available. * See the {@link MediaMetadataEditor#putLong(int, long)}, * {@link MediaMetadataEditor#putString(int, String)}, * {@link MediaMetadataEditor#putBitmap(int, Bitmap)}, and @@ -177,39 +187,9 @@ public final class RemoteController * can be queried. * @param metadataEditor the container of the new metadata. */ - public void onClientMetadataUpdate(MetadataEditor metadataEditor) { } + public void onClientMetadataUpdate(MetadataEditor metadataEditor); }; - /** - * Sets the listener to be called whenever new client information is available. - * This method can only be called on a registered RemoteController. - * @param l the update listener to be called. - */ - 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 @@ -256,6 +236,7 @@ public final class RemoteController return -1; } + /** * Send a simulated key event for a media button to be received by the current client. * To simulate a key press, you must first send a KeyEvent built with @@ -280,17 +261,22 @@ public final class RemoteController * {@link KeyEvent#KEYCODE_MEDIA_CLOSE}, * {@link KeyEvent#KEYCODE_MEDIA_EJECT}, * or {@link KeyEvent#KEYCODE_MEDIA_AUDIO_TRACK}. + * @return true if the event was successfully sent, false otherwise. + * @throws IllegalArgumentException */ - public int sendMediaKeyEvent(KeyEvent keyEvent) { + public boolean sendMediaKeyEvent(KeyEvent keyEvent) throws IllegalArgumentException { if (!MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode())) { - Log.e(TAG, "Cannot use sendMediaKeyEvent() for a non-media key event"); - return ERROR_BAD_VALUE; + throw new IllegalArgumentException("not a media key event"); } final PendingIntent pi; synchronized(mInfoLock) { if (!mIsRegistered) { Log.e(TAG, "Cannot use sendMediaKeyEvent() from an unregistered RemoteController"); - return ERROR; + return false; + } + if (!mEnabled) { + Log.e(TAG, "Cannot use sendMediaKeyEvent() from a disabled RemoteController"); + return false; } pi = mClientPendingIntentCurrent; } @@ -301,47 +287,37 @@ public final class RemoteController pi.send(mContext, 0, intent); } catch (CanceledException e) { Log.e(TAG, "Error sending intent for media button down: ", e); - return ERROR; + return false; } } else { Log.i(TAG, "No-op when sending key click, no receiver right now"); - return ERROR; + return false; } - return SUCCESS; + return true; } - // Error codes - /** - * Successful operation. - */ - public static final int SUCCESS = 0; - /** - * Unspecified error. - */ - public static final int ERROR = -1; - /** - * Operation failed due to bad parameter value. - */ - public static final int ERROR_BAD_VALUE = -2; - - /** * Sets the new playback position. * This method can only be called on a registered RemoteController. * @param timeMs a 0 or positive value for the new playback position, expressed in ms. - * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE} + * @return true if the command to set the playback position was successfully sent. + * @throws IllegalArgumentException */ - public int seekTo(long timeMs) { + public boolean seekTo(long timeMs) throws IllegalArgumentException { + if (!mEnabled) { + Log.e(TAG, "Cannot use seekTo() from a disabled RemoteController"); + return false; + } if (timeMs < 0) { - return ERROR_BAD_VALUE; + throw new IllegalArgumentException("illegal negative time value"); } final int genId; synchronized (mGenLock) { genId = mClientGenerationIdCurrent; } mAudioManager.setRemoteControlClientPlaybackPosition(genId, timeMs); - return SUCCESS; + return true; } @@ -350,9 +326,11 @@ public final class RemoteController * @param wantBitmap * @param width * @param height - * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE} + * @return true if successful + * @throws IllegalArgumentException */ - public int setArtworkConfiguration(boolean wantBitmap, int width, int height) { + public boolean setArtworkConfiguration(boolean wantBitmap, int width, int height) + throws IllegalArgumentException { synchronized (mInfoLock) { if (wantBitmap) { if ((width > 0) && (height > 0)) { @@ -361,8 +339,7 @@ public final class RemoteController mArtworkWidth = width; mArtworkHeight = height; } else { - Log.e(TAG, "Invalid dimensions"); - return ERROR_BAD_VALUE; + throw new IllegalArgumentException("Invalid dimensions"); } } else { mArtworkWidth = -1; @@ -375,7 +352,7 @@ public final class RemoteController // RemoteController.getArtworkSize() when AudioManager.registerRemoteController() // is called. } - return SUCCESS; + return true; } /** @@ -383,17 +360,18 @@ public final class RemoteController * No bitmaps will be received unless this has been specified. * @param width the maximum width in pixels * @param height the maximum height in pixels - * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE} + * @return true if the artwork dimension was successfully set. + * @throws IllegalArgumentException */ - public int setArtworkConfiguration(int width, int height) { + public boolean setArtworkConfiguration(int width, int height) throws IllegalArgumentException { return setArtworkConfiguration(true, width, height); } /** * Prevents this RemoteController from receiving artwork images. - * @return {@link #SUCCESS}, {@link #ERROR} + * @return true if receiving artwork images was successfully disabled. */ - public int clearArtworkConfiguration() { + public boolean clearArtworkConfiguration() { return setArtworkConfiguration(false, -1, -1); } @@ -420,20 +398,20 @@ public final class RemoteController * 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} + * @return true if the synchronization mode was successfully set. + * @throws IllegalArgumentException */ - public int setSynchronizationMode(int sync) { + public boolean setSynchronizationMode(int sync) throws IllegalArgumentException { if ((sync != POSITION_SYNCHRONIZATION_NONE) || (sync != POSITION_SYNCHRONIZATION_CHECK)) { - Log.e(TAG, "Unknown synchronization mode"); - return ERROR_BAD_VALUE; + throw new IllegalArgumentException("Unknown synchronization mode " + sync); } if (!mIsRegistered) { Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController"); - return ERROR; + return false; } mAudioManager.remoteControlDisplayWantsPlaybackPositionSync(mRcd, POSITION_SYNCHRONIZATION_CHECK == sync); - return SUCCESS; + return true; } @@ -548,6 +526,11 @@ public final class RemoteController } } + public void setEnabled(boolean enabled) { + sendMsg(mEventHandler, MSG_DISPLAY_ENABLE, SENDMSG_REPLACE, + enabled ? 1 : 0 /*arg1*/, 0, null /*obj*/, 0 /*delay*/); + } + public void setPlaybackState(int genId, int state, long stateChangeTimeMs, long currentPosMs, float speed) { if (DEBUG) { @@ -642,6 +625,7 @@ public final class RemoteController 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_CHANGE = 4; + private final static int MSG_DISPLAY_ENABLE = 5; private class EventHandler extends Handler { @@ -667,6 +651,9 @@ public final class RemoteController case MSG_CLIENT_CHANGE: onClientChange(msg.arg1, msg.arg2 == 1); break; + case MSG_DISPLAY_ENABLE: + onDisplayEnable(msg.arg1 == 1); + break; default: Log.e(TAG, "unknown event " + msg.what); } @@ -735,7 +722,6 @@ public final class RemoteController final OnClientUpdateListener l; synchronized(mInfoLock) { l = mOnClientUpdateListener; - mLastTransportControlFlags = transportControlFlags; } if (l != null) { l.onClientTransportControlUpdate(transportControlFlags); @@ -797,6 +783,11 @@ public final class RemoteController } } + private void onDisplayEnable(boolean enabled) { + synchronized(mInfoLock) { + mEnabled = enabled; + } + } //================================================== private static class PlaybackInfo { @@ -818,7 +809,7 @@ public final class RemoteController * Used by AudioManager to mark this instance as registered. * @param registered */ - protected void setIsRegistered(boolean registered) { + void setIsRegistered(boolean registered) { synchronized (mInfoLock) { mIsRegistered = registered; } @@ -829,7 +820,7 @@ public final class RemoteController * Used by AudioManager to access binder to be registered/unregistered inside MediaFocusControl * @return */ - protected RcDisplay getRcDisplay() { + RcDisplay getRcDisplay() { return mRcd; } @@ -838,11 +829,19 @@ public final class RemoteController * Used by AudioManager to read the current artwork dimension * @return array containing width (index 0) and height (index 1) of currently set artwork size */ - protected int[] getArtworkSize() { + int[] getArtworkSize() { synchronized (mInfoLock) { int[] size = { mArtworkWidth, mArtworkHeight }; return size; } } + /** + * @hide + * Used by AudioManager to access user listener receiving the client update notifications + * @return + */ + OnClientUpdateListener getUpdateListener() { + return mOnClientUpdateListener; + } } |