import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
-import android.app.PendingIntent.OnFinished;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
*
* @hide
*/
-public class AudioService extends IAudioService.Stub implements OnFinished {
+public class AudioService extends IAudioService.Stub {
private static final String TAG = "AudioService";
private static final int MSG_BTA2DP_DOCK_TIMEOUT = 7;
private static final int MSG_LOAD_SOUND_EFFECTS = 8;
private static final int MSG_SET_FORCE_USE = 9;
- private static final int MSG_PERSIST_MEDIABUTTONRECEIVER = 10;
- private static final int MSG_BT_HEADSET_CNCT_FAILED = 11;
- private static final int MSG_RCDISPLAY_CLEAR = 12;
- private static final int MSG_RCDISPLAY_UPDATE = 13;
- private static final int MSG_SET_ALL_VOLUMES = 14;
- private static final int MSG_PERSIST_MASTER_VOLUME_MUTE = 15;
- private static final int MSG_REPORT_NEW_ROUTES = 16;
- private static final int MSG_REEVALUATE_REMOTE = 17;
- private static final int MSG_RCC_NEW_PLAYBACK_INFO = 18;
- private static final int MSG_RCC_NEW_VOLUME_OBS = 19;
- private static final int MSG_SET_FORCE_BT_A2DP_USE = 20;
+ private static final int MSG_BT_HEADSET_CNCT_FAILED = 10;
+ private static final int MSG_SET_ALL_VOLUMES = 11;
+ private static final int MSG_PERSIST_MASTER_VOLUME_MUTE = 12;
+ private static final int MSG_REPORT_NEW_ROUTES = 13;
+ private static final int MSG_SET_FORCE_BT_A2DP_USE = 14;
+ private static final int MSG_SET_RSX_CONNECTION_STATE = 15; // change remote submix connection
+ private static final int MSG_CHECK_MUSIC_ACTIVE = 16;
+ private static final int MSG_BROADCAST_AUDIO_BECOMING_NOISY = 17;
+ private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME = 18;
+ private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED = 19;
+ private static final int MSG_PERSIST_SAFE_VOLUME_STATE = 20;
+ private static final int MSG_BROADCAST_BT_CONNECTION_STATE = 21;
+ private static final int MSG_UNLOAD_SOUND_EFFECTS = 22;
// start of messages handled under wakelock
// these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
// and not with sendMsg(..., ..., SENDMSG_QUEUE, ...)
- private static final int MSG_SET_WIRED_DEVICE_CONNECTION_STATE = 21;
- private static final int MSG_SET_A2DP_CONNECTION_STATE = 22;
+ private static final int MSG_SET_WIRED_DEVICE_CONNECTION_STATE = 100;
+ private static final int MSG_SET_A2DP_CONNECTION_STATE = 101;
// end of messages handled under wakelock
- private static final int MSG_SET_RSX_CONNECTION_STATE = 23; // change remote submix connection
- private static final int MSG_CHECK_MUSIC_ACTIVE = 24;
- private static final int MSG_BROADCAST_AUDIO_BECOMING_NOISY = 25;
- private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME = 26;
- private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED = 27;
- private static final int MSG_PERSIST_SAFE_VOLUME_STATE = 28;
- private static final int MSG_PROMOTE_RCC = 29;
- private static final int MSG_BROADCAST_BT_CONNECTION_STATE = 30;
- private static final int MSG_UNLOAD_SOUND_EFFECTS = 31;
- private static final int MSG_RCC_NEW_PLAYBACK_STATE = 32;
- private static final int MSG_RCC_SEEK_REQUEST = 33;
private static final int BTA2DP_DOCK_TIMEOUT_MILLIS = 8000;
// Timeout for connection to bluetooth headset service
private VolumeStreamState[] mStreamStates;
private SettingsObserver mSettingsObserver;
- private int mMode;
+ private int mMode = AudioSystem.MODE_NORMAL;
// protects mRingerMode
private final Object mSettingsLock = new Object();
private final int[][] SOUND_EFFECT_FILES_MAP = new int[AudioManager.NUM_SOUND_EFFECTS][2];
/** @hide Maximum volume index values for audio streams */
- private final int[] MAX_STREAM_VOLUME = new int[] {
+ private static final int[] MAX_STREAM_VOLUME = new int[] {
5, // STREAM_VOICE_CALL
7, // STREAM_SYSTEM
7, // STREAM_RING
// Broadcast receiver for device connections intent broadcasts
private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
- // Used to alter media button redirection when the phone is ringing.
- private boolean mIsRinging = false;
-
// Devices currently connected
private final HashMap <Integer, String> mConnectedDevices = new HashMap <Integer, String>();
// and used later when/if disableSafeMediaVolume() is called.
private StreamVolumeCommand mPendingVolumeCommand;
+ private PowerManager.WakeLock mAudioEventWakeLock;
+
+ private final MediaFocusControl mMediaFocusControl;
+
///////////////////////////////////////////////////////////////////////////
// Construction
///////////////////////////////////////////////////////////////////////////
com.android.internal.R.bool.config_voice_capable);
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
- mMediaEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleMediaEvent");
+ mAudioEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleAudioEvent");
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
mHasVibrator = vibrator == null ? false : vibrator.hasVibrator();
com.android.internal.R.integer.config_soundEffectVolumeDb);
mVolumePanel = new VolumePanel(context, this);
- mMode = AudioSystem.MODE_NORMAL;
mForcedUseForComm = AudioSystem.FORCE_NONE;
createAudioSystemThread();
+ mMediaFocusControl = new MediaFocusControl(mAudioHandler.getLooper(),
+ mContext, /*VolumeController*/ mVolumePanel, this);
+
boolean cameraSoundForced = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_camera_sound_forced);
mCameraSoundForced = new Boolean(cameraSoundForced);
context.registerReceiver(mReceiver, intentFilter);
- // Register for package removal intent broadcasts for media button receiver persistence
- IntentFilter pkgFilter = new IntentFilter();
- pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
- pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
- pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
- pkgFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
- pkgFilter.addDataScheme("package");
- context.registerReceiver(mReceiver, pkgFilter);
-
- // Register for phone state monitoring
- TelephonyManager tmgr = (TelephonyManager)
- context.getSystemService(Context.TELEPHONY_SERVICE);
- tmgr.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
-
mUseMasterVolume = context.getResources().getBoolean(
com.android.internal.R.bool.config_useMasterVolume);
restoreMasterVolume();
mMasterVolumeRamp = context.getResources().getIntArray(
com.android.internal.R.array.config_masterVolumeRamp);
- mMainRemote = new RemotePlaybackState(-1, MAX_STREAM_VOLUME[AudioManager.STREAM_MUSIC],
- MAX_STREAM_VOLUME[AudioManager.STREAM_MUSIC]);
- mHasRemotePlayback = false;
- mMainRemoteIsActive = false;
- postReevaluateRemote();
}
private void createAudioSystemThread() {
broadcastVibrateSetting(AudioManager.VIBRATE_TYPE_NOTIFICATION);
// Restore the default media button receiver from the system settings
- restoreMediaButtonReceiver();
+ mMediaFocusControl.restoreMediaButtonReceiver();
}
private int rescaleIndex(int index, int srcStream, int dstStream) {
public void adjustLocalOrRemoteStreamVolume(int streamType, int direction,
String callingPackage) {
if (DEBUG_VOL) Log.d(TAG, "adjustLocalOrRemoteStreamVolume(dir="+direction+")");
- if (checkUpdateRemoteStateIfActive(AudioSystem.STREAM_MUSIC)) {
- adjustRemoteVolume(AudioSystem.STREAM_MUSIC, direction, 0);
+ if (mMediaFocusControl.checkUpdateRemoteStateIfActive(AudioSystem.STREAM_MUSIC)) {
+ mMediaFocusControl.adjustRemoteVolume(AudioSystem.STREAM_MUSIC, direction, 0);
} else if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) {
adjustStreamVolume(AudioSystem.STREAM_MUSIC, direction, 0, callingPackage);
}
// don't play sounds for remote
flags &= ~(AudioManager.FLAG_PLAY_SOUND|AudioManager.FLAG_FIXED_VOLUME);
//if (DEBUG_VOL) Log.i(TAG, "Need to adjust remote volume: calling adjustRemoteVolume()");
- adjustRemoteVolume(AudioSystem.STREAM_MUSIC, direction, flags);
+ mMediaFocusControl.adjustRemoteVolume(AudioSystem.STREAM_MUSIC, direction, flags);
} else {
adjustStreamVolume(streamType, direction, flags, callingPackage);
}
return AudioSystem.getMasterMute();
}
+ protected static int getMaxStreamVolume(int streamType) {
+ return MAX_STREAM_VOLUME[streamType];
+ }
+
/** @see AudioManager#getStreamVolume(int) */
public int getStreamVolume(int streamType) {
ensureValidStreamType(streamType);
} else if (suggestedStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
// Having the suggested stream be USE_DEFAULT_STREAM_TYPE is how remote control
// volume can have priority over STREAM_MUSIC
- if (checkUpdateRemoteStateIfActive(AudioSystem.STREAM_MUSIC)) {
+ if (mMediaFocusControl.checkUpdateRemoteStateIfActive(AudioSystem.STREAM_MUSIC)) {
if (DEBUG_VOL)
Log.v(TAG, "getActiveStreamType: Forcing STREAM_REMOTE_MUSIC");
return STREAM_REMOTE_MUSIC;
if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_NOTIFICATION");
return AudioSystem.STREAM_NOTIFICATION;
} else if (suggestedStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
- if (checkUpdateRemoteStateIfActive(AudioSystem.STREAM_MUSIC)) {
+ if (mMediaFocusControl.checkUpdateRemoteStateIfActive(AudioSystem.STREAM_MUSIC)) {
// Having the suggested stream be USE_DEFAULT_STREAM_TYPE is how remote control
// volume can have priority over STREAM_MUSIC
if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_REMOTE_MUSIC");
*/
private void queueMsgUnderWakeLock(Handler handler, int msg,
int arg1, int arg2, Object obj, int delay) {
- mMediaEventWakeLock.acquire();
+ mAudioEventWakeLock.acquire();
sendMsg(handler, msg, SENDMSG_QUEUE, arg1, arg2, obj, delay);
}
}
}
- private void onHandlePersistMediaButtonReceiver(ComponentName receiver) {
- Settings.System.putStringForUser(mContentResolver,
- Settings.System.MEDIA_BUTTON_RECEIVER,
- receiver == null ? "" : receiver.flattenToString(),
- UserHandle.USER_CURRENT);
- }
-
private void cleanupPlayer(MediaPlayer mp) {
if (mp != null) {
try {
setForceUse(msg.arg1, msg.arg2);
break;
- case MSG_PERSIST_MEDIABUTTONRECEIVER:
- onHandlePersistMediaButtonReceiver( (ComponentName) msg.obj );
- break;
-
- case MSG_RCDISPLAY_CLEAR:
- onRcDisplayClear();
- break;
-
- case MSG_RCDISPLAY_UPDATE:
- // msg.obj is guaranteed to be non null
- onRcDisplayUpdate( (RemoteControlStackEntry) msg.obj, msg.arg1);
- break;
-
case MSG_BT_HEADSET_CNCT_FAILED:
resetBluetoothSco();
break;
case MSG_SET_WIRED_DEVICE_CONNECTION_STATE:
onSetWiredDeviceConnectionState(msg.arg1, msg.arg2, (String)msg.obj);
- mMediaEventWakeLock.release();
+ mAudioEventWakeLock.release();
break;
case MSG_SET_A2DP_CONNECTION_STATE:
onSetA2dpConnectionState((BluetoothDevice)msg.obj, msg.arg1);
- mMediaEventWakeLock.release();
+ mAudioEventWakeLock.release();
break;
case MSG_REPORT_NEW_ROUTES: {
break;
}
- case MSG_REEVALUATE_REMOTE:
- onReevaluateRemote();
- break;
-
- case MSG_RCC_NEW_PLAYBACK_INFO:
- onNewPlaybackInfoForRcc(msg.arg1 /* rccId */, msg.arg2 /* key */,
- ((Integer)msg.obj).intValue() /* value */);
- break;
- case MSG_RCC_NEW_VOLUME_OBS:
- onRegisterVolumeObserverForRcc(msg.arg1 /* rccId */,
- (IRemoteVolumeObserver)msg.obj /* rvo */);
- break;
- case MSG_RCC_NEW_PLAYBACK_STATE:
- onNewPlaybackStateForRcc(msg.arg1 /* rccId */, msg.arg2 /* state */,
- (RccPlaybackState)msg.obj /* newState */);
- break;
- case MSG_RCC_SEEK_REQUEST:
- onSetRemoteControlClientPlaybackPosition(msg.arg1 /* generationId */,
- ((Long)msg.obj).longValue() /* timeMs */);
-
case MSG_SET_RSX_CONNECTION_STATE:
onSetRsxConnectionState(msg.arg1/*available*/, msg.arg2/*address*/);
break;
onPersistSafeVolumeState(msg.arg1);
break;
- case MSG_PROMOTE_RCC:
- onPromoteRcc(msg.arg1);
- break;
-
case MSG_BROADCAST_BT_CONNECTION_STATE:
onBroadcastScoConnectionState(msg.arg1);
break;
0,
null,
SAFE_VOLUME_CONFIGURE_TIMEOUT_MS);
- } else if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
- || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) {
- if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
- // a package is being removed, not replaced
- String packageName = intent.getData().getSchemeSpecificPart();
- if (packageName != null) {
- cleanupMediaButtonReceiverForPackage(packageName, true);
- }
- }
- } else if (action.equals(Intent.ACTION_PACKAGE_ADDED)
- || action.equals(Intent.ACTION_PACKAGE_CHANGED)) {
- String packageName = intent.getData().getSchemeSpecificPart();
- if (packageName != null) {
- cleanupMediaButtonReceiverForPackage(packageName, false);
- }
} else if (action.equals(Intent.ACTION_SCREEN_ON)) {
AudioSystem.setParameters("screen_state=on");
} else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
null,
0);
// the current audio focus owner is no longer valid
- discardAudioFocusOwner();
+ mMediaFocusControl.discardAudioFocusOwner();
// load volume settings for new user
readAudioSettings(true /*userSwitch*/);
}
//==========================================================================================
- // AudioFocus
+ // RemoteControlDisplay / RemoteControlClient / Remote info
//==========================================================================================
-
- /* constant to identify focus stack entry that is used to hold the focus while the phone
- * is ringing or during a call. Used by com.android.internal.telephony.CallManager when
- * entering and exiting calls.
- */
- public final static String IN_VOICE_COMM_FOCUS_ID = "AudioFocus_For_Phone_Ring_And_Calls";
-
- private final static Object mAudioFocusLock = new Object();
-
- private final static Object mRingingLock = new Object();
-
- private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
- @Override
- public void onCallStateChanged(int state, String incomingNumber) {
- if (state == TelephonyManager.CALL_STATE_RINGING) {
- //Log.v(TAG, " CALL_STATE_RINGING");
- synchronized(mRingingLock) {
- mIsRinging = true;
- }
- } else if ((state == TelephonyManager.CALL_STATE_OFFHOOK)
- || (state == TelephonyManager.CALL_STATE_IDLE)) {
- synchronized(mRingingLock) {
- mIsRinging = false;
- }
- }
- }
- };
-
- /**
- * Discard the current audio focus owner.
- * Notify top of audio focus stack that it lost focus (regardless of possibility to reassign
- * focus), remove it from the stack, and clear the remote control display.
- */
- private void discardAudioFocusOwner() {
- synchronized(mAudioFocusLock) {
- if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null)) {
- // notify the current focus owner it lost focus after removing it from stack
- FocusStackEntry focusOwner = mFocusStack.pop();
- try {
- focusOwner.mFocusDispatcher.dispatchAudioFocusChange(
- AudioManager.AUDIOFOCUS_LOSS, focusOwner.mClientId);
- } catch (RemoteException e) {
- Log.e(TAG, "Failure to signal loss of audio focus due to "+ e);
- e.printStackTrace();
- }
- focusOwner.unlinkToDeath();
- // clear RCD
- synchronized(mRCStack) {
- clearRemoteControlDisplay_syncAfRcs();
- }
- }
- }
- }
-
- private void notifyTopOfAudioFocusStack() {
- // notify the top of the stack it gained focus
- if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null)) {
- if (canReassignAudioFocus()) {
- try {
- mFocusStack.peek().mFocusDispatcher.dispatchAudioFocusChange(
- AudioManager.AUDIOFOCUS_GAIN, mFocusStack.peek().mClientId);
- } catch (RemoteException e) {
- Log.e(TAG, "Failure to signal gain of audio control focus due to "+ e);
- e.printStackTrace();
- }
- }
- }
+ public void registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) {
+ mMediaFocusControl.registerRemoteControlDisplay(rcd, w, h);
}
- private static class FocusStackEntry {
- public int mStreamType = -1;// no stream type
- public IAudioFocusDispatcher mFocusDispatcher = null;
- public IBinder mSourceRef = null;
- public String mClientId;
- public int mFocusChangeType;
- public AudioFocusDeathHandler mHandler;
- public String mPackageName;
- public int mCallingUid;
-
- public FocusStackEntry() {
- }
-
- public FocusStackEntry(int streamType, int duration,
- IAudioFocusDispatcher afl, IBinder source, String id, AudioFocusDeathHandler hdlr,
- String pn, int uid) {
- mStreamType = streamType;
- mFocusDispatcher = afl;
- mSourceRef = source;
- mClientId = id;
- mFocusChangeType = duration;
- mHandler = hdlr;
- mPackageName = pn;
- mCallingUid = uid;
- }
-
- public void unlinkToDeath() {
- try {
- if (mSourceRef != null && mHandler != null) {
- mSourceRef.unlinkToDeath(mHandler, 0);
- mHandler = null;
- }
- } catch (java.util.NoSuchElementException e) {
- Log.e(TAG, "Encountered " + e + " in FocusStackEntry.unlinkToDeath()");
- }
- }
-
- @Override
- protected void finalize() throws Throwable {
- unlinkToDeath(); // unlink exception handled inside method
- super.finalize();
- }
+ public void unregisterRemoteControlDisplay(IRemoteControlDisplay rcd) {
+ mMediaFocusControl.unregisterRemoteControlDisplay(rcd);
}
- private final Stack<FocusStackEntry> mFocusStack = new Stack<FocusStackEntry>();
-
- /**
- * Helper function:
- * Display in the log the current entries in the audio focus stack
- */
- private void dumpFocusStack(PrintWriter pw) {
- pw.println("\nAudio Focus stack entries (last is top of stack):");
- synchronized(mAudioFocusLock) {
- Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator();
- while(stackIterator.hasNext()) {
- FocusStackEntry fse = stackIterator.next();
- pw.println(" source:" + fse.mSourceRef
- + " -- pack: " + fse.mPackageName
- + " -- client: " + fse.mClientId
- + " -- duration: " + fse.mFocusChangeType
- + " -- uid: " + fse.mCallingUid
- + " -- stream: " + fse.mStreamType);
- }
- }
+ public void remoteControlDisplayUsesBitmapSize(IRemoteControlDisplay rcd, int w, int h) {
+ mMediaFocusControl.remoteControlDisplayUsesBitmapSize(rcd, w, h);
}
- /**
- * Helper function:
- * Called synchronized on mAudioFocusLock
- * Remove a focus listener from the focus stack.
- * @param clientToRemove the focus listener
- * @param signal if true and the listener was at the top of the focus stack, i.e. it was holding
- * focus, notify the next item in the stack it gained focus.
- */
- private void removeFocusStackEntry(String clientToRemove, boolean signal) {
- // is the current top of the focus stack abandoning focus? (because of request, not death)
- if (!mFocusStack.empty() && mFocusStack.peek().mClientId.equals(clientToRemove))
- {
- //Log.i(TAG, " removeFocusStackEntry() removing top of stack");
- FocusStackEntry fse = mFocusStack.pop();
- fse.unlinkToDeath();
- if (signal) {
- // notify the new top of the stack it gained focus
- notifyTopOfAudioFocusStack();
- // there's a new top of the stack, let the remote control know
- synchronized(mRCStack) {
- checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
- }
- }
- } else {
- // focus is abandoned by a client that's not at the top of the stack,
- // no need to update focus.
- // (using an iterator on the stack so we can safely remove an entry after having
- // evaluated it, traversal order doesn't matter here)
- Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator();
- while(stackIterator.hasNext()) {
- FocusStackEntry fse = (FocusStackEntry)stackIterator.next();
- if(fse.mClientId.equals(clientToRemove)) {
- Log.i(TAG, " AudioFocus abandonAudioFocus(): removing entry for "
- + fse.mClientId);
- stackIterator.remove();
- fse.unlinkToDeath();
- }
- }
- }
+ public void remoteControlDisplayWantsPlaybackPositionSync(IRemoteControlDisplay rcd,
+ boolean wantsSync) {
+ mMediaFocusControl.remoteControlDisplayWantsPlaybackPositionSync(rcd, wantsSync);
}
- /**
- * Helper function:
- * Called synchronized on mAudioFocusLock
- * Remove focus listeners from the focus stack for a particular client when it has died.
- */
- private void removeFocusStackEntryForClient(IBinder cb) {
- // is the owner of the audio focus part of the client to remove?
- boolean isTopOfStackForClientToRemove = !mFocusStack.isEmpty() &&
- mFocusStack.peek().mSourceRef.equals(cb);
- // (using an iterator on the stack so we can safely remove an entry after having
- // evaluated it, traversal order doesn't matter here)
- Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator();
- while(stackIterator.hasNext()) {
- FocusStackEntry fse = (FocusStackEntry)stackIterator.next();
- if(fse.mSourceRef.equals(cb)) {
- Log.i(TAG, " AudioFocus abandonAudioFocus(): removing entry for "
- + fse.mClientId);
- stackIterator.remove();
- // the client just died, no need to unlink to its death
- }
- }
- if (isTopOfStackForClientToRemove) {
- // we removed an entry at the top of the stack:
- // notify the new top of the stack it gained focus.
- notifyTopOfAudioFocusStack();
- // there's a new top of the stack, let the remote control know
- synchronized(mRCStack) {
- checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
- }
- }
+ public void registerMediaButtonEventReceiverForCalls(ComponentName c) {
+ mMediaFocusControl.registerMediaButtonEventReceiverForCalls(c);
}
- /**
- * Helper function:
- * Returns true if the system is in a state where the focus can be reevaluated, false otherwise.
- */
- private boolean canReassignAudioFocus() {
- // focus requests are rejected during a phone call or when the phone is ringing
- // this is equivalent to IN_VOICE_COMM_FOCUS_ID having the focus
- if (!mFocusStack.isEmpty() && IN_VOICE_COMM_FOCUS_ID.equals(mFocusStack.peek().mClientId)) {
- return false;
- }
- return true;
+ public void unregisterMediaButtonEventReceiverForCalls() {
+ mMediaFocusControl.unregisterMediaButtonEventReceiverForCalls();
}
- /**
- * Inner class to monitor audio focus client deaths, and remove them from the audio focus
- * stack if necessary.
- */
- private class AudioFocusDeathHandler implements IBinder.DeathRecipient {
- private IBinder mCb; // To be notified of client's death
-
- AudioFocusDeathHandler(IBinder cb) {
- mCb = cb;
- }
-
- public void binderDied() {
- synchronized(mAudioFocusLock) {
- Log.w(TAG, " AudioFocus audio focus client died");
- removeFocusStackEntryForClient(mCb);
- }
- }
-
- public IBinder getBinder() {
- return mCb;
- }
+ public void registerMediaButtonIntent(PendingIntent pi, ComponentName c, IBinder token) {
+ mMediaFocusControl.registerMediaButtonIntent(pi, c, token);
}
-
- /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int) */
- public int requestAudioFocus(int mainStreamType, int focusChangeHint, IBinder cb,
- IAudioFocusDispatcher fd, String clientId, String callingPackageName) {
- Log.i(TAG, " AudioFocus requestAudioFocus() from " + clientId);
- // the main stream type for the audio focus request is currently not used. It may
- // potentially be used to handle multiple stream type-dependent audio focuses.
-
- // we need a valid binder callback for clients
- if (!cb.pingBinder()) {
- Log.e(TAG, " AudioFocus DOA client for requestAudioFocus(), aborting.");
- return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
- }
-
- if (mAppOps.noteOp(AppOpsManager.OP_TAKE_AUDIO_FOCUS, Binder.getCallingUid(),
- callingPackageName) != AppOpsManager.MODE_ALLOWED) {
- return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
- }
-
- synchronized(mAudioFocusLock) {
- if (!canReassignAudioFocus()) {
- return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
- }
-
- // handle the potential premature death of the new holder of the focus
- // (premature death == death before abandoning focus)
- // Register for client death notification
- AudioFocusDeathHandler afdh = new AudioFocusDeathHandler(cb);
- try {
- cb.linkToDeath(afdh, 0);
- } catch (RemoteException e) {
- // client has already died!
- Log.w(TAG, "AudioFocus requestAudioFocus() could not link to "+cb+" binder death");
- return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
- }
-
- if (!mFocusStack.empty() && mFocusStack.peek().mClientId.equals(clientId)) {
- // if focus is already owned by this client and the reason for acquiring the focus
- // hasn't changed, don't do anything
- if (mFocusStack.peek().mFocusChangeType == focusChangeHint) {
- // unlink death handler so it can be gc'ed.
- // linkToDeath() creates a JNI global reference preventing collection.
- cb.unlinkToDeath(afdh, 0);
- return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
- }
- // the reason for the audio focus request has changed: remove the current top of
- // stack and respond as if we had a new focus owner
- FocusStackEntry fse = mFocusStack.pop();
- fse.unlinkToDeath();
- }
-
- // notify current top of stack it is losing focus
- if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null)) {
- try {
- mFocusStack.peek().mFocusDispatcher.dispatchAudioFocusChange(
- -1 * focusChangeHint, // loss and gain codes are inverse of each other
- mFocusStack.peek().mClientId);
- } catch (RemoteException e) {
- Log.e(TAG, " Failure to signal loss of focus due to "+ e);
- e.printStackTrace();
- }
- }
-
- // focus requester might already be somewhere below in the stack, remove it
- removeFocusStackEntry(clientId, false /* signal */);
-
- // push focus requester at the top of the audio focus stack
- mFocusStack.push(new FocusStackEntry(mainStreamType, focusChangeHint, fd, cb,
- clientId, afdh, callingPackageName, Binder.getCallingUid()));
-
- // there's a new top of the stack, let the remote control know
- synchronized(mRCStack) {
- checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
- }
- }//synchronized(mAudioFocusLock)
-
- return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+ public void unregisterMediaButtonIntent(PendingIntent pi) {
+ mMediaFocusControl.unregisterMediaButtonIntent(pi);
}
-
- /** @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener) */
- public int abandonAudioFocus(IAudioFocusDispatcher fl, String clientId) {
- Log.i(TAG, " AudioFocus abandonAudioFocus() from " + clientId);
- try {
- // this will take care of notifying the new focus owner if needed
- synchronized(mAudioFocusLock) {
- removeFocusStackEntry(clientId, true);
- }
- } catch (java.util.ConcurrentModificationException cme) {
- // Catching this exception here is temporary. It is here just to prevent
- // a crash seen when the "Silent" notification is played. This is believed to be fixed
- // but this try catch block is left just to be safe.
- Log.e(TAG, "FATAL EXCEPTION AudioFocus abandonAudioFocus() caused " + cme);
- cme.printStackTrace();
- }
-
- return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+
+ public int registerRemoteControlClient(PendingIntent mediaIntent,
+ IRemoteControlClient rcClient, String callingPckg) {
+ return mMediaFocusControl.registerRemoteControlClient(mediaIntent, rcClient, callingPckg);
}
-
- public void unregisterAudioFocusClient(String clientId) {
- synchronized(mAudioFocusLock) {
- removeFocusStackEntry(clientId, false);
- }
+ public void unregisterRemoteControlClient(PendingIntent mediaIntent,
+ IRemoteControlClient rcClient) {
+ mMediaFocusControl.unregisterRemoteControlClient(mediaIntent, rcClient);
}
-
- //==========================================================================================
- // RemoteControl
- //==========================================================================================
- public void dispatchMediaKeyEvent(KeyEvent keyEvent) {
- filterMediaKeyEvent(keyEvent, false /*needWakeLock*/);
+ public void setRemoteControlClientPlaybackPosition(int generationId, long timeMs) {
+ mMediaFocusControl.setRemoteControlClientPlaybackPosition(generationId, timeMs);
}
- public void dispatchMediaKeyEventUnderWakelock(KeyEvent keyEvent) {
- filterMediaKeyEvent(keyEvent, true /*needWakeLock*/);
+ public void registerRemoteVolumeObserverForRcc(int rccId, IRemoteVolumeObserver rvo) {
+ mMediaFocusControl.registerRemoteVolumeObserverForRcc(rccId, rvo);
}
- private void filterMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
- // sanity check on the incoming key event
- if (!isValidMediaKeyEvent(keyEvent)) {
- Log.e(TAG, "not dispatching invalid media key event " + keyEvent);
- return;
- }
- // event filtering for telephony
- synchronized(mRingingLock) {
- synchronized(mRCStack) {
- if ((mMediaReceiverForCalls != null) &&
- (mIsRinging || (getMode() == AudioSystem.MODE_IN_CALL))) {
- dispatchMediaKeyEventForCalls(keyEvent, needWakeLock);
- return;
- }
- }
- }
- // event filtering based on voice-based interactions
- if (isValidVoiceInputKeyCode(keyEvent.getKeyCode())) {
- filterVoiceInputKeyEvent(keyEvent, needWakeLock);
- } else {
- dispatchMediaKeyEvent(keyEvent, needWakeLock);
- }
+ public int getRemoteStreamVolume() {
+ return mMediaFocusControl.getRemoteStreamVolume();
}
- /**
- * Handles the dispatching of the media button events to the telephony package.
- * Precondition: mMediaReceiverForCalls != null
- * @param keyEvent a non-null KeyEvent whose key code is one of the supported media buttons
- * @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
- * is dispatched.
- */
- private void dispatchMediaKeyEventForCalls(KeyEvent keyEvent, boolean needWakeLock) {
- Intent keyIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
- keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
- keyIntent.setPackage(mMediaReceiverForCalls.getPackageName());
- if (needWakeLock) {
- mMediaEventWakeLock.acquire();
- keyIntent.putExtra(EXTRA_WAKELOCK_ACQUIRED, WAKELOCK_RELEASE_ON_FINISHED);
- }
- final long ident = Binder.clearCallingIdentity();
- try {
- mContext.sendOrderedBroadcastAsUser(keyIntent, UserHandle.ALL,
- null, mKeyEventDone, mAudioHandler, Activity.RESULT_OK, null, null);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
+ public int getRemoteStreamMaxVolume() {
+ return mMediaFocusControl.getRemoteStreamMaxVolume();
}
- /**
- * Handles the dispatching of the media button events to one of the registered listeners,
- * or if there was none, broadcast an ACTION_MEDIA_BUTTON intent to the rest of the system.
- * @param keyEvent a non-null KeyEvent whose key code is one of the supported media buttons
- * @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
- * is dispatched.
- */
- private void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
- if (needWakeLock) {
- mMediaEventWakeLock.acquire();
- }
- Intent keyIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
- keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
- synchronized(mRCStack) {
- if (!mRCStack.empty()) {
- // send the intent that was registered by the client
- try {
- mRCStack.peek().mMediaIntent.send(mContext,
- needWakeLock ? WAKELOCK_RELEASE_ON_FINISHED : 0 /*code*/,
- keyIntent, AudioService.this, mAudioHandler);
- } catch (CanceledException e) {
- Log.e(TAG, "Error sending pending intent " + mRCStack.peek());
- e.printStackTrace();
- }
- } else {
- // legacy behavior when nobody registered their media button event receiver
- // through AudioManager
- if (needWakeLock) {
- keyIntent.putExtra(EXTRA_WAKELOCK_ACQUIRED, WAKELOCK_RELEASE_ON_FINISHED);
- }
- final long ident = Binder.clearCallingIdentity();
- try {
- mContext.sendOrderedBroadcastAsUser(keyIntent, UserHandle.ALL,
- null, mKeyEventDone,
- mAudioHandler, Activity.RESULT_OK, null, null);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- }
- }
+ public void setRemoteStreamVolume(int index) {
+ mMediaFocusControl.setRemoteStreamVolume(index);
}
- /**
- * The different actions performed in response to a voice button key event.
- */
- private final static int VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS = 1;
- private final static int VOICEBUTTON_ACTION_START_VOICE_INPUT = 2;
- private final static int VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS = 3;
-
- private final Object mVoiceEventLock = new Object();
- private boolean mVoiceButtonDown;
- private boolean mVoiceButtonHandled;
-
- /**
- * Filter key events that may be used for voice-based interactions
- * @param keyEvent a non-null KeyEvent whose key code is that of one of the supported
- * media buttons that can be used to trigger voice-based interactions.
- * @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
- * is dispatched.
- */
- private void filterVoiceInputKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
- if (DEBUG_RC) {
- Log.v(TAG, "voice input key event: " + keyEvent + ", needWakeLock=" + needWakeLock);
- }
-
- int voiceButtonAction = VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS;
- int keyAction = keyEvent.getAction();
- synchronized (mVoiceEventLock) {
- if (keyAction == KeyEvent.ACTION_DOWN) {
- if (keyEvent.getRepeatCount() == 0) {
- // initial down
- mVoiceButtonDown = true;
- mVoiceButtonHandled = false;
- } else if (mVoiceButtonDown && !mVoiceButtonHandled
- && (keyEvent.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) {
- // long-press, start voice-based interactions
- mVoiceButtonHandled = true;
- voiceButtonAction = VOICEBUTTON_ACTION_START_VOICE_INPUT;
- }
- } else if (keyAction == KeyEvent.ACTION_UP) {
- if (mVoiceButtonDown) {
- // voice button up
- mVoiceButtonDown = false;
- if (!mVoiceButtonHandled && !keyEvent.isCanceled()) {
- voiceButtonAction = VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS;
- }
- }
- }
- }//synchronized (mVoiceEventLock)
-
- // take action after media button event filtering for voice-based interactions
- switch (voiceButtonAction) {
- case VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS:
- if (DEBUG_RC) Log.v(TAG, " ignore key event");
- break;
- case VOICEBUTTON_ACTION_START_VOICE_INPUT:
- if (DEBUG_RC) Log.v(TAG, " start voice-based interactions");
- // then start the voice-based interactions
- startVoiceBasedInteractions(needWakeLock);
- break;
- case VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS:
- if (DEBUG_RC) Log.v(TAG, " send simulated key event, wakelock=" + needWakeLock);
- sendSimulatedMediaButtonEvent(keyEvent, needWakeLock);
- break;
- }
+ public void setPlaybackStateForRcc(int rccId, int state, long timeMs, float speed) {
+ mMediaFocusControl.setPlaybackStateForRcc(rccId, state, timeMs, speed);
}
- private void sendSimulatedMediaButtonEvent(KeyEvent originalKeyEvent, boolean needWakeLock) {
- // send DOWN event
- KeyEvent keyEvent = KeyEvent.changeAction(originalKeyEvent, KeyEvent.ACTION_DOWN);
- dispatchMediaKeyEvent(keyEvent, needWakeLock);
- // send UP event
- keyEvent = KeyEvent.changeAction(originalKeyEvent, KeyEvent.ACTION_UP);
- dispatchMediaKeyEvent(keyEvent, needWakeLock);
-
+ public void setPlaybackInfoForRcc(int rccId, int what, int value) {
+ mMediaFocusControl.setPlaybackInfoForRcc(rccId, what, value);
}
-
- private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
- if (keyEvent == null) {
- return false;
- }
- final int keyCode = keyEvent.getKeyCode();
- switch (keyCode) {
- case KeyEvent.KEYCODE_MUTE:
- case KeyEvent.KEYCODE_HEADSETHOOK:
- case KeyEvent.KEYCODE_MEDIA_PLAY:
- case KeyEvent.KEYCODE_MEDIA_PAUSE:
- case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
- case KeyEvent.KEYCODE_MEDIA_STOP:
- case KeyEvent.KEYCODE_MEDIA_NEXT:
- case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
- case KeyEvent.KEYCODE_MEDIA_REWIND:
- case KeyEvent.KEYCODE_MEDIA_RECORD:
- case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
- case KeyEvent.KEYCODE_MEDIA_CLOSE:
- case KeyEvent.KEYCODE_MEDIA_EJECT:
- case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
- break;
- default:
- return false;
- }
- return true;
+ public void dispatchMediaKeyEvent(KeyEvent keyEvent) {
+ mMediaFocusControl.dispatchMediaKeyEvent(keyEvent);
}
- /**
- * Checks whether the given key code is one that can trigger the launch of voice-based
- * interactions.
- * @param keyCode the key code associated with the key event
- * @return true if the key is one of the supported voice-based interaction triggers
- */
- private static boolean isValidVoiceInputKeyCode(int keyCode) {
- if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK) {
- return true;
- } else {
- return false;
- }
+ public void dispatchMediaKeyEventUnderWakelock(KeyEvent keyEvent) {
+ mMediaFocusControl.dispatchMediaKeyEventUnderWakelock(keyEvent);
}
- /**
- * Tell the system to start voice-based interactions / voice commands
- */
- private void startVoiceBasedInteractions(boolean needWakeLock) {
- Intent voiceIntent = null;
- // select which type of search to launch:
- // - screen on and device unlocked: action is ACTION_WEB_SEARCH
- // - device locked or screen off: action is ACTION_VOICE_SEARCH_HANDS_FREE
- // with EXTRA_SECURE set to true if the device is securely locked
- PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
- boolean isLocked = mKeyguardManager != null && mKeyguardManager.isKeyguardLocked();
- if (!isLocked && pm.isScreenOn()) {
- voiceIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
- Log.i(TAG, "voice-based interactions: about to use ACTION_WEB_SEARCH");
- } else {
- voiceIntent = new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE);
- voiceIntent.putExtra(RecognizerIntent.EXTRA_SECURE,
- isLocked && mKeyguardManager.isKeyguardSecure());
- Log.i(TAG, "voice-based interactions: about to use ACTION_VOICE_SEARCH_HANDS_FREE");
- }
- // start the search activity
- if (needWakeLock) {
- mMediaEventWakeLock.acquire();
- }
- try {
- if (voiceIntent != null) {
- voiceIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- mContext.startActivity(voiceIntent);
- }
- } catch (ActivityNotFoundException e) {
- Log.w(TAG, "No activity for search: " + e);
- } finally {
- if (needWakeLock) {
- mMediaEventWakeLock.release();
- }
- }
+ //==========================================================================================
+ // Audio Focus
+ //==========================================================================================
+ public int requestAudioFocus(int mainStreamType, int durationHint, IBinder cb,
+ IAudioFocusDispatcher fd, String clientId, String callingPackageName) {
+ return mMediaFocusControl.requestAudioFocus(mainStreamType, durationHint, cb, fd,
+ clientId, callingPackageName);
}
- private PowerManager.WakeLock mMediaEventWakeLock;
-
- private static final int WAKELOCK_RELEASE_ON_FINISHED = 1980; //magic number
-
- // only set when wakelock was acquired, no need to check value when received
- private static final String EXTRA_WAKELOCK_ACQUIRED =
- "android.media.AudioService.WAKELOCK_ACQUIRED";
-
- public void onSendFinished(PendingIntent pendingIntent, Intent intent,
- int resultCode, String resultData, Bundle resultExtras) {
- if (resultCode == WAKELOCK_RELEASE_ON_FINISHED) {
- mMediaEventWakeLock.release();
- }
+ public int abandonAudioFocus(IAudioFocusDispatcher fd, String clientId) {
+ return mMediaFocusControl.abandonAudioFocus(fd, clientId);
}
- BroadcastReceiver mKeyEventDone = new BroadcastReceiver() {
- public void onReceive(Context context, Intent intent) {
- if (intent == null) {
- return;
- }
- Bundle extras = intent.getExtras();
- if (extras == null) {
- return;
- }
- if (extras.containsKey(EXTRA_WAKELOCK_ACQUIRED)) {
- mMediaEventWakeLock.release();
- }
- }
- };
-
- /**
- * Synchronization on mCurrentRcLock always inside a block synchronized on mRCStack
- */
- private final Object mCurrentRcLock = new Object();
- /**
- * The one remote control client which will receive a request for display information.
- * This object may be null.
- * Access protected by mCurrentRcLock.
- */
- private IRemoteControlClient mCurrentRcClient = null;
-
- private final static int RC_INFO_NONE = 0;
- private final static int RC_INFO_ALL =
- RemoteControlClient.FLAG_INFORMATION_REQUEST_ALBUM_ART |
- RemoteControlClient.FLAG_INFORMATION_REQUEST_KEY_MEDIA |
- RemoteControlClient.FLAG_INFORMATION_REQUEST_METADATA |
- RemoteControlClient.FLAG_INFORMATION_REQUEST_PLAYSTATE;
-
- /**
- * A monotonically increasing generation counter for mCurrentRcClient.
- * Only accessed with a lock on mCurrentRcLock.
- * No value wrap-around issues as we only act on equal values.
- */
- private int mCurrentRcClientGen = 0;
-
- /**
- * Inner class to monitor remote control client deaths, and remove the client for the
- * remote control stack if necessary.
- */
- private class RcClientDeathHandler implements IBinder.DeathRecipient {
- final private IBinder mCb; // To be notified of client's death
- final private PendingIntent mMediaIntent;
-
- RcClientDeathHandler(IBinder cb, PendingIntent pi) {
- mCb = cb;
- mMediaIntent = pi;
- }
-
- public void binderDied() {
- Log.w(TAG, " RemoteControlClient died");
- // remote control client died, make sure the displays don't use it anymore
- // by setting its remote control client to null
- registerRemoteControlClient(mMediaIntent, null/*rcClient*/, null/*ignored*/);
- // the dead client was maybe handling remote playback, reevaluate
- postReevaluateRemote();
- }
-
- public IBinder getBinder() {
- return mCb;
- }
- }
-
- /**
- * A global counter for RemoteControlClient identifiers
- */
- private static int sLastRccId = 0;
-
- private class RemotePlaybackState {
- int mRccId;
- int mVolume;
- int mVolumeMax;
- int mVolumeHandling;
-
- private RemotePlaybackState(int id, int vol, int volMax) {
- mRccId = id;
- mVolume = vol;
- mVolumeMax = volMax;
- mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
- }
- }
-
- /**
- * Internal cache for the playback information of the RemoteControlClient whose volume gets to
- * be controlled by the volume keys ("main"), so we don't have to iterate over the RC stack
- * every time we need this info.
- */
- private RemotePlaybackState mMainRemote;
- /**
- * Indicates whether the "main" RemoteControlClient is considered active.
- * Use synchronized on mMainRemote.
- */
- private boolean mMainRemoteIsActive;
- /**
- * Indicates whether there is remote playback going on. True even if there is no "active"
- * remote playback (mMainRemoteIsActive is false), but a RemoteControlClient has declared it
- * handles remote playback.
- * Use synchronized on mMainRemote.
- */
- private boolean mHasRemotePlayback;
-
- private static class RccPlaybackState {
- public int mState;
- public long mPositionMs;
- public float mSpeed;
-
- public RccPlaybackState(int state, long positionMs, float speed) {
- mState = state;
- mPositionMs = positionMs;
- mSpeed = speed;
- }
-
- public void reset() {
- mState = RemoteControlClient.PLAYSTATE_STOPPED;
- mPositionMs = RemoteControlClient.PLAYBACK_POSITION_INVALID;
- mSpeed = RemoteControlClient.PLAYBACK_SPEED_1X;
- }
-
- @Override
- public String toString() {
- return stateToString() + ", "
- + ((mPositionMs == RemoteControlClient.PLAYBACK_POSITION_INVALID) ?
- "PLAYBACK_POSITION_INVALID ," : String.valueOf(mPositionMs)) + "ms ,"
- + mSpeed + "X";
- }
-
- private String stateToString() {
- switch (mState) {
- case RemoteControlClient.PLAYSTATE_NONE:
- return "PLAYSTATE_NONE";
- case RemoteControlClient.PLAYSTATE_STOPPED:
- return "PLAYSTATE_STOPPED";
- case RemoteControlClient.PLAYSTATE_PAUSED:
- return "PLAYSTATE_PAUSED";
- case RemoteControlClient.PLAYSTATE_PLAYING:
- return "PLAYSTATE_PLAYING";
- case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
- return "PLAYSTATE_FAST_FORWARDING";
- case RemoteControlClient.PLAYSTATE_REWINDING:
- return "PLAYSTATE_REWINDING";
- case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
- return "PLAYSTATE_SKIPPING_FORWARDS";
- case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
- return "PLAYSTATE_SKIPPING_BACKWARDS";
- case RemoteControlClient.PLAYSTATE_BUFFERING:
- return "PLAYSTATE_BUFFERING";
- case RemoteControlClient.PLAYSTATE_ERROR:
- return "PLAYSTATE_ERROR";
- default:
- return "[invalid playstate]";
- }
- }
- }
-
- private static class RemoteControlStackEntry implements DeathRecipient {
- public int mRccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
- final public AudioService mService;
- /**
- * The target for the ACTION_MEDIA_BUTTON events.
- * Always non null.
- */
- final public PendingIntent mMediaIntent;
- /**
- * The registered media button event receiver.
- * Always non null.
- */
- final public ComponentName mReceiverComponent;
- public IBinder mToken;
- public String mCallingPackageName;
- public int mCallingUid;
- /**
- * Provides access to the information to display on the remote control.
- * May be null (when a media button event receiver is registered,
- * but no remote control client has been registered) */
- public IRemoteControlClient mRcClient;
- public RcClientDeathHandler mRcClientDeathHandler;
- /**
- * Information only used for non-local playback
- */
- public int mPlaybackType;
- public int mPlaybackVolume;
- public int mPlaybackVolumeMax;
- public int mPlaybackVolumeHandling;
- public int mPlaybackStream;
- public RccPlaybackState mPlaybackState;
- public IRemoteVolumeObserver mRemoteVolumeObs;
-
- public void resetPlaybackInfo() {
- mPlaybackType = RemoteControlClient.PLAYBACK_TYPE_LOCAL;
- mPlaybackVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
- mPlaybackVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
- mPlaybackVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
- mPlaybackStream = AudioManager.STREAM_MUSIC;
- mPlaybackState.reset();
- mRemoteVolumeObs = null;
- }
-
- /** precondition: mediaIntent != null */
- public RemoteControlStackEntry(AudioService service, PendingIntent mediaIntent,
- ComponentName eventReceiver, IBinder token) {
- mService = service;
- mMediaIntent = mediaIntent;
- mReceiverComponent = eventReceiver;
- mToken = token;
- mCallingUid = -1;
- mRcClient = null;
- mRccId = ++sLastRccId;
- mPlaybackState = new RccPlaybackState(
- RemoteControlClient.PLAYSTATE_STOPPED,
- RemoteControlClient.PLAYBACK_POSITION_INVALID,
- RemoteControlClient.PLAYBACK_SPEED_1X);
-
- resetPlaybackInfo();
- if (mToken != null) {
- try {
- mToken.linkToDeath(this, 0);
- } catch (RemoteException e) {
- mService.mAudioHandler.post(new Runnable() {
- @Override public void run() {
- mService.unregisterMediaButtonIntent(mMediaIntent);
- }
- });
- }
- }
- }
-
- public void unlinkToRcClientDeath() {
- if ((mRcClientDeathHandler != null) && (mRcClientDeathHandler.mCb != null)) {
- try {
- mRcClientDeathHandler.mCb.unlinkToDeath(mRcClientDeathHandler, 0);
- mRcClientDeathHandler = null;
- } catch (java.util.NoSuchElementException e) {
- // not much we can do here
- Log.e(TAG, "Encountered " + e + " in unlinkToRcClientDeath()");
- e.printStackTrace();
- }
- }
- }
-
- public void destroy() {
- unlinkToRcClientDeath();
- if (mToken != null) {
- mToken.unlinkToDeath(this, 0);
- mToken = null;
- }
- }
-
- @Override
- public void binderDied() {
- mService.unregisterMediaButtonIntent(mMediaIntent);
- }
-
- @Override
- protected void finalize() throws Throwable {
- destroy(); // unlink exception handled inside method
- super.finalize();
- }
- }
-
- /**
- * The stack of remote control event receivers.
- * Code sections and methods that modify the remote control event receiver stack are
- * synchronized on mRCStack, but also BEFORE on mFocusLock as any change in either
- * stack, audio focus or RC, can lead to a change in the remote control display
- */
- private final Stack<RemoteControlStackEntry> mRCStack = new Stack<RemoteControlStackEntry>();
-
- /**
- * The component the telephony package can register so telephony calls have priority to
- * handle media button events
- */
- private ComponentName mMediaReceiverForCalls = null;
-
- /**
- * Helper function:
- * Display in the log the current entries in the remote control focus stack
- */
- private void dumpRCStack(PrintWriter pw) {
- pw.println("\nRemote Control stack entries (last is top of stack):");
- synchronized(mRCStack) {
- Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
- while(stackIterator.hasNext()) {
- RemoteControlStackEntry rcse = stackIterator.next();
- pw.println(" pi: " + rcse.mMediaIntent +
- " -- pack: " + rcse.mCallingPackageName +
- " -- ercvr: " + rcse.mReceiverComponent +
- " -- client: " + rcse.mRcClient +
- " -- uid: " + rcse.mCallingUid +
- " -- type: " + rcse.mPlaybackType +
- " state: " + rcse.mPlaybackState);
- }
- }
- }
-
- /**
- * Helper function:
- * Display in the log the current entries in the remote control stack, focusing
- * on RemoteControlClient data
- */
- private void dumpRCCStack(PrintWriter pw) {
- pw.println("\nRemote Control Client stack entries (last is top of stack):");
- synchronized(mRCStack) {
- Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
- while(stackIterator.hasNext()) {
- RemoteControlStackEntry rcse = stackIterator.next();
- pw.println(" uid: " + rcse.mCallingUid +
- " -- id: " + rcse.mRccId +
- " -- type: " + rcse.mPlaybackType +
- " -- state: " + rcse.mPlaybackState +
- " -- vol handling: " + rcse.mPlaybackVolumeHandling +
- " -- vol: " + rcse.mPlaybackVolume +
- " -- volMax: " + rcse.mPlaybackVolumeMax +
- " -- volObs: " + rcse.mRemoteVolumeObs);
- }
- synchronized(mCurrentRcLock) {
- pw.println("\nCurrent remote control generation ID = " + mCurrentRcClientGen);
- }
- }
- synchronized (mMainRemote) {
- pw.println("\nRemote Volume State:");
- pw.println(" has remote: " + mHasRemotePlayback);
- pw.println(" is remote active: " + mMainRemoteIsActive);
- pw.println(" rccId: " + mMainRemote.mRccId);
- pw.println(" volume handling: "
- + ((mMainRemote.mVolumeHandling == RemoteControlClient.PLAYBACK_VOLUME_FIXED) ?
- "PLAYBACK_VOLUME_FIXED(0)" : "PLAYBACK_VOLUME_VARIABLE(1)"));
- pw.println(" volume: " + mMainRemote.mVolume);
- pw.println(" volume steps: " + mMainRemote.mVolumeMax);
- }
- }
-
- /**
- * Helper function:
- * Display in the log the current entries in the list of remote control displays
- */
- private void dumpRCDList(PrintWriter pw) {
- pw.println("\nRemote Control Display list entries:");
- synchronized(mRCStack) {
- final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
- while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
- pw.println(" IRCD: " + di.mRcDisplay +
- " -- w:" + di.mArtworkExpectedWidth +
- " -- h:" + di.mArtworkExpectedHeight+
- " -- wantsPosSync:" + di.mWantsPositionSync);
- }
- }
- }
-
- /**
- * Helper function:
- * Remove any entry in the remote control stack that has the same package name as packageName
- * Pre-condition: packageName != null
- */
- private void cleanupMediaButtonReceiverForPackage(String packageName, boolean removeAll) {
- synchronized(mRCStack) {
- if (mRCStack.empty()) {
- return;
- } else {
- final PackageManager pm = mContext.getPackageManager();
- RemoteControlStackEntry oldTop = mRCStack.peek();
- Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
- // iterate over the stack entries
- // (using an iterator on the stack so we can safely remove an entry after having
- // evaluated it, traversal order doesn't matter here)
- while(stackIterator.hasNext()) {
- RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
- if (removeAll && packageName.equals(rcse.mMediaIntent.getCreatorPackage())) {
- // a stack entry is from the package being removed, remove it from the stack
- stackIterator.remove();
- rcse.destroy();
- } else if (rcse.mReceiverComponent != null) {
- try {
- // Check to see if this receiver still exists.
- pm.getReceiverInfo(rcse.mReceiverComponent, 0);
- } catch (PackageManager.NameNotFoundException e) {
- // Not found -- remove it!
- stackIterator.remove();
- rcse.destroy();
- }
- }
- }
- if (mRCStack.empty()) {
- // no saved media button receiver
- mAudioHandler.sendMessage(
- mAudioHandler.obtainMessage(MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0,
- null));
- } else if (oldTop != mRCStack.peek()) {
- // the top of the stack has changed, save it in the system settings
- // by posting a message to persist it; only do this however if it has
- // a concrete component name (is not a transient registration)
- RemoteControlStackEntry rcse = mRCStack.peek();
- if (rcse.mReceiverComponent != null) {
- mAudioHandler.sendMessage(
- mAudioHandler.obtainMessage(MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0,
- rcse.mReceiverComponent));
- }
- }
- }
- }
- }
-
- /**
- * Helper function:
- * Restore remote control receiver from the system settings.
- */
- private void restoreMediaButtonReceiver() {
- String receiverName = Settings.System.getStringForUser(mContentResolver,
- Settings.System.MEDIA_BUTTON_RECEIVER, UserHandle.USER_CURRENT);
- if ((null != receiverName) && !receiverName.isEmpty()) {
- ComponentName eventReceiver = ComponentName.unflattenFromString(receiverName);
- if (eventReceiver == null) {
- // an invalid name was persisted
- return;
- }
- // construct a PendingIntent targeted to the restored component name
- // for the media button and register it
- Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
- // the associated intent will be handled by the component being registered
- mediaButtonIntent.setComponent(eventReceiver);
- PendingIntent pi = PendingIntent.getBroadcast(mContext,
- 0/*requestCode, ignored*/, mediaButtonIntent, 0/*flags*/);
- registerMediaButtonIntent(pi, eventReceiver, null);
- }
- }
-
- /**
- * Helper function:
- * Set the new remote control receiver at the top of the RC focus stack.
- * Called synchronized on mAudioFocusLock, then mRCStack
- * precondition: mediaIntent != null
- */
- private void pushMediaButtonReceiver_syncAfRcs(PendingIntent mediaIntent, ComponentName target,
- IBinder token) {
- // already at top of stack?
- if (!mRCStack.empty() && mRCStack.peek().mMediaIntent.equals(mediaIntent)) {
- return;
- }
- if (mAppOps.noteOp(AppOpsManager.OP_TAKE_MEDIA_BUTTONS, Binder.getCallingUid(),
- mediaIntent.getCreatorPackage()) != AppOpsManager.MODE_ALLOWED) {
- return;
- }
- RemoteControlStackEntry rcse = null;
- boolean wasInsideStack = false;
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- rcse = mRCStack.elementAt(index);
- if(rcse.mMediaIntent.equals(mediaIntent)) {
- // ok to remove element while traversing the stack since we're leaving the loop
- mRCStack.removeElementAt(index);
- wasInsideStack = true;
- break;
- }
- }
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
- }
- if (!wasInsideStack) {
- rcse = new RemoteControlStackEntry(this, mediaIntent, target, token);
- }
- mRCStack.push(rcse); // rcse is never null
-
- // post message to persist the default media button receiver
- if (target != null) {
- mAudioHandler.sendMessage( mAudioHandler.obtainMessage(
- MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0, target/*obj*/) );
- }
- }
-
- /**
- * Helper function:
- * Remove the remote control receiver from the RC focus stack.
- * Called synchronized on mAudioFocusLock, then mRCStack
- * precondition: pi != null
- */
- private void removeMediaButtonReceiver_syncAfRcs(PendingIntent pi) {
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
- if (rcse.mMediaIntent.equals(pi)) {
- rcse.destroy();
- // ok to remove element while traversing the stack since we're leaving the loop
- mRCStack.removeElementAt(index);
- break;
- }
- }
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
- }
- }
-
- /**
- * Helper function:
- * Called synchronized on mRCStack
- */
- private boolean isCurrentRcController(PendingIntent pi) {
- if (!mRCStack.empty() && mRCStack.peek().mMediaIntent.equals(pi)) {
- return true;
- }
- return false;
- }
-
- //==========================================================================================
- // Remote control display / client
- //==========================================================================================
- /**
- * Update the remote control displays with the new "focused" client generation
- */
- private void setNewRcClientOnDisplays_syncRcsCurrc(int newClientGeneration,
- PendingIntent newMediaIntent, boolean clearing) {
- synchronized(mRCStack) {
- if (mRcDisplays.size() > 0) {
- final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
- while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = displayIterator.next();
- try {
- di.mRcDisplay.setCurrentClientId(
- newClientGeneration, newMediaIntent, clearing);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead display in setNewRcClientOnDisplays_syncRcsCurrc()",e);
- di.release();
- displayIterator.remove();
- }
- }
- }
- }
- }
-
- /**
- * Update the remote control clients with the new "focused" client generation
- */
- private void setNewRcClientGenerationOnClients_syncRcsCurrc(int newClientGeneration) {
- // (using an iterator on the stack so we can safely remove an entry if needed,
- // traversal order doesn't matter here as we update all entries)
- Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
- while(stackIterator.hasNext()) {
- RemoteControlStackEntry se = stackIterator.next();
- if ((se != null) && (se.mRcClient != null)) {
- try {
- se.mRcClient.setCurrentClientGenerationId(newClientGeneration);
- } catch (RemoteException e) {
- Log.w(TAG, "Dead client in setNewRcClientGenerationOnClients_syncRcsCurrc()",e);
- stackIterator.remove();
- se.unlinkToRcClientDeath();
- }
- }
- }
- }
-
- /**
- * Update the displays and clients with the new "focused" client generation and name
- * @param newClientGeneration the new generation value matching a client update
- * @param newMediaIntent the media button event receiver associated with the client.
- * May be null, which implies there is no registered media button event receiver.
- * @param clearing true if the new client generation value maps to a remote control update
- * where the display should be cleared.
- */
- private void setNewRcClient_syncRcsCurrc(int newClientGeneration,
- PendingIntent newMediaIntent, boolean clearing) {
- // send the new valid client generation ID to all displays
- setNewRcClientOnDisplays_syncRcsCurrc(newClientGeneration, newMediaIntent, clearing);
- // send the new valid client generation ID to all clients
- setNewRcClientGenerationOnClients_syncRcsCurrc(newClientGeneration);
- }
-
- /**
- * Called when processing MSG_RCDISPLAY_CLEAR event
- */
- private void onRcDisplayClear() {
- if (DEBUG_RC) Log.i(TAG, "Clear remote control display");
-
- synchronized(mRCStack) {
- synchronized(mCurrentRcLock) {
- mCurrentRcClientGen++;
- // synchronously update the displays and clients with the new client generation
- setNewRcClient_syncRcsCurrc(mCurrentRcClientGen,
- null /*newMediaIntent*/, true /*clearing*/);
- }
- }
- }
-
- /**
- * Called when processing MSG_RCDISPLAY_UPDATE event
- */
- private void onRcDisplayUpdate(RemoteControlStackEntry rcse, int flags /* USED ?*/) {
- synchronized(mRCStack) {
- synchronized(mCurrentRcLock) {
- if ((mCurrentRcClient != null) && (mCurrentRcClient.equals(rcse.mRcClient))) {
- if (DEBUG_RC) Log.i(TAG, "Display/update remote control ");
-
- mCurrentRcClientGen++;
- // synchronously update the displays and clients with
- // the new client generation
- setNewRcClient_syncRcsCurrc(mCurrentRcClientGen,
- rcse.mMediaIntent /*newMediaIntent*/,
- false /*clearing*/);
-
- // tell the current client that it needs to send info
- try {
- mCurrentRcClient.onInformationRequested(mCurrentRcClientGen, flags);
- } catch (RemoteException e) {
- Log.e(TAG, "Current valid remote client is dead: "+e);
- mCurrentRcClient = null;
- }
- } else {
- // the remote control display owner has changed between the
- // the message to update the display was sent, and the time it
- // gets to be processed (now)
- }
- }
- }
- }
-
-
- /**
- * Helper function:
- * Called synchronized on mRCStack
- */
- private void clearRemoteControlDisplay_syncAfRcs() {
- synchronized(mCurrentRcLock) {
- mCurrentRcClient = null;
- }
- // will cause onRcDisplayClear() to be called in AudioService's handler thread
- mAudioHandler.sendMessage( mAudioHandler.obtainMessage(MSG_RCDISPLAY_CLEAR) );
- }
-
- /**
- * Helper function for code readability: only to be called from
- * checkUpdateRemoteControlDisplay_syncAfRcs() which checks the preconditions for
- * this method.
- * Preconditions:
- * - called synchronized mAudioFocusLock then on mRCStack
- * - mRCStack.isEmpty() is false
- */
- private void updateRemoteControlDisplay_syncAfRcs(int infoChangedFlags) {
- RemoteControlStackEntry rcse = mRCStack.peek();
- int infoFlagsAboutToBeUsed = infoChangedFlags;
- // this is where we enforce opt-in for information display on the remote controls
- // with the new AudioManager.registerRemoteControlClient() API
- if (rcse.mRcClient == null) {
- //Log.w(TAG, "Can't update remote control display with null remote control client");
- clearRemoteControlDisplay_syncAfRcs();
- return;
- }
- synchronized(mCurrentRcLock) {
- if (!rcse.mRcClient.equals(mCurrentRcClient)) {
- // new RC client, assume every type of information shall be queried
- infoFlagsAboutToBeUsed = RC_INFO_ALL;
- }
- mCurrentRcClient = rcse.mRcClient;
- }
- // will cause onRcDisplayUpdate() to be called in AudioService's handler thread
- mAudioHandler.sendMessage( mAudioHandler.obtainMessage(MSG_RCDISPLAY_UPDATE,
- infoFlagsAboutToBeUsed /* arg1 */, 0, rcse /* obj, != null */) );
- }
-
- /**
- * Helper function:
- * Called synchronized on mAudioFocusLock, then mRCStack
- * Check whether the remote control display should be updated, triggers the update if required
- * @param infoChangedFlags the flags corresponding to the remote control client information
- * that has changed, if applicable (checking for the update conditions might trigger a
- * clear, rather than an update event).
- */
- private void checkUpdateRemoteControlDisplay_syncAfRcs(int infoChangedFlags) {
- // determine whether the remote control display should be refreshed
- // if either stack is empty, there is a mismatch, so clear the RC display
- if (mRCStack.isEmpty() || mFocusStack.isEmpty()) {
- clearRemoteControlDisplay_syncAfRcs();
- return;
- }
-
- // determine which entry in the AudioFocus stack to consider, and compare against the
- // top of the stack for the media button event receivers : simply using the top of the
- // stack would make the entry disappear from the RemoteControlDisplay in conditions such as
- // notifications playing during music playback.
- // Crawl the AudioFocus stack from the top until an entry is found with the following
- // characteristics:
- // - focus gain on STREAM_MUSIC stream
- // - non-transient focus gain on a stream other than music
- FocusStackEntry af = null;
- try {
- for (int index = mFocusStack.size()-1; index >= 0; index--) {
- FocusStackEntry fse = mFocusStack.elementAt(index);
- if ((fse.mStreamType == AudioManager.STREAM_MUSIC)
- || (fse.mFocusChangeType == AudioManager.AUDIOFOCUS_GAIN)) {
- af = fse;
- break;
- }
- }
- } catch (ArrayIndexOutOfBoundsException e) {
- Log.e(TAG, "Wrong index accessing audio focus stack when updating RCD: " + e);
- af = null;
- }
- if (af == null) {
- clearRemoteControlDisplay_syncAfRcs();
- return;
- }
-
- // if the audio focus and RC owners belong to different packages, there is a mismatch, clear
- if ((mRCStack.peek().mCallingPackageName != null)
- && (af.mPackageName != null)
- && !(mRCStack.peek().mCallingPackageName.compareTo(
- af.mPackageName) == 0)) {
- clearRemoteControlDisplay_syncAfRcs();
- return;
- }
- // if the audio focus didn't originate from the same Uid as the one in which the remote
- // control information will be retrieved, clear
- if (mRCStack.peek().mCallingUid != af.mCallingUid) {
- clearRemoteControlDisplay_syncAfRcs();
- return;
- }
-
- // refresh conditions were verified: update the remote controls
- // ok to call: synchronized mAudioFocusLock then on mRCStack, mRCStack is not empty
- updateRemoteControlDisplay_syncAfRcs(infoChangedFlags);
- }
-
- /**
- * Helper function:
- * Post a message to asynchronously move the media button event receiver associated with the
- * given remote control client ID to the top of the remote control stack
- * @param rccId
- */
- private void postPromoteRcc(int rccId) {
- sendMsg(mAudioHandler, MSG_PROMOTE_RCC, SENDMSG_REPLACE,
- rccId /*arg1*/, 0, null, 0/*delay*/);
- }
-
- private void onPromoteRcc(int rccId) {
- if (DEBUG_RC) { Log.d(TAG, "Promoting RCC " + rccId); }
- synchronized(mAudioFocusLock) {
- synchronized(mRCStack) {
- // ignore if given RCC ID is already at top of remote control stack
- if (!mRCStack.isEmpty() && (mRCStack.peek().mRccId == rccId)) {
- return;
- }
- int indexToPromote = -1;
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
- if (rcse.mRccId == rccId) {
- indexToPromote = index;
- break;
- }
- }
- if (indexToPromote >= 0) {
- if (DEBUG_RC) { Log.d(TAG, " moving RCC from index " + indexToPromote
- + " to " + (mRCStack.size()-1)); }
- final RemoteControlStackEntry rcse = mRCStack.remove(indexToPromote);
- mRCStack.push(rcse);
- // the RC stack changed, reevaluate the display
- checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
- }
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index accessing RC stack, lock error? ", e);
- }
- }//synchronized(mRCStack)
- }//synchronized(mAudioFocusLock)
- }
-
- /**
- * see AudioManager.registerMediaButtonIntent(PendingIntent pi, ComponentName c)
- * precondition: mediaIntent != null
- */
- public void registerMediaButtonIntent(PendingIntent mediaIntent, ComponentName eventReceiver,
- IBinder token) {
- Log.i(TAG, " Remote Control registerMediaButtonIntent() for " + mediaIntent);
-
- synchronized(mAudioFocusLock) {
- synchronized(mRCStack) {
- pushMediaButtonReceiver_syncAfRcs(mediaIntent, eventReceiver, token);
- // new RC client, assume every type of information shall be queried
- checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
- }
- }
- }
-
- /**
- * see AudioManager.unregisterMediaButtonIntent(PendingIntent mediaIntent)
- * precondition: mediaIntent != null, eventReceiver != null
- */
- public void unregisterMediaButtonIntent(PendingIntent mediaIntent)
- {
- Log.i(TAG, " Remote Control unregisterMediaButtonIntent() for " + mediaIntent);
-
- synchronized(mAudioFocusLock) {
- synchronized(mRCStack) {
- boolean topOfStackWillChange = isCurrentRcController(mediaIntent);
- removeMediaButtonReceiver_syncAfRcs(mediaIntent);
- if (topOfStackWillChange) {
- // current RC client will change, assume every type of info needs to be queried
- checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
- }
- }
- }
- }
-
- /**
- * see AudioManager.registerMediaButtonEventReceiverForCalls(ComponentName c)
- * precondition: c != null
- */
- public void registerMediaButtonEventReceiverForCalls(ComponentName c) {
- if (mContext.checkCallingPermission("android.permission.MODIFY_PHONE_STATE")
- != PackageManager.PERMISSION_GRANTED) {
- Log.e(TAG, "Invalid permissions to register media button receiver for calls");
- return;
- }
- synchronized(mRCStack) {
- mMediaReceiverForCalls = c;
- }
- }
-
- /**
- * see AudioManager.unregisterMediaButtonEventReceiverForCalls()
- */
- public void unregisterMediaButtonEventReceiverForCalls() {
- if (mContext.checkCallingPermission("android.permission.MODIFY_PHONE_STATE")
- != PackageManager.PERMISSION_GRANTED) {
- Log.e(TAG, "Invalid permissions to unregister media button receiver for calls");
- return;
- }
- synchronized(mRCStack) {
- mMediaReceiverForCalls = null;
- }
- }
-
- /**
- * see AudioManager.registerRemoteControlClient(ComponentName eventReceiver, ...)
- * @return the unique ID of the RemoteControlStackEntry associated with the RemoteControlClient
- * Note: using this method with rcClient == null is a way to "disable" the IRemoteControlClient
- * without modifying the RC stack, but while still causing the display to refresh (will
- * become blank as a result of this)
- */
- public int registerRemoteControlClient(PendingIntent mediaIntent,
- IRemoteControlClient rcClient, String callingPackageName) {
- if (DEBUG_RC) Log.i(TAG, "Register remote control client rcClient="+rcClient);
- int rccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
- synchronized(mAudioFocusLock) {
- synchronized(mRCStack) {
- // store the new display information
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
- if(rcse.mMediaIntent.equals(mediaIntent)) {
- // already had a remote control client?
- if (rcse.mRcClientDeathHandler != null) {
- // stop monitoring the old client's death
- rcse.unlinkToRcClientDeath();
- }
- // save the new remote control client
- rcse.mRcClient = rcClient;
- rcse.mCallingPackageName = callingPackageName;
- rcse.mCallingUid = Binder.getCallingUid();
- if (rcClient == null) {
- // here rcse.mRcClientDeathHandler is null;
- rcse.resetPlaybackInfo();
- break;
- }
- rccId = rcse.mRccId;
-
- // there is a new (non-null) client:
- // 1/ give the new client the displays (if any)
- if (mRcDisplays.size() > 0) {
- plugRemoteControlDisplaysIntoClient_syncRcStack(rcse.mRcClient);
- }
- // 2/ monitor the new client's death
- IBinder b = rcse.mRcClient.asBinder();
- RcClientDeathHandler rcdh =
- new RcClientDeathHandler(b, rcse.mMediaIntent);
- try {
- b.linkToDeath(rcdh, 0);
- } catch (RemoteException e) {
- // remote control client is DOA, disqualify it
- Log.w(TAG, "registerRemoteControlClient() has a dead client " + b);
- rcse.mRcClient = null;
- }
- rcse.mRcClientDeathHandler = rcdh;
- break;
- }
- }//for
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index accessing RC stack, lock error? ", e);
- }
-
- // if the eventReceiver is at the top of the stack
- // then check for potential refresh of the remote controls
- if (isCurrentRcController(mediaIntent)) {
- checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
- }
- }//synchronized(mRCStack)
- }//synchronized(mAudioFocusLock)
- return rccId;
- }
-
- /**
- * see AudioManager.unregisterRemoteControlClient(PendingIntent pi, ...)
- * rcClient is guaranteed non-null
- */
- public void unregisterRemoteControlClient(PendingIntent mediaIntent,
- IRemoteControlClient rcClient) {
- if (DEBUG_RC) Log.i(TAG, "Unregister remote control client rcClient="+rcClient);
- synchronized(mAudioFocusLock) {
- synchronized(mRCStack) {
- boolean topRccChange = false;
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
- if ((rcse.mMediaIntent.equals(mediaIntent))
- && rcClient.equals(rcse.mRcClient)) {
- // we found the IRemoteControlClient to unregister
- // stop monitoring its death
- rcse.unlinkToRcClientDeath();
- // reset the client-related fields
- rcse.mRcClient = null;
- rcse.mCallingPackageName = null;
- topRccChange = (index == mRCStack.size()-1);
- // there can only be one matching RCC in the RC stack, we're done
- break;
- }
- }
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index accessing RC stack, lock error? ", e);
- }
- if (topRccChange) {
- // no more RCC for the RCD, check for potential refresh of the remote controls
- checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
- }
- }
- }
- }
-
-
- /**
- * A class to encapsulate all the information about a remote control display.
- * After instanciation, init() must always be called before the object is added in the list
- * of displays.
- * Before being removed from the list of displays, release() must always be called (otherwise
- * it will leak death handlers).
- */
- private class DisplayInfoForServer implements IBinder.DeathRecipient {
- /** may never be null */
- private IRemoteControlDisplay mRcDisplay;
- private IBinder mRcDisplayBinder;
- private int mArtworkExpectedWidth = -1;
- private int mArtworkExpectedHeight = -1;
- private boolean mWantsPositionSync = false;
-
- public DisplayInfoForServer(IRemoteControlDisplay rcd, int w, int h) {
- if (DEBUG_RC) Log.i(TAG, "new DisplayInfoForServer for " + rcd + " w=" + w + " h=" + h);
- mRcDisplay = rcd;
- mRcDisplayBinder = rcd.asBinder();
- mArtworkExpectedWidth = w;
- mArtworkExpectedHeight = h;
- }
-
- public boolean init() {
- try {
- mRcDisplayBinder.linkToDeath(this, 0);
- } catch (RemoteException e) {
- // remote control display is DOA, disqualify it
- Log.w(TAG, "registerRemoteControlDisplay() has a dead client " + mRcDisplayBinder);
- return false;
- }
- return true;
- }
-
- public void release() {
- try {
- mRcDisplayBinder.unlinkToDeath(this, 0);
- } catch (java.util.NoSuchElementException e) {
- // not much we can do here, the display should have been unregistered anyway
- Log.e(TAG, "Error in DisplaInfoForServer.relase()", e);
- }
- }
-
- public void binderDied() {
- synchronized(mRCStack) {
- Log.w(TAG, "RemoteControl: display " + mRcDisplay + " died");
- // remove the display from the list
- final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
- while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
- if (di.mRcDisplay == mRcDisplay) {
- if (DEBUG_RC) Log.w(TAG, " RCD removed from list");
- displayIterator.remove();
- return;
- }
- }
- }
- }
- }
-
- /**
- * The remote control displays.
- * Access synchronized on mRCStack
- */
- private ArrayList<DisplayInfoForServer> mRcDisplays = new ArrayList<DisplayInfoForServer>(1);
-
- /**
- * Plug each registered display into the specified client
- * @param rcc, guaranteed non null
- */
- private void plugRemoteControlDisplaysIntoClient_syncRcStack(IRemoteControlClient rcc) {
- final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
- while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
- try {
- rcc.plugRemoteControlDisplay(di.mRcDisplay, di.mArtworkExpectedWidth,
- di.mArtworkExpectedHeight);
- if (di.mWantsPositionSync) {
- rcc.setWantsSyncForDisplay(di.mRcDisplay, true);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Error connecting RCD to RCC in RCC registration",e);
- }
- }
- }
-
- /**
- * Is the remote control display interface already registered
- * @param rcd
- * @return true if the IRemoteControlDisplay is already in the list of displays
- */
- private boolean rcDisplayIsPluggedIn_syncRcStack(IRemoteControlDisplay rcd) {
- final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
- while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
- if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Register an IRemoteControlDisplay.
- * 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.
- * @see android.media.IAudioService#registerRemoteControlDisplay(android.media.IRemoteControlDisplay, int, int)
- * @param rcd the IRemoteControlDisplay to register. No effect if null.
- * @param w the maximum width of the expected bitmap. Negative or zero values indicate this
- * 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.
- */
- public void registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) {
- if (DEBUG_RC) Log.d(TAG, ">>> registerRemoteControlDisplay("+rcd+")");
- synchronized(mAudioFocusLock) {
- synchronized(mRCStack) {
- if ((rcd == null) || rcDisplayIsPluggedIn_syncRcStack(rcd)) {
- return;
- }
- DisplayInfoForServer di = new DisplayInfoForServer(rcd, w, h);
- if (!di.init()) {
- if (DEBUG_RC) Log.e(TAG, " error registering RCD");
- return;
- }
- // add RCD to list of displays
- mRcDisplays.add(di);
-
- // let all the remote control clients know there is a new display (so the remote
- // control stack traversal order doesn't matter).
- Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
- while(stackIterator.hasNext()) {
- RemoteControlStackEntry rcse = stackIterator.next();
- if(rcse.mRcClient != null) {
- try {
- rcse.mRcClient.plugRemoteControlDisplay(rcd, w, h);
- } catch (RemoteException e) {
- Log.e(TAG, "Error connecting RCD to client: ", e);
- }
- }
- }
-
- // we have a new display, of which all the clients are now aware: have it be updated
- checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
- }
- }
- }
-
- /**
- * Unregister an IRemoteControlDisplay.
- * No effect if the IRemoteControlDisplay hasn't been successfully registered.
- * @see android.media.IAudioService#unregisterRemoteControlDisplay(android.media.IRemoteControlDisplay)
- * @param rcd the IRemoteControlDisplay to unregister. No effect if null.
- */
- public void unregisterRemoteControlDisplay(IRemoteControlDisplay rcd) {
- if (DEBUG_RC) Log.d(TAG, "<<< unregisterRemoteControlDisplay("+rcd+")");
- synchronized(mRCStack) {
- if (rcd == null) {
- return;
- }
-
- boolean displayWasPluggedIn = false;
- final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
- while (displayIterator.hasNext() && !displayWasPluggedIn) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
- if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
- displayWasPluggedIn = true;
- di.release();
- displayIterator.remove();
- }
- }
-
- if (displayWasPluggedIn) {
- // disconnect this remote control display from all the clients, so the remote
- // control stack traversal order doesn't matter
- final Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
- while(stackIterator.hasNext()) {
- final RemoteControlStackEntry rcse = stackIterator.next();
- if(rcse.mRcClient != null) {
- try {
- rcse.mRcClient.unplugRemoteControlDisplay(rcd);
- } catch (RemoteException e) {
- Log.e(TAG, "Error disconnecting remote control display to client: ", e);
- }
- }
- }
- } else {
- if (DEBUG_RC) Log.w(TAG, " trying to unregister unregistered RCD");
- }
- }
- }
-
- /**
- * Update the size of the artwork used by an IRemoteControlDisplay.
- * @see android.media.IAudioService#remoteControlDisplayUsesBitmapSize(android.media.IRemoteControlDisplay, int, int)
- * @param rcd the IRemoteControlDisplay with the new artwork size requirement
- * @param w the maximum width of the expected bitmap. Negative or zero values indicate this
- * 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.
- */
- public void remoteControlDisplayUsesBitmapSize(IRemoteControlDisplay rcd, int w, int h) {
- synchronized(mRCStack) {
- final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
- boolean artworkSizeUpdate = false;
- while (displayIterator.hasNext() && !artworkSizeUpdate) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
- if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
- if ((di.mArtworkExpectedWidth != w) || (di.mArtworkExpectedHeight != h)) {
- di.mArtworkExpectedWidth = w;
- di.mArtworkExpectedHeight = h;
- artworkSizeUpdate = true;
- }
- }
- }
- if (artworkSizeUpdate) {
- // RCD is currently plugged in and its artwork size has changed, notify all RCCs,
- // stack traversal order doesn't matter
- final Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
- while(stackIterator.hasNext()) {
- final RemoteControlStackEntry rcse = stackIterator.next();
- if(rcse.mRcClient != null) {
- try {
- rcse.mRcClient.setBitmapSizeForDisplay(rcd, w, h);
- } catch (RemoteException e) {
- Log.e(TAG, "Error setting bitmap size for RCD on RCC: ", e);
- }
- }
- }
- }
- }
- }
-
- /**
- * Controls whether a remote control display needs periodic checks of the RemoteControlClient
- * playback position to verify that the estimated position has not drifted from the actual
- * position. By default the check is not performed.
- * The IRemoteControlDisplay must have been previously registered for this to have any effect.
- * @param rcd the IRemoteControlDisplay for which the anti-drift mechanism will be enabled
- * or disabled. Not null.
- * @param wantsSync if true, RemoteControlClient instances which expose their playback position
- * to the framework will regularly compare the estimated playback position with the actual
- * position, and will update the IRemoteControlDisplay implementation whenever a drift is
- * detected.
- */
- public void remoteControlDisplayWantsPlaybackPositionSync(IRemoteControlDisplay rcd,
- boolean wantsSync) {
- synchronized(mRCStack) {
- boolean rcdRegistered = false;
- // store the information about this display
- // (display stack traversal order doesn't matter).
- final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
- while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
- if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
- di.mWantsPositionSync = wantsSync;
- rcdRegistered = true;
- break;
- }
- }
- if (!rcdRegistered) {
- return;
- }
- // notify all current RemoteControlClients
- // (stack traversal order doesn't matter as we notify all RCCs)
- final Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
- while (stackIterator.hasNext()) {
- final RemoteControlStackEntry rcse = stackIterator.next();
- if (rcse.mRcClient != null) {
- try {
- rcse.mRcClient.setWantsSyncForDisplay(rcd, wantsSync);
- } catch (RemoteException e) {
- Log.e(TAG, "Error setting position sync flag for RCD on RCC: ", e);
- }
- }
- }
- }
- }
-
- public void setRemoteControlClientPlaybackPosition(int generationId, long timeMs) {
- // ignore position change requests if invalid generation ID
- synchronized(mRCStack) {
- synchronized(mCurrentRcLock) {
- if (mCurrentRcClientGen != generationId) {
- return;
- }
- }
- }
- // discard any unprocessed seek request in the message queue, and replace with latest
- sendMsg(mAudioHandler, MSG_RCC_SEEK_REQUEST, SENDMSG_REPLACE, generationId /* arg1 */,
- 0 /* arg2 ignored*/, new Long(timeMs) /* obj */, 0 /* delay */);
- }
-
- public void onSetRemoteControlClientPlaybackPosition(int generationId, long timeMs) {
- if(DEBUG_RC) Log.d(TAG, "onSetRemoteControlClientPlaybackPosition(genId=" + generationId +
- ", timeMs=" + timeMs + ")");
- synchronized(mRCStack) {
- synchronized(mCurrentRcLock) {
- if ((mCurrentRcClient != null) && (mCurrentRcClientGen == generationId)) {
- // tell the current client to seek to the requested location
- try {
- mCurrentRcClient.seekTo(generationId, timeMs);
- } catch (RemoteException e) {
- Log.e(TAG, "Current valid remote client is dead: "+e);
- mCurrentRcClient = null;
- }
- }
- }
- }
- }
-
- public void setPlaybackInfoForRcc(int rccId, int what, int value) {
- sendMsg(mAudioHandler, MSG_RCC_NEW_PLAYBACK_INFO, SENDMSG_QUEUE,
- rccId /* arg1 */, what /* arg2 */, Integer.valueOf(value) /* obj */, 0 /* delay */);
- }
-
- // handler for MSG_RCC_NEW_PLAYBACK_INFO
- private void onNewPlaybackInfoForRcc(int rccId, int key, int value) {
- if(DEBUG_RC) Log.d(TAG, "onNewPlaybackInfoForRcc(id=" + rccId +
- ", what=" + key + ",val=" + value + ")");
- synchronized(mRCStack) {
- // iterating from top of stack as playback information changes are more likely
- // on entries at the top of the remote control stack
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
- if (rcse.mRccId == rccId) {
- switch (key) {
- case RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE:
- rcse.mPlaybackType = value;
- postReevaluateRemote();
- break;
- case RemoteControlClient.PLAYBACKINFO_VOLUME:
- rcse.mPlaybackVolume = value;
- synchronized (mMainRemote) {
- if (rccId == mMainRemote.mRccId) {
- mMainRemote.mVolume = value;
- mVolumePanel.postHasNewRemotePlaybackInfo();
- }
- }
- break;
- case RemoteControlClient.PLAYBACKINFO_VOLUME_MAX:
- rcse.mPlaybackVolumeMax = value;
- synchronized (mMainRemote) {
- if (rccId == mMainRemote.mRccId) {
- mMainRemote.mVolumeMax = value;
- mVolumePanel.postHasNewRemotePlaybackInfo();
- }
- }
- break;
- case RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING:
- rcse.mPlaybackVolumeHandling = value;
- synchronized (mMainRemote) {
- if (rccId == mMainRemote.mRccId) {
- mMainRemote.mVolumeHandling = value;
- mVolumePanel.postHasNewRemotePlaybackInfo();
- }
- }
- break;
- case RemoteControlClient.PLAYBACKINFO_USES_STREAM:
- rcse.mPlaybackStream = value;
- break;
- default:
- Log.e(TAG, "unhandled key " + key + " for RCC " + rccId);
- break;
- }
- return;
- }
- }//for
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index mRCStack on onNewPlaybackInfoForRcc, lock error? ", e);
- }
- }
- }
-
- public void setPlaybackStateForRcc(int rccId, int state, long timeMs, float speed) {
- sendMsg(mAudioHandler, MSG_RCC_NEW_PLAYBACK_STATE, SENDMSG_QUEUE,
- rccId /* arg1 */, state /* arg2 */,
- new RccPlaybackState(state, timeMs, speed) /* obj */, 0 /* delay */);
- }
-
- public void onNewPlaybackStateForRcc(int rccId, int state, RccPlaybackState newState) {
- if(DEBUG_RC) Log.d(TAG, "onNewPlaybackStateForRcc(id=" + rccId + ", state=" + state
- + ", time=" + newState.mPositionMs + ", speed=" + newState.mSpeed + ")");
- synchronized(mRCStack) {
- // iterating from top of stack as playback information changes are more likely
- // on entries at the top of the remote control stack
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
- if (rcse.mRccId == rccId) {
- rcse.mPlaybackState = newState;
- synchronized (mMainRemote) {
- if (rccId == mMainRemote.mRccId) {
- mMainRemoteIsActive = isPlaystateActive(state);
- postReevaluateRemote();
- }
- }
- // an RCC moving to a "playing" state should become the media button
- // event receiver so it can be controlled, without requiring the
- // app to re-register its receiver
- if (isPlaystateActive(state)) {
- postPromoteRcc(rccId);
- }
- }
- }//for
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index on mRCStack in onNewPlaybackStateForRcc, lock error? ", e);
- }
- }
- }
-
- public void registerRemoteVolumeObserverForRcc(int rccId, IRemoteVolumeObserver rvo) {
- sendMsg(mAudioHandler, MSG_RCC_NEW_VOLUME_OBS, SENDMSG_QUEUE,
- rccId /* arg1 */, 0, rvo /* obj */, 0 /* delay */);
- }
-
- // handler for MSG_RCC_NEW_VOLUME_OBS
- private void onRegisterVolumeObserverForRcc(int rccId, IRemoteVolumeObserver rvo) {
- synchronized(mRCStack) {
- // The stack traversal order doesn't matter because there is only one stack entry
- // with this RCC ID, but the matching ID is more likely at the top of the stack, so
- // start iterating from the top.
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
- if (rcse.mRccId == rccId) {
- rcse.mRemoteVolumeObs = rvo;
- break;
- }
- }
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
- }
- }
- }
-
- /**
- * Checks if a remote client is active on the supplied stream type. Update the remote stream
- * volume state if found and playing
- * @param streamType
- * @return false if no remote playing is currently playing
- */
- private boolean checkUpdateRemoteStateIfActive(int streamType) {
- synchronized(mRCStack) {
- // iterating from top of stack as active playback is more likely on entries at the top
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
- if ((rcse.mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE)
- && isPlaystateActive(rcse.mPlaybackState.mState)
- && (rcse.mPlaybackStream == streamType)) {
- if (DEBUG_RC) Log.d(TAG, "remote playback active on stream " + streamType
- + ", vol =" + rcse.mPlaybackVolume);
- synchronized (mMainRemote) {
- mMainRemote.mRccId = rcse.mRccId;
- mMainRemote.mVolume = rcse.mPlaybackVolume;
- mMainRemote.mVolumeMax = rcse.mPlaybackVolumeMax;
- mMainRemote.mVolumeHandling = rcse.mPlaybackVolumeHandling;
- mMainRemoteIsActive = true;
- }
- return true;
- }
- }
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index accessing RC stack, lock error? ", e);
- }
- }
- synchronized (mMainRemote) {
- mMainRemoteIsActive = false;
- }
- return false;
- }
-
- /**
- * Returns true if the given playback state is considered "active", i.e. it describes a state
- * where playback is happening, or about to
- * @param playState the playback state to evaluate
- * @return true if active, false otherwise (inactive or unknown)
- */
- private static boolean isPlaystateActive(int playState) {
- switch (playState) {
- case RemoteControlClient.PLAYSTATE_PLAYING:
- case RemoteControlClient.PLAYSTATE_BUFFERING:
- case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
- case RemoteControlClient.PLAYSTATE_REWINDING:
- case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
- case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
- return true;
- default:
- return false;
- }
- }
-
- private void adjustRemoteVolume(int streamType, int direction, int flags) {
- int rccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
- boolean volFixed = false;
- synchronized (mMainRemote) {
- if (!mMainRemoteIsActive) {
- if (DEBUG_VOL) Log.w(TAG, "adjustRemoteVolume didn't find an active client");
- return;
- }
- rccId = mMainRemote.mRccId;
- volFixed = (mMainRemote.mVolumeHandling ==
- RemoteControlClient.PLAYBACK_VOLUME_FIXED);
- }
- // unlike "local" stream volumes, we can't compute the new volume based on the direction,
- // we can only notify the remote that volume needs to be updated, and we'll get an async'
- // update through setPlaybackInfoForRcc()
- if (!volFixed) {
- sendVolumeUpdateToRemote(rccId, direction);
- }
-
- // fire up the UI
- mVolumePanel.postRemoteVolumeChanged(streamType, flags);
- }
-
- private void sendVolumeUpdateToRemote(int rccId, int direction) {
- if (DEBUG_VOL) { Log.d(TAG, "sendVolumeUpdateToRemote(rccId="+rccId+" , dir="+direction); }
- if (direction == 0) {
- // only handling discrete events
- return;
- }
- IRemoteVolumeObserver rvo = null;
- synchronized (mRCStack) {
- // The stack traversal order doesn't matter because there is only one stack entry
- // with this RCC ID, but the matching ID is more likely at the top of the stack, so
- // start iterating from the top.
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
- //FIXME OPTIMIZE store this info in mMainRemote so we don't have to iterate?
- if (rcse.mRccId == rccId) {
- rvo = rcse.mRemoteVolumeObs;
- break;
- }
- }
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
- }
- }
- if (rvo != null) {
- try {
- rvo.dispatchRemoteVolumeUpdate(direction, -1);
- } catch (RemoteException e) {
- Log.e(TAG, "Error dispatching relative volume update", e);
- }
- }
- }
-
- public int getRemoteStreamMaxVolume() {
- synchronized (mMainRemote) {
- if (mMainRemote.mRccId == RemoteControlClient.RCSE_ID_UNREGISTERED) {
- return 0;
- }
- return mMainRemote.mVolumeMax;
- }
- }
-
- public int getRemoteStreamVolume() {
- synchronized (mMainRemote) {
- if (mMainRemote.mRccId == RemoteControlClient.RCSE_ID_UNREGISTERED) {
- return 0;
- }
- return mMainRemote.mVolume;
- }
- }
-
- public void setRemoteStreamVolume(int vol) {
- if (DEBUG_VOL) { Log.d(TAG, "setRemoteStreamVolume(vol="+vol+")"); }
- int rccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
- synchronized (mMainRemote) {
- if (mMainRemote.mRccId == RemoteControlClient.RCSE_ID_UNREGISTERED) {
- return;
- }
- rccId = mMainRemote.mRccId;
- }
- IRemoteVolumeObserver rvo = null;
- synchronized (mRCStack) {
- // The stack traversal order doesn't matter because there is only one stack entry
- // with this RCC ID, but the matching ID is more likely at the top of the stack, so
- // start iterating from the top.
- try {
- for (int index = mRCStack.size()-1; index >= 0; index--) {
- final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
- //FIXME OPTIMIZE store this info in mMainRemote so we don't have to iterate?
- if (rcse.mRccId == rccId) {
- rvo = rcse.mRemoteVolumeObs;
- break;
- }
- }
- } catch (ArrayIndexOutOfBoundsException e) {
- // not expected to happen, indicates improper concurrent modification
- Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
- }
- }
- if (rvo != null) {
- try {
- rvo.dispatchRemoteVolumeUpdate(0, vol);
- } catch (RemoteException e) {
- Log.e(TAG, "Error dispatching absolute volume update", e);
- }
- }
- }
-
- /**
- * Call to make AudioService reevaluate whether it's in a mode where remote players should
- * have their volume controlled. In this implementation this is only to reset whether
- * VolumePanel should display remote volumes
- */
- private void postReevaluateRemote() {
- sendMsg(mAudioHandler, MSG_REEVALUATE_REMOTE, SENDMSG_QUEUE, 0, 0, null, 0);
- }
-
- private void onReevaluateRemote() {
- if (DEBUG_VOL) { Log.w(TAG, "onReevaluateRemote()"); }
- // is there a registered RemoteControlClient that is handling remote playback
- boolean hasRemotePlayback = false;
- synchronized (mRCStack) {
- // iteration stops when PLAYBACK_TYPE_REMOTE is found, so remote control stack
- // traversal order doesn't matter
- Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
- while(stackIterator.hasNext()) {
- RemoteControlStackEntry rcse = stackIterator.next();
- if (rcse.mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE) {
- hasRemotePlayback = true;
- break;
- }
- }
- }
- synchronized (mMainRemote) {
- if (mHasRemotePlayback != hasRemotePlayback) {
- mHasRemotePlayback = hasRemotePlayback;
- mVolumePanel.postRemoteSliderVisibility(hasRemotePlayback);
- }
- }
+ public void unregisterAudioFocusClient(String clientId) {
+ mMediaFocusControl.unregisterAudioFocusClient(clientId);
}
//==========================================================================================
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
- dumpFocusStack(pw);
- dumpRCStack(pw);
- dumpRCCStack(pw);
- dumpRCDList(pw);
+ mMediaFocusControl.dump(pw);
dumpStreamStates(pw);
dumpRingerMode(pw);
pw.println("\nAudio routes:");
--- /dev/null
+/*
+ * 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.Activity;
+import android.app.AppOpsManager;
+import android.app.KeyguardManager;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.app.PendingIntent.OnFinished;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.IBinder.DeathRecipient;
+import android.provider.Settings;
+import android.speech.RecognizerIntent;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Stack;
+
+/**
+ * @hide
+ *
+ */
+public class MediaFocusControl implements OnFinished {
+
+ private static final String TAG = "MediaFocusControl";
+
+ /** Debug remote control client/display feature */
+ protected static final boolean DEBUG_RC = false;
+ /** Debug volumes */
+ protected static final boolean DEBUG_VOL = false;
+
+ /** Used to alter media button redirection when the phone is ringing. */
+ private boolean mIsRinging = false;
+
+ private final PowerManager.WakeLock mMediaEventWakeLock;
+ private final MediaEventHandler mEventHandler;
+ private final Context mContext;
+ private final ContentResolver mContentResolver;
+ private final VolumeController mVolumeController;
+ private final BroadcastReceiver mReceiver = new PackageIntentsReceiver();
+ private final AppOpsManager mAppOps;
+ private final KeyguardManager mKeyguardManager;
+ private final AudioService mAudioService;
+
+ protected MediaFocusControl(Looper looper, Context cntxt,
+ VolumeController volumeCtrl, AudioService as) {
+ mEventHandler = new MediaEventHandler(looper);
+ mContext = cntxt;
+ mContentResolver = mContext.getContentResolver();
+ mVolumeController = volumeCtrl;
+ mAudioService = as;
+
+ PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
+ mMediaEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleMediaEvent");
+ mMainRemote = new RemotePlaybackState(-1,
+ AudioService.getMaxStreamVolume(AudioManager.STREAM_MUSIC),
+ AudioService.getMaxStreamVolume(AudioManager.STREAM_MUSIC));
+
+ // Register for phone state monitoring
+ TelephonyManager tmgr = (TelephonyManager)
+ mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ tmgr.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+
+ // Register for package addition/removal/change intent broadcasts
+ // for media button receiver persistence
+ IntentFilter pkgFilter = new IntentFilter();
+ pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ pkgFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
+ pkgFilter.addDataScheme("package");
+ mContext.registerReceiver(mReceiver, pkgFilter);
+
+ mAppOps = (AppOpsManager)mContext.getSystemService(Context.APP_OPS_SERVICE);
+ mKeyguardManager =
+ (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
+
+ mHasRemotePlayback = false;
+ mMainRemoteIsActive = false;
+ postReevaluateRemote();
+ }
+
+ // event handler messages
+ private static final int MSG_PERSIST_MEDIABUTTONRECEIVER = 0;
+ private static final int MSG_RCDISPLAY_CLEAR = 1;
+ private static final int MSG_RCDISPLAY_UPDATE = 2;
+ private static final int MSG_REEVALUATE_REMOTE = 3;
+ private static final int MSG_RCC_NEW_PLAYBACK_INFO = 4;
+ private static final int MSG_RCC_NEW_VOLUME_OBS = 5;
+ 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;
+
+ // sendMsg() flags
+ /** 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 delay) {
+
+ if (existingMsgPolicy == SENDMSG_REPLACE) {
+ handler.removeMessages(msg);
+ } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
+ return;
+ }
+
+ handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delay);
+ }
+
+ private class MediaEventHandler extends Handler {
+ MediaEventHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case MSG_PERSIST_MEDIABUTTONRECEIVER:
+ onHandlePersistMediaButtonReceiver( (ComponentName) msg.obj );
+ break;
+
+ case MSG_RCDISPLAY_CLEAR:
+ onRcDisplayClear();
+ break;
+
+ case MSG_RCDISPLAY_UPDATE:
+ // msg.obj is guaranteed to be non null
+ onRcDisplayUpdate( (RemoteControlStackEntry) msg.obj, msg.arg1);
+ break;
+
+ case MSG_REEVALUATE_REMOTE:
+ onReevaluateRemote();
+ break;
+
+ case MSG_RCC_NEW_PLAYBACK_INFO:
+ onNewPlaybackInfoForRcc(msg.arg1 /* rccId */, msg.arg2 /* key */,
+ ((Integer)msg.obj).intValue() /* value */);
+ break;
+ case MSG_RCC_NEW_VOLUME_OBS:
+ onRegisterVolumeObserverForRcc(msg.arg1 /* rccId */,
+ (IRemoteVolumeObserver)msg.obj /* rvo */);
+ break;
+ case MSG_RCC_NEW_PLAYBACK_STATE:
+ onNewPlaybackStateForRcc(msg.arg1 /* rccId */,
+ msg.arg2 /* state */,
+ (RccPlaybackState)msg.obj /* newState */);
+ break;
+ case MSG_RCC_SEEK_REQUEST:
+ onSetRemoteControlClientPlaybackPosition(
+ msg.arg1 /* generationId */, ((Long)msg.obj).longValue() /* timeMs */);
+
+ case MSG_PROMOTE_RCC:
+ onPromoteRcc(msg.arg1);
+ break;
+ }
+ }
+ }
+
+ protected void dump(PrintWriter pw) {
+ dumpFocusStack(pw);
+ dumpRCStack(pw);
+ dumpRCCStack(pw);
+ dumpRCDList(pw);
+ }
+
+ private class PackageIntentsReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
+ || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) {
+ if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ // a package is being removed, not replaced
+ String packageName = intent.getData().getSchemeSpecificPart();
+ if (packageName != null) {
+ cleanupMediaButtonReceiverForPackage(packageName, true);
+ }
+ }
+ } else if (action.equals(Intent.ACTION_PACKAGE_ADDED)
+ || action.equals(Intent.ACTION_PACKAGE_CHANGED)) {
+ String packageName = intent.getData().getSchemeSpecificPart();
+ if (packageName != null) {
+ cleanupMediaButtonReceiverForPackage(packageName, false);
+ }
+ }
+ }
+ }
+
+ //==========================================================================================
+ // AudioFocus
+ //==========================================================================================
+
+ /* constant to identify focus stack entry that is used to hold the focus while the phone
+ * is ringing or during a call. Used by com.android.internal.telephony.CallManager when
+ * entering and exiting calls.
+ */
+ protected final static String IN_VOICE_COMM_FOCUS_ID = "AudioFocus_For_Phone_Ring_And_Calls";
+
+ private final static Object mAudioFocusLock = new Object();
+
+ private final static Object mRingingLock = new Object();
+
+ private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
+ @Override
+ public void onCallStateChanged(int state, String incomingNumber) {
+ if (state == TelephonyManager.CALL_STATE_RINGING) {
+ //Log.v(TAG, " CALL_STATE_RINGING");
+ synchronized(mRingingLock) {
+ mIsRinging = true;
+ }
+ } else if ((state == TelephonyManager.CALL_STATE_OFFHOOK)
+ || (state == TelephonyManager.CALL_STATE_IDLE)) {
+ synchronized(mRingingLock) {
+ mIsRinging = false;
+ }
+ }
+ }
+ };
+
+ /**
+ * Discard the current audio focus owner.
+ * Notify top of audio focus stack that it lost focus (regardless of possibility to reassign
+ * focus), remove it from the stack, and clear the remote control display.
+ */
+ protected void discardAudioFocusOwner() {
+ synchronized(mAudioFocusLock) {
+ if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null)) {
+ // notify the current focus owner it lost focus after removing it from stack
+ FocusStackEntry focusOwner = mFocusStack.pop();
+ try {
+ focusOwner.mFocusDispatcher.dispatchAudioFocusChange(
+ AudioManager.AUDIOFOCUS_LOSS, focusOwner.mClientId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failure to signal loss of audio focus due to "+ e);
+ e.printStackTrace();
+ }
+ focusOwner.unlinkToDeath();
+ // clear RCD
+ synchronized(mRCStack) {
+ clearRemoteControlDisplay_syncAfRcs();
+ }
+ }
+ }
+ }
+
+ private void notifyTopOfAudioFocusStack() {
+ // notify the top of the stack it gained focus
+ if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null)) {
+ if (canReassignAudioFocus()) {
+ try {
+ mFocusStack.peek().mFocusDispatcher.dispatchAudioFocusChange(
+ AudioManager.AUDIOFOCUS_GAIN, mFocusStack.peek().mClientId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failure to signal gain of audio control focus due to "+ e);
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ private static class FocusStackEntry {
+ public int mStreamType = -1;// no stream type
+ public IAudioFocusDispatcher mFocusDispatcher = null;
+ public IBinder mSourceRef = null;
+ public String mClientId;
+ public int mFocusChangeType;
+ public AudioFocusDeathHandler mHandler;
+ public String mPackageName;
+ public int mCallingUid;
+
+ public FocusStackEntry() {
+ }
+
+ public FocusStackEntry(int streamType, int duration,
+ IAudioFocusDispatcher afl, IBinder source, String id, AudioFocusDeathHandler hdlr,
+ String pn, int uid) {
+ mStreamType = streamType;
+ mFocusDispatcher = afl;
+ mSourceRef = source;
+ mClientId = id;
+ mFocusChangeType = duration;
+ mHandler = hdlr;
+ mPackageName = pn;
+ mCallingUid = uid;
+ }
+
+ public void unlinkToDeath() {
+ try {
+ if (mSourceRef != null && mHandler != null) {
+ mSourceRef.unlinkToDeath(mHandler, 0);
+ mHandler = null;
+ }
+ } catch (java.util.NoSuchElementException e) {
+ Log.e(TAG, "Encountered " + e + " in FocusStackEntry.unlinkToDeath()");
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ unlinkToDeath(); // unlink exception handled inside method
+ super.finalize();
+ }
+ }
+
+ private final Stack<FocusStackEntry> mFocusStack = new Stack<FocusStackEntry>();
+
+ /**
+ * Helper function:
+ * Display in the log the current entries in the audio focus stack
+ */
+ private void dumpFocusStack(PrintWriter pw) {
+ pw.println("\nAudio Focus stack entries (last is top of stack):");
+ synchronized(mAudioFocusLock) {
+ Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator();
+ while(stackIterator.hasNext()) {
+ FocusStackEntry fse = stackIterator.next();
+ pw.println(" source:" + fse.mSourceRef
+ + " -- pack: " + fse.mPackageName
+ + " -- client: " + fse.mClientId
+ + " -- duration: " + fse.mFocusChangeType
+ + " -- uid: " + fse.mCallingUid
+ + " -- stream: " + fse.mStreamType);
+ }
+ }
+ }
+
+ /**
+ * Helper function:
+ * Called synchronized on mAudioFocusLock
+ * Remove a focus listener from the focus stack.
+ * @param clientToRemove the focus listener
+ * @param signal if true and the listener was at the top of the focus stack, i.e. it was holding
+ * focus, notify the next item in the stack it gained focus.
+ */
+ private void removeFocusStackEntry(String clientToRemove, boolean signal) {
+ // is the current top of the focus stack abandoning focus? (because of request, not death)
+ if (!mFocusStack.empty() && mFocusStack.peek().mClientId.equals(clientToRemove))
+ {
+ //Log.i(TAG, " removeFocusStackEntry() removing top of stack");
+ FocusStackEntry fse = mFocusStack.pop();
+ fse.unlinkToDeath();
+ if (signal) {
+ // notify the new top of the stack it gained focus
+ notifyTopOfAudioFocusStack();
+ // there's a new top of the stack, let the remote control know
+ synchronized(mRCStack) {
+ checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
+ }
+ }
+ } else {
+ // focus is abandoned by a client that's not at the top of the stack,
+ // no need to update focus.
+ // (using an iterator on the stack so we can safely remove an entry after having
+ // evaluated it, traversal order doesn't matter here)
+ Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator();
+ while(stackIterator.hasNext()) {
+ FocusStackEntry fse = (FocusStackEntry)stackIterator.next();
+ if(fse.mClientId.equals(clientToRemove)) {
+ Log.i(TAG, " AudioFocus abandonAudioFocus(): removing entry for "
+ + fse.mClientId);
+ stackIterator.remove();
+ fse.unlinkToDeath();
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper function:
+ * Called synchronized on mAudioFocusLock
+ * Remove focus listeners from the focus stack for a particular client when it has died.
+ */
+ private void removeFocusStackEntryForClient(IBinder cb) {
+ // is the owner of the audio focus part of the client to remove?
+ boolean isTopOfStackForClientToRemove = !mFocusStack.isEmpty() &&
+ mFocusStack.peek().mSourceRef.equals(cb);
+ // (using an iterator on the stack so we can safely remove an entry after having
+ // evaluated it, traversal order doesn't matter here)
+ Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator();
+ while(stackIterator.hasNext()) {
+ FocusStackEntry fse = (FocusStackEntry)stackIterator.next();
+ if(fse.mSourceRef.equals(cb)) {
+ Log.i(TAG, " AudioFocus abandonAudioFocus(): removing entry for "
+ + fse.mClientId);
+ stackIterator.remove();
+ // the client just died, no need to unlink to its death
+ }
+ }
+ if (isTopOfStackForClientToRemove) {
+ // we removed an entry at the top of the stack:
+ // notify the new top of the stack it gained focus.
+ notifyTopOfAudioFocusStack();
+ // there's a new top of the stack, let the remote control know
+ synchronized(mRCStack) {
+ checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
+ }
+ }
+ }
+
+ /**
+ * Helper function:
+ * Returns true if the system is in a state where the focus can be reevaluated, false otherwise.
+ */
+ private boolean canReassignAudioFocus() {
+ // focus requests are rejected during a phone call or when the phone is ringing
+ // this is equivalent to IN_VOICE_COMM_FOCUS_ID having the focus
+ if (!mFocusStack.isEmpty() && IN_VOICE_COMM_FOCUS_ID.equals(mFocusStack.peek().mClientId)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Inner class to monitor audio focus client deaths, and remove them from the audio focus
+ * stack if necessary.
+ */
+ private class AudioFocusDeathHandler implements IBinder.DeathRecipient {
+ private IBinder mCb; // To be notified of client's death
+
+ AudioFocusDeathHandler(IBinder cb) {
+ mCb = cb;
+ }
+
+ public void binderDied() {
+ synchronized(mAudioFocusLock) {
+ Log.w(TAG, " AudioFocus audio focus client died");
+ removeFocusStackEntryForClient(mCb);
+ }
+ }
+
+ public IBinder getBinder() {
+ return mCb;
+ }
+ }
+
+
+ /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int) */
+ protected int requestAudioFocus(int mainStreamType, int focusChangeHint, IBinder cb,
+ IAudioFocusDispatcher fd, String clientId, String callingPackageName) {
+ Log.i(TAG, " AudioFocus requestAudioFocus() from " + clientId);
+ // the main stream type for the audio focus request is currently not used. It may
+ // potentially be used to handle multiple stream type-dependent audio focuses.
+
+ // we need a valid binder callback for clients
+ if (!cb.pingBinder()) {
+ Log.e(TAG, " AudioFocus DOA client for requestAudioFocus(), aborting.");
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ }
+
+ if (mAppOps.noteOp(AppOpsManager.OP_TAKE_AUDIO_FOCUS, Binder.getCallingUid(),
+ callingPackageName) != AppOpsManager.MODE_ALLOWED) {
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ }
+
+ synchronized(mAudioFocusLock) {
+ if (!canReassignAudioFocus()) {
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ }
+
+ // handle the potential premature death of the new holder of the focus
+ // (premature death == death before abandoning focus)
+ // Register for client death notification
+ AudioFocusDeathHandler afdh = new AudioFocusDeathHandler(cb);
+ try {
+ cb.linkToDeath(afdh, 0);
+ } catch (RemoteException e) {
+ // client has already died!
+ Log.w(TAG, "AudioFocus requestAudioFocus() could not link to "+cb+" binder death");
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ }
+
+ if (!mFocusStack.empty() && mFocusStack.peek().mClientId.equals(clientId)) {
+ // if focus is already owned by this client and the reason for acquiring the focus
+ // hasn't changed, don't do anything
+ if (mFocusStack.peek().mFocusChangeType == focusChangeHint) {
+ // unlink death handler so it can be gc'ed.
+ // linkToDeath() creates a JNI global reference preventing collection.
+ cb.unlinkToDeath(afdh, 0);
+ return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+ }
+ // the reason for the audio focus request has changed: remove the current top of
+ // stack and respond as if we had a new focus owner
+ FocusStackEntry fse = mFocusStack.pop();
+ fse.unlinkToDeath();
+ }
+
+ // notify current top of stack it is losing focus
+ if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null)) {
+ try {
+ mFocusStack.peek().mFocusDispatcher.dispatchAudioFocusChange(
+ -1 * focusChangeHint, // loss and gain codes are inverse of each other
+ mFocusStack.peek().mClientId);
+ } catch (RemoteException e) {
+ Log.e(TAG, " Failure to signal loss of focus due to "+ e);
+ e.printStackTrace();
+ }
+ }
+
+ // focus requester might already be somewhere below in the stack, remove it
+ removeFocusStackEntry(clientId, false /* signal */);
+
+ // push focus requester at the top of the audio focus stack
+ mFocusStack.push(new FocusStackEntry(mainStreamType, focusChangeHint, fd, cb,
+ clientId, afdh, callingPackageName, Binder.getCallingUid()));
+
+ // there's a new top of the stack, let the remote control know
+ synchronized(mRCStack) {
+ checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
+ }
+ }//synchronized(mAudioFocusLock)
+
+ return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+ }
+
+ /** @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener) */
+ protected int abandonAudioFocus(IAudioFocusDispatcher fl, String clientId) {
+ Log.i(TAG, " AudioFocus abandonAudioFocus() from " + clientId);
+ try {
+ // this will take care of notifying the new focus owner if needed
+ synchronized(mAudioFocusLock) {
+ removeFocusStackEntry(clientId, true);
+ }
+ } catch (java.util.ConcurrentModificationException cme) {
+ // Catching this exception here is temporary. It is here just to prevent
+ // a crash seen when the "Silent" notification is played. This is believed to be fixed
+ // but this try catch block is left just to be safe.
+ Log.e(TAG, "FATAL EXCEPTION AudioFocus abandonAudioFocus() caused " + cme);
+ cme.printStackTrace();
+ }
+
+ return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+ }
+
+
+ protected void unregisterAudioFocusClient(String clientId) {
+ synchronized(mAudioFocusLock) {
+ removeFocusStackEntry(clientId, false);
+ }
+ }
+
+
+ //==========================================================================================
+ // RemoteControl
+ //==========================================================================================
+ protected void dispatchMediaKeyEvent(KeyEvent keyEvent) {
+ filterMediaKeyEvent(keyEvent, false /*needWakeLock*/);
+ }
+
+ protected void dispatchMediaKeyEventUnderWakelock(KeyEvent keyEvent) {
+ filterMediaKeyEvent(keyEvent, true /*needWakeLock*/);
+ }
+
+ private void filterMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
+ // sanity check on the incoming key event
+ if (!isValidMediaKeyEvent(keyEvent)) {
+ Log.e(TAG, "not dispatching invalid media key event " + keyEvent);
+ return;
+ }
+ // event filtering for telephony
+ synchronized(mRingingLock) {
+ synchronized(mRCStack) {
+ if ((mMediaReceiverForCalls != null) &&
+ (mIsRinging || (mAudioService.getMode() == AudioSystem.MODE_IN_CALL))) {
+ dispatchMediaKeyEventForCalls(keyEvent, needWakeLock);
+ return;
+ }
+ }
+ }
+ // event filtering based on voice-based interactions
+ if (isValidVoiceInputKeyCode(keyEvent.getKeyCode())) {
+ filterVoiceInputKeyEvent(keyEvent, needWakeLock);
+ } else {
+ dispatchMediaKeyEvent(keyEvent, needWakeLock);
+ }
+ }
+
+ /**
+ * Handles the dispatching of the media button events to the telephony package.
+ * Precondition: mMediaReceiverForCalls != null
+ * @param keyEvent a non-null KeyEvent whose key code is one of the supported media buttons
+ * @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
+ * is dispatched.
+ */
+ private void dispatchMediaKeyEventForCalls(KeyEvent keyEvent, boolean needWakeLock) {
+ Intent keyIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
+ keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
+ keyIntent.setPackage(mMediaReceiverForCalls.getPackageName());
+ if (needWakeLock) {
+ mMediaEventWakeLock.acquire();
+ keyIntent.putExtra(EXTRA_WAKELOCK_ACQUIRED, WAKELOCK_RELEASE_ON_FINISHED);
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mContext.sendOrderedBroadcastAsUser(keyIntent, UserHandle.ALL,
+ null, mKeyEventDone, mEventHandler, Activity.RESULT_OK, null, null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * Handles the dispatching of the media button events to one of the registered listeners,
+ * or if there was none, broadcast an ACTION_MEDIA_BUTTON intent to the rest of the system.
+ * @param keyEvent a non-null KeyEvent whose key code is one of the supported media buttons
+ * @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
+ * is dispatched.
+ */
+ private void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
+ if (needWakeLock) {
+ mMediaEventWakeLock.acquire();
+ }
+ Intent keyIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
+ keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
+ synchronized(mRCStack) {
+ if (!mRCStack.empty()) {
+ // send the intent that was registered by the client
+ try {
+ mRCStack.peek().mMediaIntent.send(mContext,
+ needWakeLock ? WAKELOCK_RELEASE_ON_FINISHED : 0 /*code*/,
+ keyIntent, this, mEventHandler);
+ } catch (CanceledException e) {
+ Log.e(TAG, "Error sending pending intent " + mRCStack.peek());
+ e.printStackTrace();
+ }
+ } else {
+ // legacy behavior when nobody registered their media button event receiver
+ // through AudioManager
+ if (needWakeLock) {
+ keyIntent.putExtra(EXTRA_WAKELOCK_ACQUIRED, WAKELOCK_RELEASE_ON_FINISHED);
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mContext.sendOrderedBroadcastAsUser(keyIntent, UserHandle.ALL,
+ null, mKeyEventDone,
+ mEventHandler, Activity.RESULT_OK, null, null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+ }
+
+ /**
+ * The different actions performed in response to a voice button key event.
+ */
+ private final static int VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS = 1;
+ private final static int VOICEBUTTON_ACTION_START_VOICE_INPUT = 2;
+ private final static int VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS = 3;
+
+ private final Object mVoiceEventLock = new Object();
+ private boolean mVoiceButtonDown;
+ private boolean mVoiceButtonHandled;
+
+ /**
+ * Filter key events that may be used for voice-based interactions
+ * @param keyEvent a non-null KeyEvent whose key code is that of one of the supported
+ * media buttons that can be used to trigger voice-based interactions.
+ * @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
+ * is dispatched.
+ */
+ private void filterVoiceInputKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
+ if (DEBUG_RC) {
+ Log.v(TAG, "voice input key event: " + keyEvent + ", needWakeLock=" + needWakeLock);
+ }
+
+ int voiceButtonAction = VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS;
+ int keyAction = keyEvent.getAction();
+ synchronized (mVoiceEventLock) {
+ if (keyAction == KeyEvent.ACTION_DOWN) {
+ if (keyEvent.getRepeatCount() == 0) {
+ // initial down
+ mVoiceButtonDown = true;
+ mVoiceButtonHandled = false;
+ } else if (mVoiceButtonDown && !mVoiceButtonHandled
+ && (keyEvent.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) {
+ // long-press, start voice-based interactions
+ mVoiceButtonHandled = true;
+ voiceButtonAction = VOICEBUTTON_ACTION_START_VOICE_INPUT;
+ }
+ } else if (keyAction == KeyEvent.ACTION_UP) {
+ if (mVoiceButtonDown) {
+ // voice button up
+ mVoiceButtonDown = false;
+ if (!mVoiceButtonHandled && !keyEvent.isCanceled()) {
+ voiceButtonAction = VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS;
+ }
+ }
+ }
+ }//synchronized (mVoiceEventLock)
+
+ // take action after media button event filtering for voice-based interactions
+ switch (voiceButtonAction) {
+ case VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS:
+ if (DEBUG_RC) Log.v(TAG, " ignore key event");
+ break;
+ case VOICEBUTTON_ACTION_START_VOICE_INPUT:
+ if (DEBUG_RC) Log.v(TAG, " start voice-based interactions");
+ // then start the voice-based interactions
+ startVoiceBasedInteractions(needWakeLock);
+ break;
+ case VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS:
+ if (DEBUG_RC) Log.v(TAG, " send simulated key event, wakelock=" + needWakeLock);
+ sendSimulatedMediaButtonEvent(keyEvent, needWakeLock);
+ break;
+ }
+ }
+
+ private void sendSimulatedMediaButtonEvent(KeyEvent originalKeyEvent, boolean needWakeLock) {
+ // send DOWN event
+ KeyEvent keyEvent = KeyEvent.changeAction(originalKeyEvent, KeyEvent.ACTION_DOWN);
+ dispatchMediaKeyEvent(keyEvent, needWakeLock);
+ // send UP event
+ keyEvent = KeyEvent.changeAction(originalKeyEvent, KeyEvent.ACTION_UP);
+ dispatchMediaKeyEvent(keyEvent, needWakeLock);
+
+ }
+
+
+ private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
+ if (keyEvent == null) {
+ return false;
+ }
+ final int keyCode = keyEvent.getKeyCode();
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MUTE:
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ case KeyEvent.KEYCODE_MEDIA_RECORD:
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ case KeyEvent.KEYCODE_MEDIA_CLOSE:
+ case KeyEvent.KEYCODE_MEDIA_EJECT:
+ case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether the given key code is one that can trigger the launch of voice-based
+ * interactions.
+ * @param keyCode the key code associated with the key event
+ * @return true if the key is one of the supported voice-based interaction triggers
+ */
+ private static boolean isValidVoiceInputKeyCode(int keyCode) {
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Tell the system to start voice-based interactions / voice commands
+ */
+ private void startVoiceBasedInteractions(boolean needWakeLock) {
+ Intent voiceIntent = null;
+ // select which type of search to launch:
+ // - screen on and device unlocked: action is ACTION_WEB_SEARCH
+ // - device locked or screen off: action is ACTION_VOICE_SEARCH_HANDS_FREE
+ // with EXTRA_SECURE set to true if the device is securely locked
+ PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
+ boolean isLocked = mKeyguardManager != null && mKeyguardManager.isKeyguardLocked();
+ if (!isLocked && pm.isScreenOn()) {
+ voiceIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
+ Log.i(TAG, "voice-based interactions: about to use ACTION_WEB_SEARCH");
+ } else {
+ voiceIntent = new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE);
+ voiceIntent.putExtra(RecognizerIntent.EXTRA_SECURE,
+ isLocked && mKeyguardManager.isKeyguardSecure());
+ Log.i(TAG, "voice-based interactions: about to use ACTION_VOICE_SEARCH_HANDS_FREE");
+ }
+ // start the search activity
+ if (needWakeLock) {
+ mMediaEventWakeLock.acquire();
+ }
+ try {
+ if (voiceIntent != null) {
+ voiceIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ mContext.startActivity(voiceIntent);
+ }
+ } catch (ActivityNotFoundException e) {
+ Log.w(TAG, "No activity for search: " + e);
+ } finally {
+ if (needWakeLock) {
+ mMediaEventWakeLock.release();
+ }
+ }
+ }
+
+ private static final int WAKELOCK_RELEASE_ON_FINISHED = 1980; //magic number
+
+ // only set when wakelock was acquired, no need to check value when received
+ private static final String EXTRA_WAKELOCK_ACQUIRED =
+ "android.media.AudioService.WAKELOCK_ACQUIRED";
+
+ public void onSendFinished(PendingIntent pendingIntent, Intent intent,
+ int resultCode, String resultData, Bundle resultExtras) {
+ if (resultCode == WAKELOCK_RELEASE_ON_FINISHED) {
+ mMediaEventWakeLock.release();
+ }
+ }
+
+ BroadcastReceiver mKeyEventDone = new BroadcastReceiver() {
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ Bundle extras = intent.getExtras();
+ if (extras == null) {
+ return;
+ }
+ if (extras.containsKey(EXTRA_WAKELOCK_ACQUIRED)) {
+ mMediaEventWakeLock.release();
+ }
+ }
+ };
+
+ /**
+ * Synchronization on mCurrentRcLock always inside a block synchronized on mRCStack
+ */
+ private final Object mCurrentRcLock = new Object();
+ /**
+ * The one remote control client which will receive a request for display information.
+ * This object may be null.
+ * Access protected by mCurrentRcLock.
+ */
+ private IRemoteControlClient mCurrentRcClient = null;
+
+ private final static int RC_INFO_NONE = 0;
+ private final static int RC_INFO_ALL =
+ RemoteControlClient.FLAG_INFORMATION_REQUEST_ALBUM_ART |
+ RemoteControlClient.FLAG_INFORMATION_REQUEST_KEY_MEDIA |
+ RemoteControlClient.FLAG_INFORMATION_REQUEST_METADATA |
+ RemoteControlClient.FLAG_INFORMATION_REQUEST_PLAYSTATE;
+
+ /**
+ * A monotonically increasing generation counter for mCurrentRcClient.
+ * Only accessed with a lock on mCurrentRcLock.
+ * No value wrap-around issues as we only act on equal values.
+ */
+ private int mCurrentRcClientGen = 0;
+
+ /**
+ * Inner class to monitor remote control client deaths, and remove the client for the
+ * remote control stack if necessary.
+ */
+ private class RcClientDeathHandler implements IBinder.DeathRecipient {
+ final private IBinder mCb; // To be notified of client's death
+ final private PendingIntent mMediaIntent;
+
+ RcClientDeathHandler(IBinder cb, PendingIntent pi) {
+ mCb = cb;
+ mMediaIntent = pi;
+ }
+
+ public void binderDied() {
+ Log.w(TAG, " RemoteControlClient died");
+ // remote control client died, make sure the displays don't use it anymore
+ // by setting its remote control client to null
+ registerRemoteControlClient(mMediaIntent, null/*rcClient*/, null/*ignored*/);
+ // the dead client was maybe handling remote playback, reevaluate
+ postReevaluateRemote();
+ }
+
+ public IBinder getBinder() {
+ return mCb;
+ }
+ }
+
+ /**
+ * A global counter for RemoteControlClient identifiers
+ */
+ private static int sLastRccId = 0;
+
+ private class RemotePlaybackState {
+ int mRccId;
+ int mVolume;
+ int mVolumeMax;
+ int mVolumeHandling;
+
+ private RemotePlaybackState(int id, int vol, int volMax) {
+ mRccId = id;
+ mVolume = vol;
+ mVolumeMax = volMax;
+ mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
+ }
+ }
+
+ /**
+ * Internal cache for the playback information of the RemoteControlClient whose volume gets to
+ * be controlled by the volume keys ("main"), so we don't have to iterate over the RC stack
+ * every time we need this info.
+ */
+ private RemotePlaybackState mMainRemote;
+ /**
+ * Indicates whether the "main" RemoteControlClient is considered active.
+ * Use synchronized on mMainRemote.
+ */
+ private boolean mMainRemoteIsActive;
+ /**
+ * Indicates whether there is remote playback going on. True even if there is no "active"
+ * remote playback (mMainRemoteIsActive is false), but a RemoteControlClient has declared it
+ * handles remote playback.
+ * Use synchronized on mMainRemote.
+ */
+ private boolean mHasRemotePlayback;
+
+ private static class RccPlaybackState {
+ public int mState;
+ public long mPositionMs;
+ public float mSpeed;
+
+ public RccPlaybackState(int state, long positionMs, float speed) {
+ mState = state;
+ mPositionMs = positionMs;
+ mSpeed = speed;
+ }
+
+ public void reset() {
+ mState = RemoteControlClient.PLAYSTATE_STOPPED;
+ mPositionMs = RemoteControlClient.PLAYBACK_POSITION_INVALID;
+ mSpeed = RemoteControlClient.PLAYBACK_SPEED_1X;
+ }
+
+ @Override
+ public String toString() {
+ return stateToString() + ", "
+ + ((mPositionMs == RemoteControlClient.PLAYBACK_POSITION_INVALID) ?
+ "PLAYBACK_POSITION_INVALID ," : String.valueOf(mPositionMs)) + "ms ,"
+ + mSpeed + "X";
+ }
+
+ private String stateToString() {
+ switch (mState) {
+ case RemoteControlClient.PLAYSTATE_NONE:
+ return "PLAYSTATE_NONE";
+ case RemoteControlClient.PLAYSTATE_STOPPED:
+ return "PLAYSTATE_STOPPED";
+ case RemoteControlClient.PLAYSTATE_PAUSED:
+ return "PLAYSTATE_PAUSED";
+ case RemoteControlClient.PLAYSTATE_PLAYING:
+ return "PLAYSTATE_PLAYING";
+ case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
+ return "PLAYSTATE_FAST_FORWARDING";
+ case RemoteControlClient.PLAYSTATE_REWINDING:
+ return "PLAYSTATE_REWINDING";
+ case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
+ return "PLAYSTATE_SKIPPING_FORWARDS";
+ case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
+ return "PLAYSTATE_SKIPPING_BACKWARDS";
+ case RemoteControlClient.PLAYSTATE_BUFFERING:
+ return "PLAYSTATE_BUFFERING";
+ case RemoteControlClient.PLAYSTATE_ERROR:
+ return "PLAYSTATE_ERROR";
+ default:
+ return "[invalid playstate]";
+ }
+ }
+ }
+
+ protected static class RemoteControlStackEntry implements DeathRecipient {
+ public int mRccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
+ final public MediaFocusControl mController;
+ /**
+ * The target for the ACTION_MEDIA_BUTTON events.
+ * Always non null.
+ */
+ final public PendingIntent mMediaIntent;
+ /**
+ * The registered media button event receiver.
+ * Always non null.
+ */
+ final public ComponentName mReceiverComponent;
+ public IBinder mToken;
+ public String mCallingPackageName;
+ public int mCallingUid;
+ /**
+ * Provides access to the information to display on the remote control.
+ * May be null (when a media button event receiver is registered,
+ * but no remote control client has been registered) */
+ public IRemoteControlClient mRcClient;
+ public RcClientDeathHandler mRcClientDeathHandler;
+ /**
+ * Information only used for non-local playback
+ */
+ public int mPlaybackType;
+ public int mPlaybackVolume;
+ public int mPlaybackVolumeMax;
+ public int mPlaybackVolumeHandling;
+ public int mPlaybackStream;
+ public RccPlaybackState mPlaybackState;
+ public IRemoteVolumeObserver mRemoteVolumeObs;
+
+ public void resetPlaybackInfo() {
+ mPlaybackType = RemoteControlClient.PLAYBACK_TYPE_LOCAL;
+ mPlaybackVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
+ mPlaybackVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
+ mPlaybackVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
+ mPlaybackStream = AudioManager.STREAM_MUSIC;
+ mPlaybackState.reset();
+ mRemoteVolumeObs = null;
+ }
+
+ /** precondition: mediaIntent != null */
+ public RemoteControlStackEntry(MediaFocusControl controller, PendingIntent mediaIntent,
+ ComponentName eventReceiver, IBinder token) {
+ mController = controller;
+ mMediaIntent = mediaIntent;
+ mReceiverComponent = eventReceiver;
+ mToken = token;
+ mCallingUid = -1;
+ mRcClient = null;
+ mRccId = ++sLastRccId;
+ mPlaybackState = new RccPlaybackState(
+ RemoteControlClient.PLAYSTATE_STOPPED,
+ RemoteControlClient.PLAYBACK_POSITION_INVALID,
+ RemoteControlClient.PLAYBACK_SPEED_1X);
+
+ resetPlaybackInfo();
+ if (mToken != null) {
+ try {
+ mToken.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ mController.mEventHandler.post(new Runnable() {
+ @Override public void run() {
+ mController.unregisterMediaButtonIntent(mMediaIntent);
+ }
+ });
+ }
+ }
+ }
+
+ public void unlinkToRcClientDeath() {
+ if ((mRcClientDeathHandler != null) && (mRcClientDeathHandler.mCb != null)) {
+ try {
+ mRcClientDeathHandler.mCb.unlinkToDeath(mRcClientDeathHandler, 0);
+ mRcClientDeathHandler = null;
+ } catch (java.util.NoSuchElementException e) {
+ // not much we can do here
+ Log.e(TAG, "Encountered " + e + " in unlinkToRcClientDeath()");
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void destroy() {
+ unlinkToRcClientDeath();
+ if (mToken != null) {
+ mToken.unlinkToDeath(this, 0);
+ mToken = null;
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ mController.unregisterMediaButtonIntent(mMediaIntent);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ destroy(); // unlink exception handled inside method
+ super.finalize();
+ }
+ }
+
+ /**
+ * The stack of remote control event receivers.
+ * Code sections and methods that modify the remote control event receiver stack are
+ * synchronized on mRCStack, but also BEFORE on mFocusLock as any change in either
+ * stack, audio focus or RC, can lead to a change in the remote control display
+ */
+ private final Stack<RemoteControlStackEntry> mRCStack = new Stack<RemoteControlStackEntry>();
+
+ /**
+ * The component the telephony package can register so telephony calls have priority to
+ * handle media button events
+ */
+ private ComponentName mMediaReceiverForCalls = null;
+
+ /**
+ * Helper function:
+ * Display in the log the current entries in the remote control focus stack
+ */
+ private void dumpRCStack(PrintWriter pw) {
+ pw.println("\nRemote Control stack entries (last is top of stack):");
+ synchronized(mRCStack) {
+ Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ while(stackIterator.hasNext()) {
+ RemoteControlStackEntry rcse = stackIterator.next();
+ pw.println(" pi: " + rcse.mMediaIntent +
+ " -- pack: " + rcse.mCallingPackageName +
+ " -- ercvr: " + rcse.mReceiverComponent +
+ " -- client: " + rcse.mRcClient +
+ " -- uid: " + rcse.mCallingUid +
+ " -- type: " + rcse.mPlaybackType +
+ " state: " + rcse.mPlaybackState);
+ }
+ }
+ }
+
+ /**
+ * Helper function:
+ * Display in the log the current entries in the remote control stack, focusing
+ * on RemoteControlClient data
+ */
+ private void dumpRCCStack(PrintWriter pw) {
+ pw.println("\nRemote Control Client stack entries (last is top of stack):");
+ synchronized(mRCStack) {
+ Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ while(stackIterator.hasNext()) {
+ RemoteControlStackEntry rcse = stackIterator.next();
+ pw.println(" uid: " + rcse.mCallingUid +
+ " -- id: " + rcse.mRccId +
+ " -- type: " + rcse.mPlaybackType +
+ " -- state: " + rcse.mPlaybackState +
+ " -- vol handling: " + rcse.mPlaybackVolumeHandling +
+ " -- vol: " + rcse.mPlaybackVolume +
+ " -- volMax: " + rcse.mPlaybackVolumeMax +
+ " -- volObs: " + rcse.mRemoteVolumeObs);
+ }
+ synchronized(mCurrentRcLock) {
+ pw.println("\nCurrent remote control generation ID = " + mCurrentRcClientGen);
+ }
+ }
+ synchronized (mMainRemote) {
+ pw.println("\nRemote Volume State:");
+ pw.println(" has remote: " + mHasRemotePlayback);
+ pw.println(" is remote active: " + mMainRemoteIsActive);
+ pw.println(" rccId: " + mMainRemote.mRccId);
+ pw.println(" volume handling: "
+ + ((mMainRemote.mVolumeHandling == RemoteControlClient.PLAYBACK_VOLUME_FIXED) ?
+ "PLAYBACK_VOLUME_FIXED(0)" : "PLAYBACK_VOLUME_VARIABLE(1)"));
+ pw.println(" volume: " + mMainRemote.mVolume);
+ pw.println(" volume steps: " + mMainRemote.mVolumeMax);
+ }
+ }
+
+ /**
+ * Helper function:
+ * Display in the log the current entries in the list of remote control displays
+ */
+ private void dumpRCDList(PrintWriter pw) {
+ pw.println("\nRemote Control Display list entries:");
+ synchronized(mRCStack) {
+ final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
+ while (displayIterator.hasNext()) {
+ final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ pw.println(" IRCD: " + di.mRcDisplay +
+ " -- w:" + di.mArtworkExpectedWidth +
+ " -- h:" + di.mArtworkExpectedHeight+
+ " -- wantsPosSync:" + di.mWantsPositionSync);
+ }
+ }
+ }
+
+ /**
+ * Helper function:
+ * Remove any entry in the remote control stack that has the same package name as packageName
+ * Pre-condition: packageName != null
+ */
+ private void cleanupMediaButtonReceiverForPackage(String packageName, boolean removeAll) {
+ synchronized(mRCStack) {
+ if (mRCStack.empty()) {
+ return;
+ } else {
+ final PackageManager pm = mContext.getPackageManager();
+ RemoteControlStackEntry oldTop = mRCStack.peek();
+ Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ // iterate over the stack entries
+ // (using an iterator on the stack so we can safely remove an entry after having
+ // evaluated it, traversal order doesn't matter here)
+ while(stackIterator.hasNext()) {
+ RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
+ if (removeAll && packageName.equals(rcse.mMediaIntent.getCreatorPackage())) {
+ // a stack entry is from the package being removed, remove it from the stack
+ stackIterator.remove();
+ rcse.destroy();
+ } else if (rcse.mReceiverComponent != null) {
+ try {
+ // Check to see if this receiver still exists.
+ pm.getReceiverInfo(rcse.mReceiverComponent, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Not found -- remove it!
+ stackIterator.remove();
+ rcse.destroy();
+ }
+ }
+ }
+ if (mRCStack.empty()) {
+ // no saved media button receiver
+ mEventHandler.sendMessage(
+ mEventHandler.obtainMessage(MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0,
+ null));
+ } else if (oldTop != mRCStack.peek()) {
+ // the top of the stack has changed, save it in the system settings
+ // by posting a message to persist it; only do this however if it has
+ // a concrete component name (is not a transient registration)
+ RemoteControlStackEntry rcse = mRCStack.peek();
+ if (rcse.mReceiverComponent != null) {
+ mEventHandler.sendMessage(
+ mEventHandler.obtainMessage(MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0,
+ rcse.mReceiverComponent));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper function:
+ * Restore remote control receiver from the system settings.
+ */
+ protected void restoreMediaButtonReceiver() {
+ String receiverName = Settings.System.getStringForUser(mContentResolver,
+ Settings.System.MEDIA_BUTTON_RECEIVER, UserHandle.USER_CURRENT);
+ if ((null != receiverName) && !receiverName.isEmpty()) {
+ ComponentName eventReceiver = ComponentName.unflattenFromString(receiverName);
+ if (eventReceiver == null) {
+ // an invalid name was persisted
+ return;
+ }
+ // construct a PendingIntent targeted to the restored component name
+ // for the media button and register it
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ // the associated intent will be handled by the component being registered
+ mediaButtonIntent.setComponent(eventReceiver);
+ PendingIntent pi = PendingIntent.getBroadcast(mContext,
+ 0/*requestCode, ignored*/, mediaButtonIntent, 0/*flags*/);
+ registerMediaButtonIntent(pi, eventReceiver, null);
+ }
+ }
+
+ /**
+ * Helper function:
+ * Set the new remote control receiver at the top of the RC focus stack.
+ * Called synchronized on mAudioFocusLock, then mRCStack
+ * precondition: mediaIntent != null
+ */
+ private void pushMediaButtonReceiver_syncAfRcs(PendingIntent mediaIntent, ComponentName target,
+ IBinder token) {
+ // already at top of stack?
+ if (!mRCStack.empty() && mRCStack.peek().mMediaIntent.equals(mediaIntent)) {
+ return;
+ }
+ if (mAppOps.noteOp(AppOpsManager.OP_TAKE_MEDIA_BUTTONS, Binder.getCallingUid(),
+ mediaIntent.getCreatorPackage()) != AppOpsManager.MODE_ALLOWED) {
+ return;
+ }
+ RemoteControlStackEntry rcse = null;
+ boolean wasInsideStack = false;
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ rcse = mRCStack.elementAt(index);
+ if(rcse.mMediaIntent.equals(mediaIntent)) {
+ // ok to remove element while traversing the stack since we're leaving the loop
+ mRCStack.removeElementAt(index);
+ wasInsideStack = true;
+ break;
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
+ }
+ if (!wasInsideStack) {
+ rcse = new RemoteControlStackEntry(this, mediaIntent, target, token);
+ }
+ mRCStack.push(rcse); // rcse is never null
+
+ // post message to persist the default media button receiver
+ if (target != null) {
+ mEventHandler.sendMessage( mEventHandler.obtainMessage(
+ MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0, target/*obj*/) );
+ }
+ }
+
+ /**
+ * Helper function:
+ * Remove the remote control receiver from the RC focus stack.
+ * Called synchronized on mAudioFocusLock, then mRCStack
+ * precondition: pi != null
+ */
+ private void removeMediaButtonReceiver_syncAfRcs(PendingIntent pi) {
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
+ if (rcse.mMediaIntent.equals(pi)) {
+ rcse.destroy();
+ // ok to remove element while traversing the stack since we're leaving the loop
+ mRCStack.removeElementAt(index);
+ break;
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
+ }
+ }
+
+ /**
+ * Helper function:
+ * Called synchronized on mRCStack
+ */
+ private boolean isCurrentRcController(PendingIntent pi) {
+ if (!mRCStack.empty() && mRCStack.peek().mMediaIntent.equals(pi)) {
+ return true;
+ }
+ return false;
+ }
+
+ private void onHandlePersistMediaButtonReceiver(ComponentName receiver) {
+ Settings.System.putStringForUser(mContentResolver,
+ Settings.System.MEDIA_BUTTON_RECEIVER,
+ receiver == null ? "" : receiver.flattenToString(),
+ UserHandle.USER_CURRENT);
+ }
+
+ //==========================================================================================
+ // Remote control display / client
+ //==========================================================================================
+ /**
+ * Update the remote control displays with the new "focused" client generation
+ */
+ private void setNewRcClientOnDisplays_syncRcsCurrc(int newClientGeneration,
+ PendingIntent newMediaIntent, boolean clearing) {
+ synchronized(mRCStack) {
+ if (mRcDisplays.size() > 0) {
+ final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
+ while (displayIterator.hasNext()) {
+ final DisplayInfoForServer di = displayIterator.next();
+ try {
+ di.mRcDisplay.setCurrentClientId(
+ newClientGeneration, newMediaIntent, clearing);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead display in setNewRcClientOnDisplays_syncRcsCurrc()",e);
+ di.release();
+ displayIterator.remove();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Update the remote control clients with the new "focused" client generation
+ */
+ private void setNewRcClientGenerationOnClients_syncRcsCurrc(int newClientGeneration) {
+ // (using an iterator on the stack so we can safely remove an entry if needed,
+ // traversal order doesn't matter here as we update all entries)
+ Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ while(stackIterator.hasNext()) {
+ RemoteControlStackEntry se = stackIterator.next();
+ if ((se != null) && (se.mRcClient != null)) {
+ try {
+ se.mRcClient.setCurrentClientGenerationId(newClientGeneration);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Dead client in setNewRcClientGenerationOnClients_syncRcsCurrc()",e);
+ stackIterator.remove();
+ se.unlinkToRcClientDeath();
+ }
+ }
+ }
+ }
+
+ /**
+ * Update the displays and clients with the new "focused" client generation and name
+ * @param newClientGeneration the new generation value matching a client update
+ * @param newMediaIntent the media button event receiver associated with the client.
+ * May be null, which implies there is no registered media button event receiver.
+ * @param clearing true if the new client generation value maps to a remote control update
+ * where the display should be cleared.
+ */
+ private void setNewRcClient_syncRcsCurrc(int newClientGeneration,
+ PendingIntent newMediaIntent, boolean clearing) {
+ // send the new valid client generation ID to all displays
+ setNewRcClientOnDisplays_syncRcsCurrc(newClientGeneration, newMediaIntent, clearing);
+ // send the new valid client generation ID to all clients
+ setNewRcClientGenerationOnClients_syncRcsCurrc(newClientGeneration);
+ }
+
+ /**
+ * Called when processing MSG_RCDISPLAY_CLEAR event
+ */
+ private void onRcDisplayClear() {
+ if (DEBUG_RC) Log.i(TAG, "Clear remote control display");
+
+ synchronized(mRCStack) {
+ synchronized(mCurrentRcLock) {
+ mCurrentRcClientGen++;
+ // synchronously update the displays and clients with the new client generation
+ setNewRcClient_syncRcsCurrc(mCurrentRcClientGen,
+ null /*newMediaIntent*/, true /*clearing*/);
+ }
+ }
+ }
+
+ /**
+ * Called when processing MSG_RCDISPLAY_UPDATE event
+ */
+ private void onRcDisplayUpdate(RemoteControlStackEntry rcse, int flags /* USED ?*/) {
+ synchronized(mRCStack) {
+ synchronized(mCurrentRcLock) {
+ if ((mCurrentRcClient != null) && (mCurrentRcClient.equals(rcse.mRcClient))) {
+ if (DEBUG_RC) Log.i(TAG, "Display/update remote control ");
+
+ mCurrentRcClientGen++;
+ // synchronously update the displays and clients with
+ // the new client generation
+ setNewRcClient_syncRcsCurrc(mCurrentRcClientGen,
+ rcse.mMediaIntent /*newMediaIntent*/,
+ false /*clearing*/);
+
+ // tell the current client that it needs to send info
+ try {
+ mCurrentRcClient.onInformationRequested(mCurrentRcClientGen, flags);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Current valid remote client is dead: "+e);
+ mCurrentRcClient = null;
+ }
+ } else {
+ // the remote control display owner has changed between the
+ // the message to update the display was sent, and the time it
+ // gets to be processed (now)
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Helper function:
+ * Called synchronized on mRCStack
+ */
+ private void clearRemoteControlDisplay_syncAfRcs() {
+ synchronized(mCurrentRcLock) {
+ mCurrentRcClient = null;
+ }
+ // will cause onRcDisplayClear() to be called in AudioService's handler thread
+ mEventHandler.sendMessage( mEventHandler.obtainMessage(MSG_RCDISPLAY_CLEAR) );
+ }
+
+ /**
+ * Helper function for code readability: only to be called from
+ * checkUpdateRemoteControlDisplay_syncAfRcs() which checks the preconditions for
+ * this method.
+ * Preconditions:
+ * - called synchronized mAudioFocusLock then on mRCStack
+ * - mRCStack.isEmpty() is false
+ */
+ private void updateRemoteControlDisplay_syncAfRcs(int infoChangedFlags) {
+ RemoteControlStackEntry rcse = mRCStack.peek();
+ int infoFlagsAboutToBeUsed = infoChangedFlags;
+ // this is where we enforce opt-in for information display on the remote controls
+ // with the new AudioManager.registerRemoteControlClient() API
+ if (rcse.mRcClient == null) {
+ //Log.w(TAG, "Can't update remote control display with null remote control client");
+ clearRemoteControlDisplay_syncAfRcs();
+ return;
+ }
+ synchronized(mCurrentRcLock) {
+ if (!rcse.mRcClient.equals(mCurrentRcClient)) {
+ // new RC client, assume every type of information shall be queried
+ infoFlagsAboutToBeUsed = RC_INFO_ALL;
+ }
+ mCurrentRcClient = rcse.mRcClient;
+ }
+ // will cause onRcDisplayUpdate() to be called in AudioService's handler thread
+ mEventHandler.sendMessage( mEventHandler.obtainMessage(MSG_RCDISPLAY_UPDATE,
+ infoFlagsAboutToBeUsed /* arg1 */, 0, rcse /* obj, != null */) );
+ }
+
+ /**
+ * Helper function:
+ * Called synchronized on mAudioFocusLock, then mRCStack
+ * Check whether the remote control display should be updated, triggers the update if required
+ * @param infoChangedFlags the flags corresponding to the remote control client information
+ * that has changed, if applicable (checking for the update conditions might trigger a
+ * clear, rather than an update event).
+ */
+ private void checkUpdateRemoteControlDisplay_syncAfRcs(int infoChangedFlags) {
+ // determine whether the remote control display should be refreshed
+ // if either stack is empty, there is a mismatch, so clear the RC display
+ if (mRCStack.isEmpty() || mFocusStack.isEmpty()) {
+ clearRemoteControlDisplay_syncAfRcs();
+ return;
+ }
+
+ // determine which entry in the AudioFocus stack to consider, and compare against the
+ // top of the stack for the media button event receivers : simply using the top of the
+ // stack would make the entry disappear from the RemoteControlDisplay in conditions such as
+ // notifications playing during music playback.
+ // Crawl the AudioFocus stack from the top until an entry is found with the following
+ // characteristics:
+ // - focus gain on STREAM_MUSIC stream
+ // - non-transient focus gain on a stream other than music
+ FocusStackEntry af = null;
+ try {
+ for (int index = mFocusStack.size()-1; index >= 0; index--) {
+ FocusStackEntry fse = mFocusStack.elementAt(index);
+ if ((fse.mStreamType == AudioManager.STREAM_MUSIC)
+ || (fse.mFocusChangeType == AudioManager.AUDIOFOCUS_GAIN)) {
+ af = fse;
+ break;
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ Log.e(TAG, "Wrong index accessing audio focus stack when updating RCD: " + e);
+ af = null;
+ }
+ if (af == null) {
+ clearRemoteControlDisplay_syncAfRcs();
+ return;
+ }
+
+ // if the audio focus and RC owners belong to different packages, there is a mismatch, clear
+ if ((mRCStack.peek().mCallingPackageName != null)
+ && (af.mPackageName != null)
+ && !(mRCStack.peek().mCallingPackageName.compareTo(
+ af.mPackageName) == 0)) {
+ clearRemoteControlDisplay_syncAfRcs();
+ return;
+ }
+ // if the audio focus didn't originate from the same Uid as the one in which the remote
+ // control information will be retrieved, clear
+ if (mRCStack.peek().mCallingUid != af.mCallingUid) {
+ clearRemoteControlDisplay_syncAfRcs();
+ return;
+ }
+
+ // refresh conditions were verified: update the remote controls
+ // ok to call: synchronized mAudioFocusLock then on mRCStack, mRCStack is not empty
+ updateRemoteControlDisplay_syncAfRcs(infoChangedFlags);
+ }
+
+ /**
+ * Helper function:
+ * Post a message to asynchronously move the media button event receiver associated with the
+ * given remote control client ID to the top of the remote control stack
+ * @param rccId
+ */
+ private void postPromoteRcc(int rccId) {
+ sendMsg(mEventHandler, MSG_PROMOTE_RCC, SENDMSG_REPLACE,
+ rccId /*arg1*/, 0, null, 0/*delay*/);
+ }
+
+ private void onPromoteRcc(int rccId) {
+ if (DEBUG_RC) { Log.d(TAG, "Promoting RCC " + rccId); }
+ synchronized(mAudioFocusLock) {
+ synchronized(mRCStack) {
+ // ignore if given RCC ID is already at top of remote control stack
+ if (!mRCStack.isEmpty() && (mRCStack.peek().mRccId == rccId)) {
+ return;
+ }
+ int indexToPromote = -1;
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
+ if (rcse.mRccId == rccId) {
+ indexToPromote = index;
+ break;
+ }
+ }
+ if (indexToPromote >= 0) {
+ if (DEBUG_RC) { Log.d(TAG, " moving RCC from index " + indexToPromote
+ + " to " + (mRCStack.size()-1)); }
+ final RemoteControlStackEntry rcse = mRCStack.remove(indexToPromote);
+ mRCStack.push(rcse);
+ // the RC stack changed, reevaluate the display
+ checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index accessing RC stack, lock error? ", e);
+ }
+ }//synchronized(mRCStack)
+ }//synchronized(mAudioFocusLock)
+ }
+
+ /**
+ * see AudioManager.registerMediaButtonIntent(PendingIntent pi, ComponentName c)
+ * precondition: mediaIntent != null
+ */
+ protected void registerMediaButtonIntent(PendingIntent mediaIntent, ComponentName eventReceiver,
+ IBinder token) {
+ Log.i(TAG, " Remote Control registerMediaButtonIntent() for " + mediaIntent);
+
+ synchronized(mAudioFocusLock) {
+ synchronized(mRCStack) {
+ pushMediaButtonReceiver_syncAfRcs(mediaIntent, eventReceiver, token);
+ // new RC client, assume every type of information shall be queried
+ checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
+ }
+ }
+ }
+
+ /**
+ * see AudioManager.unregisterMediaButtonIntent(PendingIntent mediaIntent)
+ * precondition: mediaIntent != null, eventReceiver != null
+ */
+ protected void unregisterMediaButtonIntent(PendingIntent mediaIntent)
+ {
+ Log.i(TAG, " Remote Control unregisterMediaButtonIntent() for " + mediaIntent);
+
+ synchronized(mAudioFocusLock) {
+ synchronized(mRCStack) {
+ boolean topOfStackWillChange = isCurrentRcController(mediaIntent);
+ removeMediaButtonReceiver_syncAfRcs(mediaIntent);
+ if (topOfStackWillChange) {
+ // current RC client will change, assume every type of info needs to be queried
+ checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
+ }
+ }
+ }
+ }
+
+ /**
+ * see AudioManager.registerMediaButtonEventReceiverForCalls(ComponentName c)
+ * precondition: c != null
+ */
+ protected void registerMediaButtonEventReceiverForCalls(ComponentName c) {
+ if (mContext.checkCallingPermission("android.permission.MODIFY_PHONE_STATE")
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "Invalid permissions to register media button receiver for calls");
+ return;
+ }
+ synchronized(mRCStack) {
+ mMediaReceiverForCalls = c;
+ }
+ }
+
+ /**
+ * see AudioManager.unregisterMediaButtonEventReceiverForCalls()
+ */
+ protected void unregisterMediaButtonEventReceiverForCalls() {
+ if (mContext.checkCallingPermission("android.permission.MODIFY_PHONE_STATE")
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "Invalid permissions to unregister media button receiver for calls");
+ return;
+ }
+ synchronized(mRCStack) {
+ mMediaReceiverForCalls = null;
+ }
+ }
+
+ /**
+ * see AudioManager.registerRemoteControlClient(ComponentName eventReceiver, ...)
+ * @return the unique ID of the RemoteControlStackEntry associated with the RemoteControlClient
+ * Note: using this method with rcClient == null is a way to "disable" the IRemoteControlClient
+ * without modifying the RC stack, but while still causing the display to refresh (will
+ * become blank as a result of this)
+ */
+ protected int registerRemoteControlClient(PendingIntent mediaIntent,
+ IRemoteControlClient rcClient, String callingPackageName) {
+ if (DEBUG_RC) Log.i(TAG, "Register remote control client rcClient="+rcClient);
+ int rccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
+ synchronized(mAudioFocusLock) {
+ synchronized(mRCStack) {
+ // store the new display information
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
+ if(rcse.mMediaIntent.equals(mediaIntent)) {
+ // already had a remote control client?
+ if (rcse.mRcClientDeathHandler != null) {
+ // stop monitoring the old client's death
+ rcse.unlinkToRcClientDeath();
+ }
+ // save the new remote control client
+ rcse.mRcClient = rcClient;
+ rcse.mCallingPackageName = callingPackageName;
+ rcse.mCallingUid = Binder.getCallingUid();
+ if (rcClient == null) {
+ // here rcse.mRcClientDeathHandler is null;
+ rcse.resetPlaybackInfo();
+ break;
+ }
+ rccId = rcse.mRccId;
+
+ // there is a new (non-null) client:
+ // 1/ give the new client the displays (if any)
+ if (mRcDisplays.size() > 0) {
+ plugRemoteControlDisplaysIntoClient_syncRcStack(rcse.mRcClient);
+ }
+ // 2/ monitor the new client's death
+ IBinder b = rcse.mRcClient.asBinder();
+ RcClientDeathHandler rcdh =
+ new RcClientDeathHandler(b, rcse.mMediaIntent);
+ try {
+ b.linkToDeath(rcdh, 0);
+ } catch (RemoteException e) {
+ // remote control client is DOA, disqualify it
+ Log.w(TAG, "registerRemoteControlClient() has a dead client " + b);
+ rcse.mRcClient = null;
+ }
+ rcse.mRcClientDeathHandler = rcdh;
+ break;
+ }
+ }//for
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index accessing RC stack, lock error? ", e);
+ }
+
+ // if the eventReceiver is at the top of the stack
+ // then check for potential refresh of the remote controls
+ if (isCurrentRcController(mediaIntent)) {
+ checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
+ }
+ }//synchronized(mRCStack)
+ }//synchronized(mAudioFocusLock)
+ return rccId;
+ }
+
+ /**
+ * see AudioManager.unregisterRemoteControlClient(PendingIntent pi, ...)
+ * rcClient is guaranteed non-null
+ */
+ protected void unregisterRemoteControlClient(PendingIntent mediaIntent,
+ IRemoteControlClient rcClient) {
+ if (DEBUG_RC) Log.i(TAG, "Unregister remote control client rcClient="+rcClient);
+ synchronized(mAudioFocusLock) {
+ synchronized(mRCStack) {
+ boolean topRccChange = false;
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
+ if ((rcse.mMediaIntent.equals(mediaIntent))
+ && rcClient.equals(rcse.mRcClient)) {
+ // we found the IRemoteControlClient to unregister
+ // stop monitoring its death
+ rcse.unlinkToRcClientDeath();
+ // reset the client-related fields
+ rcse.mRcClient = null;
+ rcse.mCallingPackageName = null;
+ topRccChange = (index == mRCStack.size()-1);
+ // there can only be one matching RCC in the RC stack, we're done
+ break;
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index accessing RC stack, lock error? ", e);
+ }
+ if (topRccChange) {
+ // no more RCC for the RCD, check for potential refresh of the remote controls
+ checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * A class to encapsulate all the information about a remote control display.
+ * After instanciation, init() must always be called before the object is added in the list
+ * of displays.
+ * Before being removed from the list of displays, release() must always be called (otherwise
+ * it will leak death handlers).
+ */
+ private class DisplayInfoForServer implements IBinder.DeathRecipient {
+ /** may never be null */
+ private IRemoteControlDisplay mRcDisplay;
+ private IBinder mRcDisplayBinder;
+ private int mArtworkExpectedWidth = -1;
+ private int mArtworkExpectedHeight = -1;
+ private boolean mWantsPositionSync = false;
+
+ public DisplayInfoForServer(IRemoteControlDisplay rcd, int w, int h) {
+ if (DEBUG_RC) Log.i(TAG, "new DisplayInfoForServer for " + rcd + " w=" + w + " h=" + h);
+ mRcDisplay = rcd;
+ mRcDisplayBinder = rcd.asBinder();
+ mArtworkExpectedWidth = w;
+ mArtworkExpectedHeight = h;
+ }
+
+ public boolean init() {
+ try {
+ mRcDisplayBinder.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ // remote control display is DOA, disqualify it
+ Log.w(TAG, "registerRemoteControlDisplay() has a dead client " + mRcDisplayBinder);
+ return false;
+ }
+ return true;
+ }
+
+ public void release() {
+ try {
+ mRcDisplayBinder.unlinkToDeath(this, 0);
+ } catch (java.util.NoSuchElementException e) {
+ // not much we can do here, the display should have been unregistered anyway
+ Log.e(TAG, "Error in DisplaInfoForServer.relase()", e);
+ }
+ }
+
+ public void binderDied() {
+ synchronized(mRCStack) {
+ Log.w(TAG, "RemoteControl: display " + mRcDisplay + " died");
+ // remove the display from the list
+ final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
+ while (displayIterator.hasNext()) {
+ final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ if (di.mRcDisplay == mRcDisplay) {
+ if (DEBUG_RC) Log.w(TAG, " RCD removed from list");
+ displayIterator.remove();
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * The remote control displays.
+ * Access synchronized on mRCStack
+ */
+ private ArrayList<DisplayInfoForServer> mRcDisplays = new ArrayList<DisplayInfoForServer>(1);
+
+ /**
+ * Plug each registered display into the specified client
+ * @param rcc, guaranteed non null
+ */
+ private void plugRemoteControlDisplaysIntoClient_syncRcStack(IRemoteControlClient rcc) {
+ final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
+ while (displayIterator.hasNext()) {
+ final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ try {
+ rcc.plugRemoteControlDisplay(di.mRcDisplay, di.mArtworkExpectedWidth,
+ di.mArtworkExpectedHeight);
+ if (di.mWantsPositionSync) {
+ rcc.setWantsSyncForDisplay(di.mRcDisplay, true);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error connecting RCD to RCC in RCC registration",e);
+ }
+ }
+ }
+
+ /**
+ * Is the remote control display interface already registered
+ * @param rcd
+ * @return true if the IRemoteControlDisplay is already in the list of displays
+ */
+ private boolean rcDisplayIsPluggedIn_syncRcStack(IRemoteControlDisplay rcd) {
+ final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
+ while (displayIterator.hasNext()) {
+ final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Register an IRemoteControlDisplay.
+ * 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.
+ * @see android.media.IAudioService#registerRemoteControlDisplay(android.media.IRemoteControlDisplay, int, int)
+ * @param rcd the IRemoteControlDisplay to register. No effect if null.
+ * @param w the maximum width of the expected bitmap. Negative or zero values indicate this
+ * 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.
+ */
+ protected void registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) {
+ if (DEBUG_RC) Log.d(TAG, ">>> registerRemoteControlDisplay("+rcd+")");
+ synchronized(mAudioFocusLock) {
+ synchronized(mRCStack) {
+ if ((rcd == null) || rcDisplayIsPluggedIn_syncRcStack(rcd)) {
+ return;
+ }
+ DisplayInfoForServer di = new DisplayInfoForServer(rcd, w, h);
+ if (!di.init()) {
+ if (DEBUG_RC) Log.e(TAG, " error registering RCD");
+ return;
+ }
+ // add RCD to list of displays
+ mRcDisplays.add(di);
+
+ // let all the remote control clients know there is a new display (so the remote
+ // control stack traversal order doesn't matter).
+ Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ while(stackIterator.hasNext()) {
+ RemoteControlStackEntry rcse = stackIterator.next();
+ if(rcse.mRcClient != null) {
+ try {
+ rcse.mRcClient.plugRemoteControlDisplay(rcd, w, h);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error connecting RCD to client: ", e);
+ }
+ }
+ }
+
+ // we have a new display, of which all the clients are now aware: have it be updated
+ checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
+ }
+ }
+ }
+
+ /**
+ * Unregister an IRemoteControlDisplay.
+ * No effect if the IRemoteControlDisplay hasn't been successfully registered.
+ * @see android.media.IAudioService#unregisterRemoteControlDisplay(android.media.IRemoteControlDisplay)
+ * @param rcd the IRemoteControlDisplay to unregister. No effect if null.
+ */
+ protected void unregisterRemoteControlDisplay(IRemoteControlDisplay rcd) {
+ if (DEBUG_RC) Log.d(TAG, "<<< unregisterRemoteControlDisplay("+rcd+")");
+ synchronized(mRCStack) {
+ if (rcd == null) {
+ return;
+ }
+
+ boolean displayWasPluggedIn = false;
+ final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
+ while (displayIterator.hasNext() && !displayWasPluggedIn) {
+ final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
+ displayWasPluggedIn = true;
+ di.release();
+ displayIterator.remove();
+ }
+ }
+
+ if (displayWasPluggedIn) {
+ // disconnect this remote control display from all the clients, so the remote
+ // control stack traversal order doesn't matter
+ final Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ while(stackIterator.hasNext()) {
+ final RemoteControlStackEntry rcse = stackIterator.next();
+ if(rcse.mRcClient != null) {
+ try {
+ rcse.mRcClient.unplugRemoteControlDisplay(rcd);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error disconnecting remote control display to client: ", e);
+ }
+ }
+ }
+ } else {
+ if (DEBUG_RC) Log.w(TAG, " trying to unregister unregistered RCD");
+ }
+ }
+ }
+
+ /**
+ * Update the size of the artwork used by an IRemoteControlDisplay.
+ * @see android.media.IAudioService#remoteControlDisplayUsesBitmapSize(android.media.IRemoteControlDisplay, int, int)
+ * @param rcd the IRemoteControlDisplay with the new artwork size requirement
+ * @param w the maximum width of the expected bitmap. Negative or zero values indicate this
+ * 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.
+ */
+ protected void remoteControlDisplayUsesBitmapSize(IRemoteControlDisplay rcd, int w, int h) {
+ synchronized(mRCStack) {
+ final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
+ boolean artworkSizeUpdate = false;
+ while (displayIterator.hasNext() && !artworkSizeUpdate) {
+ final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
+ if ((di.mArtworkExpectedWidth != w) || (di.mArtworkExpectedHeight != h)) {
+ di.mArtworkExpectedWidth = w;
+ di.mArtworkExpectedHeight = h;
+ artworkSizeUpdate = true;
+ }
+ }
+ }
+ if (artworkSizeUpdate) {
+ // RCD is currently plugged in and its artwork size has changed, notify all RCCs,
+ // stack traversal order doesn't matter
+ final Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ while(stackIterator.hasNext()) {
+ final RemoteControlStackEntry rcse = stackIterator.next();
+ if(rcse.mRcClient != null) {
+ try {
+ rcse.mRcClient.setBitmapSizeForDisplay(rcd, w, h);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error setting bitmap size for RCD on RCC: ", e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Controls whether a remote control display needs periodic checks of the RemoteControlClient
+ * playback position to verify that the estimated position has not drifted from the actual
+ * position. By default the check is not performed.
+ * The IRemoteControlDisplay must have been previously registered for this to have any effect.
+ * @param rcd the IRemoteControlDisplay for which the anti-drift mechanism will be enabled
+ * or disabled. Not null.
+ * @param wantsSync if true, RemoteControlClient instances which expose their playback position
+ * to the framework will regularly compare the estimated playback position with the actual
+ * position, and will update the IRemoteControlDisplay implementation whenever a drift is
+ * detected.
+ */
+ protected void remoteControlDisplayWantsPlaybackPositionSync(IRemoteControlDisplay rcd,
+ boolean wantsSync) {
+ synchronized(mRCStack) {
+ boolean rcdRegistered = false;
+ // store the information about this display
+ // (display stack traversal order doesn't matter).
+ final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
+ while (displayIterator.hasNext()) {
+ final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
+ di.mWantsPositionSync = wantsSync;
+ rcdRegistered = true;
+ break;
+ }
+ }
+ if (!rcdRegistered) {
+ return;
+ }
+ // notify all current RemoteControlClients
+ // (stack traversal order doesn't matter as we notify all RCCs)
+ final Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ while (stackIterator.hasNext()) {
+ final RemoteControlStackEntry rcse = stackIterator.next();
+ if (rcse.mRcClient != null) {
+ try {
+ rcse.mRcClient.setWantsSyncForDisplay(rcd, wantsSync);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error setting position sync flag for RCD on RCC: ", e);
+ }
+ }
+ }
+ }
+ }
+
+ protected void setRemoteControlClientPlaybackPosition(int generationId, long timeMs) {
+ // ignore position change requests if invalid generation ID
+ synchronized(mRCStack) {
+ synchronized(mCurrentRcLock) {
+ if (mCurrentRcClientGen != generationId) {
+ return;
+ }
+ }
+ }
+ // discard any unprocessed seek request in the message queue, and replace with latest
+ sendMsg(mEventHandler, MSG_RCC_SEEK_REQUEST, SENDMSG_REPLACE, generationId /* arg1 */,
+ 0 /* arg2 ignored*/, new Long(timeMs) /* obj */, 0 /* delay */);
+ }
+
+ private void onSetRemoteControlClientPlaybackPosition(int generationId, long timeMs) {
+ if(DEBUG_RC) Log.d(TAG, "onSetRemoteControlClientPlaybackPosition(genId=" + generationId +
+ ", timeMs=" + timeMs + ")");
+ synchronized(mRCStack) {
+ synchronized(mCurrentRcLock) {
+ if ((mCurrentRcClient != null) && (mCurrentRcClientGen == generationId)) {
+ // tell the current client to seek to the requested location
+ try {
+ mCurrentRcClient.seekTo(generationId, timeMs);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Current valid remote client is dead: "+e);
+ mCurrentRcClient = null;
+ }
+ }
+ }
+ }
+ }
+
+ protected void setPlaybackInfoForRcc(int rccId, int what, int value) {
+ sendMsg(mEventHandler, MSG_RCC_NEW_PLAYBACK_INFO, SENDMSG_QUEUE,
+ rccId /* arg1 */, what /* arg2 */, Integer.valueOf(value) /* obj */, 0 /* delay */);
+ }
+
+ // handler for MSG_RCC_NEW_PLAYBACK_INFO
+ private void onNewPlaybackInfoForRcc(int rccId, int key, int value) {
+ if(DEBUG_RC) Log.d(TAG, "onNewPlaybackInfoForRcc(id=" + rccId +
+ ", what=" + key + ",val=" + value + ")");
+ synchronized(mRCStack) {
+ // iterating from top of stack as playback information changes are more likely
+ // on entries at the top of the remote control stack
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
+ if (rcse.mRccId == rccId) {
+ switch (key) {
+ case RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE:
+ rcse.mPlaybackType = value;
+ postReevaluateRemote();
+ break;
+ case RemoteControlClient.PLAYBACKINFO_VOLUME:
+ rcse.mPlaybackVolume = value;
+ synchronized (mMainRemote) {
+ if (rccId == mMainRemote.mRccId) {
+ mMainRemote.mVolume = value;
+ mVolumeController.postHasNewRemotePlaybackInfo();
+ }
+ }
+ break;
+ case RemoteControlClient.PLAYBACKINFO_VOLUME_MAX:
+ rcse.mPlaybackVolumeMax = value;
+ synchronized (mMainRemote) {
+ if (rccId == mMainRemote.mRccId) {
+ mMainRemote.mVolumeMax = value;
+ mVolumeController.postHasNewRemotePlaybackInfo();
+ }
+ }
+ break;
+ case RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING:
+ rcse.mPlaybackVolumeHandling = value;
+ synchronized (mMainRemote) {
+ if (rccId == mMainRemote.mRccId) {
+ mMainRemote.mVolumeHandling = value;
+ mVolumeController.postHasNewRemotePlaybackInfo();
+ }
+ }
+ break;
+ case RemoteControlClient.PLAYBACKINFO_USES_STREAM:
+ rcse.mPlaybackStream = value;
+ break;
+ default:
+ Log.e(TAG, "unhandled key " + key + " for RCC " + rccId);
+ break;
+ }
+ return;
+ }
+ }//for
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index mRCStack on onNewPlaybackInfoForRcc, lock error? ", e);
+ }
+ }
+ }
+
+ protected void setPlaybackStateForRcc(int rccId, int state, long timeMs, float speed) {
+ sendMsg(mEventHandler, MSG_RCC_NEW_PLAYBACK_STATE, SENDMSG_QUEUE,
+ rccId /* arg1 */, state /* arg2 */,
+ new RccPlaybackState(state, timeMs, speed) /* obj */, 0 /* delay */);
+ }
+
+ protected void onNewPlaybackStateForRcc(int rccId, int state, RccPlaybackState newState) {
+ if(DEBUG_RC) Log.d(TAG, "onNewPlaybackStateForRcc(id=" + rccId + ", state=" + state
+ + ", time=" + newState.mPositionMs + ", speed=" + newState.mSpeed + ")");
+ synchronized(mRCStack) {
+ // iterating from top of stack as playback information changes are more likely
+ // on entries at the top of the remote control stack
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
+ if (rcse.mRccId == rccId) {
+ rcse.mPlaybackState = newState;
+ synchronized (mMainRemote) {
+ if (rccId == mMainRemote.mRccId) {
+ mMainRemoteIsActive = isPlaystateActive(state);
+ postReevaluateRemote();
+ }
+ }
+ // an RCC moving to a "playing" state should become the media button
+ // event receiver so it can be controlled, without requiring the
+ // app to re-register its receiver
+ if (isPlaystateActive(state)) {
+ postPromoteRcc(rccId);
+ }
+ }
+ }//for
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index on mRCStack in onNewPlaybackStateForRcc, lock error? ", e);
+ }
+ }
+ }
+
+ protected void registerRemoteVolumeObserverForRcc(int rccId, IRemoteVolumeObserver rvo) {
+ sendMsg(mEventHandler, MSG_RCC_NEW_VOLUME_OBS, SENDMSG_QUEUE,
+ rccId /* arg1 */, 0, rvo /* obj */, 0 /* delay */);
+ }
+
+ // handler for MSG_RCC_NEW_VOLUME_OBS
+ private void onRegisterVolumeObserverForRcc(int rccId, IRemoteVolumeObserver rvo) {
+ synchronized(mRCStack) {
+ // The stack traversal order doesn't matter because there is only one stack entry
+ // with this RCC ID, but the matching ID is more likely at the top of the stack, so
+ // start iterating from the top.
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
+ if (rcse.mRccId == rccId) {
+ rcse.mRemoteVolumeObs = rvo;
+ break;
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
+ }
+ }
+ }
+
+ /**
+ * Checks if a remote client is active on the supplied stream type. Update the remote stream
+ * volume state if found and playing
+ * @param streamType
+ * @return false if no remote playing is currently playing
+ */
+ protected boolean checkUpdateRemoteStateIfActive(int streamType) {
+ synchronized(mRCStack) {
+ // iterating from top of stack as active playback is more likely on entries at the top
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
+ if ((rcse.mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE)
+ && isPlaystateActive(rcse.mPlaybackState.mState)
+ && (rcse.mPlaybackStream == streamType)) {
+ if (DEBUG_RC) Log.d(TAG, "remote playback active on stream " + streamType
+ + ", vol =" + rcse.mPlaybackVolume);
+ synchronized (mMainRemote) {
+ mMainRemote.mRccId = rcse.mRccId;
+ mMainRemote.mVolume = rcse.mPlaybackVolume;
+ mMainRemote.mVolumeMax = rcse.mPlaybackVolumeMax;
+ mMainRemote.mVolumeHandling = rcse.mPlaybackVolumeHandling;
+ mMainRemoteIsActive = true;
+ }
+ return true;
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index accessing RC stack, lock error? ", e);
+ }
+ }
+ synchronized (mMainRemote) {
+ mMainRemoteIsActive = false;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the given playback state is considered "active", i.e. it describes a state
+ * where playback is happening, or about to
+ * @param playState the playback state to evaluate
+ * @return true if active, false otherwise (inactive or unknown)
+ */
+ private static boolean isPlaystateActive(int playState) {
+ switch (playState) {
+ case RemoteControlClient.PLAYSTATE_PLAYING:
+ case RemoteControlClient.PLAYSTATE_BUFFERING:
+ case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
+ case RemoteControlClient.PLAYSTATE_REWINDING:
+ case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
+ case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ protected void adjustRemoteVolume(int streamType, int direction, int flags) {
+ int rccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
+ boolean volFixed = false;
+ synchronized (mMainRemote) {
+ if (!mMainRemoteIsActive) {
+ if (DEBUG_VOL) Log.w(TAG, "adjustRemoteVolume didn't find an active client");
+ return;
+ }
+ rccId = mMainRemote.mRccId;
+ volFixed = (mMainRemote.mVolumeHandling ==
+ RemoteControlClient.PLAYBACK_VOLUME_FIXED);
+ }
+ // unlike "local" stream volumes, we can't compute the new volume based on the direction,
+ // we can only notify the remote that volume needs to be updated, and we'll get an async'
+ // update through setPlaybackInfoForRcc()
+ if (!volFixed) {
+ sendVolumeUpdateToRemote(rccId, direction);
+ }
+
+ // fire up the UI
+ mVolumeController.postRemoteVolumeChanged(streamType, flags);
+ }
+
+ private void sendVolumeUpdateToRemote(int rccId, int direction) {
+ if (DEBUG_VOL) { Log.d(TAG, "sendVolumeUpdateToRemote(rccId="+rccId+" , dir="+direction); }
+ if (direction == 0) {
+ // only handling discrete events
+ return;
+ }
+ IRemoteVolumeObserver rvo = null;
+ synchronized (mRCStack) {
+ // The stack traversal order doesn't matter because there is only one stack entry
+ // with this RCC ID, but the matching ID is more likely at the top of the stack, so
+ // start iterating from the top.
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
+ //FIXME OPTIMIZE store this info in mMainRemote so we don't have to iterate?
+ if (rcse.mRccId == rccId) {
+ rvo = rcse.mRemoteVolumeObs;
+ break;
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
+ }
+ }
+ if (rvo != null) {
+ try {
+ rvo.dispatchRemoteVolumeUpdate(direction, -1);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error dispatching relative volume update", e);
+ }
+ }
+ }
+
+ protected int getRemoteStreamMaxVolume() {
+ synchronized (mMainRemote) {
+ if (mMainRemote.mRccId == RemoteControlClient.RCSE_ID_UNREGISTERED) {
+ return 0;
+ }
+ return mMainRemote.mVolumeMax;
+ }
+ }
+
+ protected int getRemoteStreamVolume() {
+ synchronized (mMainRemote) {
+ if (mMainRemote.mRccId == RemoteControlClient.RCSE_ID_UNREGISTERED) {
+ return 0;
+ }
+ return mMainRemote.mVolume;
+ }
+ }
+
+ protected void setRemoteStreamVolume(int vol) {
+ if (DEBUG_VOL) { Log.d(TAG, "setRemoteStreamVolume(vol="+vol+")"); }
+ int rccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
+ synchronized (mMainRemote) {
+ if (mMainRemote.mRccId == RemoteControlClient.RCSE_ID_UNREGISTERED) {
+ return;
+ }
+ rccId = mMainRemote.mRccId;
+ }
+ IRemoteVolumeObserver rvo = null;
+ synchronized (mRCStack) {
+ // The stack traversal order doesn't matter because there is only one stack entry
+ // with this RCC ID, but the matching ID is more likely at the top of the stack, so
+ // start iterating from the top.
+ try {
+ for (int index = mRCStack.size()-1; index >= 0; index--) {
+ final RemoteControlStackEntry rcse = mRCStack.elementAt(index);
+ //FIXME OPTIMIZE store this info in mMainRemote so we don't have to iterate?
+ if (rcse.mRccId == rccId) {
+ rvo = rcse.mRemoteVolumeObs;
+ break;
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // not expected to happen, indicates improper concurrent modification
+ Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
+ }
+ }
+ if (rvo != null) {
+ try {
+ rvo.dispatchRemoteVolumeUpdate(0, vol);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error dispatching absolute volume update", e);
+ }
+ }
+ }
+
+ /**
+ * Call to make AudioService reevaluate whether it's in a mode where remote players should
+ * have their volume controlled. In this implementation this is only to reset whether
+ * VolumePanel should display remote volumes
+ */
+ private void postReevaluateRemote() {
+ sendMsg(mEventHandler, MSG_REEVALUATE_REMOTE, SENDMSG_QUEUE, 0, 0, null, 0);
+ }
+
+ private void onReevaluateRemote() {
+ if (DEBUG_VOL) { Log.w(TAG, "onReevaluateRemote()"); }
+ // is there a registered RemoteControlClient that is handling remote playback
+ boolean hasRemotePlayback = false;
+ synchronized (mRCStack) {
+ // iteration stops when PLAYBACK_TYPE_REMOTE is found, so remote control stack
+ // traversal order doesn't matter
+ Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ while(stackIterator.hasNext()) {
+ RemoteControlStackEntry rcse = stackIterator.next();
+ if (rcse.mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE) {
+ hasRemotePlayback = true;
+ break;
+ }
+ }
+ }
+ synchronized (mMainRemote) {
+ if (mHasRemotePlayback != hasRemotePlayback) {
+ mHasRemotePlayback = hasRemotePlayback;
+ mVolumeController.postRemoteSliderVisibility(hasRemotePlayback);
+ }
+ }
+ }
+
+}