Registration of a RemoteController may succeed only if: - the caller has the MEDIA_CONTENT_CONTROL permission, - or if the RemoteController.OnClientUpdateListener it registers if one of the enabled notification listeners. For using the "enabled notification listener" functionality, the CL involved: - making OnClientUpdateListener an interface so a 3rd-party application may have its implementation extend NotificationListenerService, which is required for a listener to be enabled by the user. - add the concept of "enabled" status in an IRemoteControlDisplay, so a RemoteController (which encapsulates the IRemoteControlDisplay implementation) may be registered, but later temporarily disabled by the user, as a result of a user action in the security settings, or a user switch. - making MediaFocusControl, the component tied to AudioService, monitor changes in enabled notification listeners, and act upon enable/disable changes. Bug 8209392 Change-Id: Ia8dfa2156c65668b2b0d4ae92048005912652d84
2718 lines
119 KiB
Java
2718 lines
119 KiB
Java
/*
|
|
* 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.ActivityManager;
|
|
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.database.ContentObserver;
|
|
import android.net.Uri;
|
|
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.util.Slog;
|
|
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;
|
|
private final NotificationListenerObserver mNotifListenerObserver;
|
|
|
|
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);
|
|
mNotifListenerObserver = new NotificationListenerObserver();
|
|
|
|
mHasRemotePlayback = false;
|
|
mMainRemoteIsActive = false;
|
|
postReevaluateRemote();
|
|
}
|
|
|
|
protected void dump(PrintWriter pw) {
|
|
dumpFocusStack(pw);
|
|
dumpRCStack(pw);
|
|
dumpRCCStack(pw);
|
|
dumpRCDList(pw);
|
|
}
|
|
|
|
//==========================================================================================
|
|
// Management of RemoteControlDisplay registration permissions
|
|
//==========================================================================================
|
|
private final static Uri ENABLED_NOTIFICATION_LISTENERS_URI =
|
|
Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
|
|
|
|
private class NotificationListenerObserver extends ContentObserver {
|
|
|
|
NotificationListenerObserver() {
|
|
super(mEventHandler);
|
|
mContentResolver.registerContentObserver(Settings.Secure.getUriFor(
|
|
Settings.Secure.ENABLED_NOTIFICATION_LISTENERS), false, this);
|
|
}
|
|
|
|
@Override
|
|
public void onChange(boolean selfChange, Uri uri) {
|
|
if (!ENABLED_NOTIFICATION_LISTENERS_URI.equals(uri) || selfChange) {
|
|
return;
|
|
}
|
|
if (DEBUG_RC) { Log.d(TAG, "NotificationListenerObserver.onChange()"); }
|
|
postReevaluateRemoteControlDisplays();
|
|
}
|
|
}
|
|
|
|
private final static int RCD_REG_FAILURE = 0;
|
|
private final static int RCD_REG_SUCCESS_PERMISSION = 1;
|
|
private final static int RCD_REG_SUCCESS_ENABLED_NOTIF = 2;
|
|
|
|
/**
|
|
* Checks a caller's authorization to register an IRemoteControlDisplay.
|
|
* Authorization is granted if one of the following is true:
|
|
* <ul>
|
|
* <li>the caller has android.Manifest.permission.MEDIA_CONTENT_CONTROL permission</li>
|
|
* <li>the caller's listener is one of the enabled notification listeners</li>
|
|
* </ul>
|
|
* @return RCD_REG_FAILURE if it's not safe to proceed with the IRemoteControlDisplay
|
|
* registration.
|
|
*/
|
|
private int checkRcdRegistrationAuthorization(ComponentName listenerComp) {
|
|
// MEDIA_CONTENT_CONTROL permission check
|
|
if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
|
|
android.Manifest.permission.MEDIA_CONTENT_CONTROL)) {
|
|
if (DEBUG_RC) { Log.d(TAG, "ok to register Rcd: has MEDIA_CONTENT_CONTROL permission");}
|
|
return RCD_REG_SUCCESS_PERMISSION;
|
|
}
|
|
|
|
// ENABLED_NOTIFICATION_LISTENERS settings check
|
|
if (listenerComp != null) {
|
|
// this call is coming from an app, can't use its identity to read secure settings
|
|
final long ident = Binder.clearCallingIdentity();
|
|
try {
|
|
final int currentUser = ActivityManager.getCurrentUser();
|
|
final String enabledNotifListeners = Settings.Secure.getStringForUser(
|
|
mContext.getContentResolver(),
|
|
Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
|
|
currentUser);
|
|
if (enabledNotifListeners != null) {
|
|
final String[] components = enabledNotifListeners.split(":");
|
|
for (int i=0; i<components.length; i++) {
|
|
final ComponentName component =
|
|
ComponentName.unflattenFromString(components[i]);
|
|
if (component != null) {
|
|
if (listenerComp.equals(component)) {
|
|
if (DEBUG_RC) { Log.d(TAG, "ok to register RCC: " + component +
|
|
" is authorized notification listener"); }
|
|
return RCD_REG_SUCCESS_ENABLED_NOTIF;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (DEBUG_RC) { Log.d(TAG, "not ok to register RCD, " + listenerComp +
|
|
" is not in list of ENABLED_NOTIFICATION_LISTENERS"); }
|
|
} finally {
|
|
Binder.restoreCallingIdentity(ident);
|
|
}
|
|
}
|
|
|
|
return RCD_REG_FAILURE;
|
|
}
|
|
|
|
protected boolean registerRemoteController(IRemoteControlDisplay rcd, int w, int h,
|
|
ComponentName listenerComp) {
|
|
int reg = checkRcdRegistrationAuthorization(listenerComp);
|
|
if (reg != RCD_REG_FAILURE) {
|
|
registerRemoteControlDisplay_int(rcd, w, h, listenerComp);
|
|
return true;
|
|
} else {
|
|
Slog.w(TAG, "Access denied to process: " + Binder.getCallingPid() +
|
|
", must have permission " + android.Manifest.permission.MEDIA_CONTENT_CONTROL +
|
|
" or be an enabled NotificationListenerService for registerRemoteController");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected boolean registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) {
|
|
int reg = checkRcdRegistrationAuthorization(null);
|
|
if (reg != RCD_REG_FAILURE) {
|
|
registerRemoteControlDisplay_int(rcd, w, h, null);
|
|
return true;
|
|
} else {
|
|
Slog.w(TAG, "Access denied to process: " + Binder.getCallingPid() +
|
|
", must have permission " + android.Manifest.permission.MEDIA_CONTENT_CONTROL +
|
|
" to register IRemoteControlDisplay");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void postReevaluateRemoteControlDisplays() {
|
|
sendMsg(mEventHandler, MSG_REEVALUATE_RCD, SENDMSG_QUEUE, 0, 0, null, 0);
|
|
}
|
|
|
|
private void onReevaluateRemoteControlDisplays() {
|
|
if (DEBUG_RC) { Log.d(TAG, "onReevaluateRemoteControlDisplays()"); }
|
|
// read which components are enabled notification listeners
|
|
final int currentUser = ActivityManager.getCurrentUser();
|
|
final String enabledNotifListeners = Settings.Secure.getStringForUser(
|
|
mContext.getContentResolver(),
|
|
Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
|
|
currentUser);
|
|
if (DEBUG_RC) { Log.d(TAG, " > enabled list: " + enabledNotifListeners); }
|
|
synchronized(mAudioFocusLock) {
|
|
synchronized(mRCStack) {
|
|
// check whether the "enable" status of each RCD with a notification listener
|
|
// has changed
|
|
final String[] enabledComponents;
|
|
if (enabledNotifListeners == null) {
|
|
enabledComponents = null;
|
|
} else {
|
|
enabledComponents = enabledNotifListeners.split(":");
|
|
}
|
|
final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
|
|
while (displayIterator.hasNext()) {
|
|
final DisplayInfoForServer di =
|
|
(DisplayInfoForServer) displayIterator.next();
|
|
if (di.mClientNotifListComp != null) {
|
|
boolean wasEnabled = di.mEnabled;
|
|
di.mEnabled = isComponentInStringArray(di.mClientNotifListComp,
|
|
enabledComponents);
|
|
if (wasEnabled != di.mEnabled){
|
|
try {
|
|
// tell the RCD whether it's enabled
|
|
di.mRcDisplay.setEnabled(di.mEnabled);
|
|
// tell the RCCs about the change for this RCD
|
|
enableRemoteControlDisplayForClient_syncRcStack(
|
|
di.mRcDisplay, di.mEnabled);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error en/disabling RCD: ", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param comp a non-null ComponentName
|
|
* @param enabledArray may be null
|
|
* @return
|
|
*/
|
|
private boolean isComponentInStringArray(ComponentName comp, String[] enabledArray) {
|
|
if (enabledArray == null || enabledArray.length == 0) {
|
|
if (DEBUG_RC) { Log.d(TAG, " > " + comp + " is NOT enabled"); }
|
|
return false;
|
|
}
|
|
final String compString = comp.flattenToString();
|
|
for (int i=0; i<enabledArray.length; i++) {
|
|
if (compString.equals(enabledArray[i])) {
|
|
if (DEBUG_RC) { Log.d(TAG, " > " + compString + " is enabled"); }
|
|
return true;
|
|
}
|
|
}
|
|
if (DEBUG_RC) { Log.d(TAG, " > " + compString + " is NOT enabled"); }
|
|
return false;
|
|
}
|
|
|
|
//==========================================================================================
|
|
// Internal event handling
|
|
//==========================================================================================
|
|
|
|
// 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;
|
|
private static final int MSG_RCC_UPDATE_METADATA = 9;
|
|
private static final int MSG_RCDISPLAY_INIT_INFO = 10;
|
|
private static final int MSG_REEVALUATE_RCD = 11;
|
|
|
|
// sendMsg() flags
|
|
/** If the msg is already queued, replace it with this one. */
|
|
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 */);
|
|
break;
|
|
|
|
case MSG_RCC_UPDATE_METADATA:
|
|
onUpdateRemoteControlClientMetadata(msg.arg1 /*genId*/, msg.arg2 /*key*/,
|
|
(Rating) msg.obj /* value */);
|
|
break;
|
|
|
|
case MSG_PROMOTE_RCC:
|
|
onPromoteRcc(msg.arg1);
|
|
break;
|
|
|
|
case MSG_RCDISPLAY_INIT_INFO:
|
|
// msg.obj is guaranteed to be non null
|
|
onRcDisplayInitInfo((IRemoteControlDisplay)msg.obj /*newRcd*/,
|
|
msg.arg1/*w*/, msg.arg2/*h*/);
|
|
break;
|
|
|
|
case MSG_REEVALUATE_RCD:
|
|
onReevaluateRemoteControlDisplays();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//==========================================================================================
|
|
// 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()) {
|
|
// notify the current focus owner it lost focus after removing it from stack
|
|
final FocusRequester exFocusOwner = mFocusStack.pop();
|
|
exFocusOwner.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS);
|
|
exFocusOwner.release();
|
|
// clear RCD
|
|
synchronized(mRCStack) {
|
|
clearRemoteControlDisplay_syncAfRcs();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void notifyTopOfAudioFocusStack() {
|
|
// notify the top of the stack it gained focus
|
|
if (!mFocusStack.empty()) {
|
|
if (canReassignAudioFocus()) {
|
|
mFocusStack.peek().handleFocusGain(AudioManager.AUDIOFOCUS_GAIN);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Focus is requested, propagate the associated loss throughout the stack.
|
|
* @param focusGain the new focus gain that will later be added at the top of the stack
|
|
*/
|
|
private void propagateFocusLossFromGain_syncAf(int focusGain) {
|
|
// going through the audio focus stack to signal new focus, traversing order doesn't
|
|
// matter as all entries respond to the same external focus gain
|
|
Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
stackIterator.next().handleExternalFocusGain(focusGain);
|
|
}
|
|
}
|
|
|
|
private final Stack<FocusRequester> mFocusStack = new Stack<FocusRequester>();
|
|
|
|
/**
|
|
* 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<FocusRequester> stackIterator = mFocusStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
stackIterator.next().dump(pw);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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().hasSameClient(clientToRemove))
|
|
{
|
|
//Log.i(TAG, " removeFocusStackEntry() removing top of stack");
|
|
FocusRequester fr = mFocusStack.pop();
|
|
fr.release();
|
|
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<FocusRequester> stackIterator = mFocusStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
FocusRequester fr = (FocusRequester)stackIterator.next();
|
|
if(fr.hasSameClient(clientToRemove)) {
|
|
Log.i(TAG, "AudioFocus removeFocusStackEntry(): removing entry for "
|
|
+ clientToRemove);
|
|
stackIterator.remove();
|
|
fr.release();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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().hasSameBinder(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<FocusRequester> stackIterator = mFocusStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
FocusRequester fr = (FocusRequester)stackIterator.next();
|
|
if(fr.hasSameBinder(cb)) {
|
|
Log.i(TAG, "AudioFocus removeFocusStackEntry(): removing entry for " + cb);
|
|
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() && mFocusStack.peek().hasSameClient(IN_VOICE_COMM_FOCUS_ID)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Inner class to monitor audio focus client deaths, and remove them from the audio focus
|
|
* stack if necessary.
|
|
*/
|
|
protected 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;
|
|
}
|
|
}
|
|
|
|
protected int getCurrentAudioFocus() {
|
|
synchronized(mAudioFocusLock) {
|
|
if (mFocusStack.empty()) {
|
|
return AudioManager.AUDIOFOCUS_NONE;
|
|
} else {
|
|
return mFocusStack.peek().getGainRequest();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @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);
|
|
// 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().hasSameClient(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().getGainRequest() == 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
|
|
FocusRequester fr = mFocusStack.pop();
|
|
fr.release();
|
|
}
|
|
|
|
// focus requester might already be somewhere below in the stack, remove it
|
|
removeFocusStackEntry(clientId, false /* signal */);
|
|
|
|
// propagate the focus change through the stack
|
|
if (!mFocusStack.empty()) {
|
|
propagateFocusLossFromGain_syncAf(focusChangeHint);
|
|
}
|
|
|
|
// push focus requester at the top of the audio focus stack
|
|
mFocusStack.push(new FocusRequester(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 /*signal*/);
|
|
}
|
|
} 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
|
|
//==========================================================================================
|
|
/**
|
|
* No-op if the key code for keyEvent is not a valid media key
|
|
* (see {@link #isValidMediaKeyEvent(KeyEvent)})
|
|
* @param keyEvent the key event to send
|
|
*/
|
|
protected void dispatchMediaKeyEvent(KeyEvent keyEvent) {
|
|
filterMediaKeyEvent(keyEvent, false /*needWakeLock*/);
|
|
}
|
|
|
|
/**
|
|
* No-op if the key code for keyEvent is not a valid media key
|
|
* (see {@link #isValidMediaKeyEvent(KeyEvent)})
|
|
* @param keyEvent the key event to send
|
|
*/
|
|
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 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected static boolean isMediaKeyCode(int keyCode) {
|
|
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:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
|
|
if (keyEvent == null) {
|
|
return false;
|
|
}
|
|
return MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode());
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
final long identity = Binder.clearCallingIdentity();
|
|
try {
|
|
if (voiceIntent != null) {
|
|
voiceIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
|
mContext.startActivityAsUser(voiceIntent, UserHandle.CURRENT);
|
|
}
|
|
} catch (ActivityNotFoundException e) {
|
|
Log.w(TAG, "No activity for search: " + e);
|
|
} finally {
|
|
Binder.restoreCallingIdentity(identity);
|
|
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;
|
|
/**
|
|
* The PendingIntent associated with mCurrentRcClient. Its value is irrelevant
|
|
* if mCurrentRcClient is null
|
|
*/
|
|
private PendingIntent mCurrentRcClientIntent = 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() + ", " + posToString() + ", " + mSpeed + "X";
|
|
}
|
|
|
|
private String posToString() {
|
|
if (mPositionMs == RemoteControlClient.PLAYBACK_POSITION_INVALID) {
|
|
return "PLAYBACK_POSITION_INVALID";
|
|
} else if (mPositionMs == RemoteControlClient.PLAYBACK_POSITION_ALWAYS_UNKNOWN) {
|
|
return "PLAYBACK_POSITION_ALWAYS_UNKNOWN";
|
|
} else {
|
|
return (String.valueOf(mPositionMs) + "ms");
|
|
}
|
|
}
|
|
|
|
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 +
|
|
" -- " + (di.mEnabled ? "enabled" : "disabled"));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
//TODO change name to informationRequestForAllDisplays()
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when processing MSG_RCDISPLAY_INIT_INFO event
|
|
* Causes the current RemoteControlClient to send its info (metadata, playstate...) to
|
|
* a single RemoteControlDisplay, NOT all of them, as with MSG_RCDISPLAY_UPDATE.
|
|
*/
|
|
private void onRcDisplayInitInfo(IRemoteControlDisplay newRcd, int w, int h) {
|
|
synchronized(mRCStack) {
|
|
synchronized(mCurrentRcLock) {
|
|
if (mCurrentRcClient != null) {
|
|
if (DEBUG_RC) { Log.i(TAG, "Init RCD with current info"); }
|
|
try {
|
|
// synchronously update the new RCD with the current client generation
|
|
// and matching PendingIntent
|
|
newRcd.setCurrentClientId(mCurrentRcClientGen, mCurrentRcClientIntent,
|
|
false);
|
|
|
|
// tell the current RCC that it needs to send info, but only to the new RCD
|
|
try {
|
|
mCurrentRcClient.informationRequestForDisplay(newRcd, w, h);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Current valid remote client is dead: ", e);
|
|
mCurrentRcClient = null;
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Dead display in onRcDisplayInitInfo()", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
mCurrentRcClientIntent = rcse.mMediaIntent;
|
|
}
|
|
// 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
|
|
FocusRequester af = null;
|
|
try {
|
|
for (int index = mFocusStack.size()-1; index >= 0; index--) {
|
|
FocusRequester fr = mFocusStack.elementAt(index);
|
|
if ((fr.getStreamType() == AudioManager.STREAM_MUSIC)
|
|
|| (fr.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN)) {
|
|
af = fr;
|
|
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 (!af.hasSamePackage(mRCStack.peek().mCallingPackageName)) {
|
|
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 (!af.hasSameUid(mRCStack.peek().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 final IRemoteControlDisplay mRcDisplay;
|
|
private final IBinder mRcDisplayBinder;
|
|
private int mArtworkExpectedWidth = -1;
|
|
private int mArtworkExpectedHeight = -1;
|
|
private boolean mWantsPositionSync = false;
|
|
private ComponentName mClientNotifListComp;
|
|
private boolean mEnabled = true;
|
|
|
|
public DisplayInfoForServer(IRemoteControlDisplay rcd, int w, int h) {
|
|
if (DEBUG_RC) Log.i(TAG, "new DisplayInfoForServer for " + rcd + " w=" + w + " h=" + h);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void enableRemoteControlDisplayForClient_syncRcStack(IRemoteControlDisplay rcd,
|
|
boolean enabled) {
|
|
// let all the remote control clients know whether the given display is enabled
|
|
// (so the remote control stack traversal order doesn't matter).
|
|
final Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
if(rcse.mRcClient != null) {
|
|
try {
|
|
rcse.mRcClient.enableRemoteControlDisplay(rcd, enabled);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error connecting RCD to client: ", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is the remote control display interface already registered
|
|
* @param rcd
|
|
* @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.
|
|
* @param listenerComp the component for the listener interface, may be null if it's not needed
|
|
* to verify it belongs to one of the enabled notification listeners
|
|
*/
|
|
private void registerRemoteControlDisplay_int(IRemoteControlDisplay rcd, int w, int h,
|
|
ComponentName listenerComp) {
|
|
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);
|
|
di.mEnabled = true;
|
|
di.mClientNotifListComp = listenerComp;
|
|
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
|
|
// initialized wih the current gen ID and the current client info, do not
|
|
// reset the information for the other (existing) displays
|
|
sendMsg(mEventHandler, MSG_RCDISPLAY_INIT_INFO, SENDMSG_QUEUE,
|
|
w /*arg1*/, h /*arg2*/,
|
|
rcd /*obj*/, 0/*delay*/);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 updateRemoteControlClientMetadata(int genId, int key, Rating value) {
|
|
sendMsg(mEventHandler, MSG_RCC_UPDATE_METADATA, SENDMSG_QUEUE,
|
|
genId /* arg1 */, key /* arg2 */, value /* obj */, 0 /* delay */);
|
|
}
|
|
|
|
private void onUpdateRemoteControlClientMetadata(int genId, int key, Rating value) {
|
|
if(DEBUG_RC) Log.d(TAG, "onUpdateRemoteControlClientMetadata(genId=" + genId +
|
|
", what=" + key + ",rating=" + value + ")");
|
|
synchronized(mRCStack) {
|
|
synchronized(mCurrentRcLock) {
|
|
if ((mCurrentRcClient != null) && (mCurrentRcClientGen == genId)) {
|
|
try {
|
|
switch (key) {
|
|
case MediaMetadataEditor.RATING_KEY_BY_USER:
|
|
mCurrentRcClient.updateMetadata(genId, key, value);
|
|
break;
|
|
default:
|
|
Log.e(TAG, "unhandled metadata key " + key + " update for RCC "
|
|
+ genId);
|
|
break;
|
|
}
|
|
} 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 */);
|
|
}
|
|
|
|
private 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|