From 632ca417f0a33e3fa9ccece531afa2db3f0d4a30 Mon Sep 17 00:00:00 2001 From: Dianne Hackborn Date: Thu, 14 Jun 2012 19:34:10 -0700 Subject: [PATCH] Add new callback for MediaRouter to found out about device changes. The AudioService now has an API to call to get the currently connected devices, and later reports of changes in connection state. The information includes the name of the bluetooth device if one is connected for display to the user, and states for all of the pluggable devices. No longer requires a Bluetooth permission to keep the routes updated. Change-Id: I81ca421c60592fbc1592477d59bf1c9d1b64954a --- Android.mk | 1 + media/java/android/media/AudioRoutesInfo.aidl | 18 +++ media/java/android/media/AudioRoutesInfo.java | 71 +++++++++ media/java/android/media/AudioService.java | 82 +++++++++- .../android/media/IAudioRoutesObserver.aidl | 27 ++++ media/java/android/media/IAudioService.aidl | 4 + media/java/android/media/MediaRouter.java | 148 ++++++++---------- 7 files changed, 268 insertions(+), 83 deletions(-) create mode 100644 media/java/android/media/AudioRoutesInfo.aidl create mode 100644 media/java/android/media/AudioRoutesInfo.java create mode 100644 media/java/android/media/IAudioRoutesObserver.aidl diff --git a/Android.mk b/Android.mk index b0a3dace6477b..5aaea972221f9 100644 --- a/Android.mk +++ b/Android.mk @@ -193,6 +193,7 @@ LOCAL_SRC_FILES += \ location/java/android/location/INetInitiatedListener.aidl \ media/java/android/media/IAudioService.aidl \ media/java/android/media/IAudioFocusDispatcher.aidl \ + media/java/android/media/IAudioRoutesObserver.aidl \ media/java/android/media/IMediaScannerListener.aidl \ media/java/android/media/IMediaScannerService.aidl \ media/java/android/media/IRemoteControlClient.aidl \ diff --git a/media/java/android/media/AudioRoutesInfo.aidl b/media/java/android/media/AudioRoutesInfo.aidl new file mode 100644 index 0000000000000..d665851b78d47 --- /dev/null +++ b/media/java/android/media/AudioRoutesInfo.aidl @@ -0,0 +1,18 @@ +/* Copyright 2012, 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; + +parcelable AudioRoutesInfo; diff --git a/media/java/android/media/AudioRoutesInfo.java b/media/java/android/media/AudioRoutesInfo.java new file mode 100644 index 0000000000000..df9fc06fb034e --- /dev/null +++ b/media/java/android/media/AudioRoutesInfo.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012 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.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Information available from AudioService about the current routes. + * @hide + */ +public class AudioRoutesInfo implements Parcelable { + static final int MAIN_SPEAKER = 0; + static final int MAIN_HEADSET = 1<<0; + static final int MAIN_HEADPHONES = 1<<1; + static final int MAIN_DOCK_SPEAKERS = 1<<2; + static final int MAIN_HDMI = 1<<3; + + CharSequence mBluetoothName; + int mMainType = MAIN_SPEAKER; + + public AudioRoutesInfo() { + } + + public AudioRoutesInfo(AudioRoutesInfo o) { + mBluetoothName = o.mBluetoothName; + mMainType = o.mMainType; + } + + AudioRoutesInfo(Parcel src) { + mBluetoothName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src); + mMainType = src.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + TextUtils.writeToParcel(mBluetoothName, dest, flags); + dest.writeInt(mMainType); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public AudioRoutesInfo createFromParcel(Parcel in) { + return new AudioRoutesInfo(in); + } + + public AudioRoutesInfo[] newArray(int size) { + return new AudioRoutesInfo[size]; + } + }; +} diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java index a69912d3d36f3..ddb7e6b1812db 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -53,6 +53,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; @@ -62,6 +63,7 @@ import android.provider.Settings.System; import android.speech.RecognizerIntent; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; +import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.view.VolumePanel; @@ -135,10 +137,11 @@ public class AudioService extends IAudioService.Stub implements OnFinished { private static final int MSG_RCDISPLAY_UPDATE = 13; private static final int MSG_SET_ALL_VOLUMES = 14; private static final int MSG_PERSIST_MASTER_VOLUME_MUTE = 15; + private static final int MSG_REPORT_NEW_ROUTES = 16; // messages handled under wakelock, can only be queued, i.e. sent with queueMsgUnderWakeLock(), // and not with sendMsg(..., ..., SENDMSG_QUEUE, ...) - private static final int MSG_SET_WIRED_DEVICE_CONNECTION_STATE = 16; - private static final int MSG_SET_A2DP_CONNECTION_STATE = 17; + private static final int MSG_SET_WIRED_DEVICE_CONNECTION_STATE = 17; + private static final int MSG_SET_A2DP_CONNECTION_STATE = 18; // flags for MSG_PERSIST_VOLUME indicating if current and/or last audible volume should be @@ -397,6 +400,11 @@ public class AudioService extends IAudioService.Stub implements OnFinished { private boolean mBluetoothA2dpEnabled; private final Object mBluetoothA2dpEnabledLock = new Object(); + // Monitoring of audio routes. Protected by mCurAudioRoutes. + final AudioRoutesInfo mCurAudioRoutes = new AudioRoutesInfo(); + final RemoteCallbackList mRoutesObservers + = new RemoteCallbackList(); + /////////////////////////////////////////////////////////////////////////// // Construction /////////////////////////////////////////////////////////////////////////// @@ -3011,6 +3019,26 @@ public class AudioService extends IAudioService.Stub implements OnFinished { onSetA2dpConnectionState((BluetoothDevice)msg.obj, msg.arg1); mMediaEventWakeLock.release(); break; + + case MSG_REPORT_NEW_ROUTES: { + int N = mRoutesObservers.beginBroadcast(); + if (N > 0) { + AudioRoutesInfo routes; + synchronized (mCurAudioRoutes) { + routes = new AudioRoutesInfo(mCurAudioRoutes); + } + while (N > 0) { + N--; + IAudioRoutesObserver obs = mRoutesObservers.getBroadcastItem(N); + try { + obs.dispatchAudioRoutesChanged(routes); + } catch (RemoteException e) { + } + } + } + mRoutesObservers.finishBroadcast(); + break; + } } } } @@ -3127,6 +3155,13 @@ public class AudioService extends IAudioService.Stub implements OnFinished { } else { makeA2dpDeviceUnavailableNow(address); } + synchronized (mCurAudioRoutes) { + if (mCurAudioRoutes.mBluetoothName != null) { + mCurAudioRoutes.mBluetoothName = null; + sendMsg(mAudioHandler, MSG_REPORT_NEW_ROUTES, + SENDMSG_NOOP, 0, 0, null, 0); + } + } } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) { if (btDevice.isBluetoothDock()) { // this could be a reconnection after a transient disconnection @@ -3141,6 +3176,14 @@ public class AudioService extends IAudioService.Stub implements OnFinished { } } makeA2dpDeviceAvailable(address); + synchronized (mCurAudioRoutes) { + String name = btDevice.getAliasName(); + if (!TextUtils.equals(mCurAudioRoutes.mBluetoothName, name)) { + mCurAudioRoutes.mBluetoothName = name; + sendMsg(mAudioHandler, MSG_REPORT_NEW_ROUTES, + SENDMSG_NOOP, 0, 0, null, 0); + } + } } } } @@ -3204,20 +3247,43 @@ public class AudioService extends IAudioService.Stub implements OnFinished { intent.putExtra("name", name); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + int connType = 0; + if (device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) { + connType = AudioRoutesInfo.MAIN_HEADSET; intent.setAction(Intent.ACTION_HEADSET_PLUG); intent.putExtra("microphone", 1); } else if (device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE) { + connType = AudioRoutesInfo.MAIN_HEADPHONES; intent.setAction(Intent.ACTION_HEADSET_PLUG); intent.putExtra("microphone", 0); } else if (device == AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET) { + connType = AudioRoutesInfo.MAIN_DOCK_SPEAKERS; intent.setAction(Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG); } else if (device == AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET) { + connType = AudioRoutesInfo.MAIN_DOCK_SPEAKERS; intent.setAction(Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG); } else if (device == AudioSystem.DEVICE_OUT_AUX_DIGITAL) { + connType = AudioRoutesInfo.MAIN_HDMI; intent.setAction(Intent.ACTION_HDMI_AUDIO_PLUG); } + synchronized (mCurAudioRoutes) { + if (connType != 0) { + int newConn = mCurAudioRoutes.mMainType; + if (state != 0) { + newConn |= connType; + } else { + newConn &= ~connType; + } + if (newConn != mCurAudioRoutes.mMainType) { + mCurAudioRoutes.mMainType = newConn; + sendMsg(mAudioHandler, MSG_REPORT_NEW_ROUTES, + SENDMSG_NOOP, 0, 0, null, 0); + } + } + } + ActivityManagerNative.broadcastStickyIntent(intent, null); } @@ -4795,6 +4861,15 @@ public class AudioService extends IAudioService.Stub implements OnFinished { return mRingtonePlayer; } + @Override + public AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) { + synchronized (mCurAudioRoutes) { + AudioRoutesInfo routes = new AudioRoutesInfo(mCurAudioRoutes); + mRoutesObservers.register(observer); + return routes; + } + } + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); @@ -4803,5 +4878,8 @@ public class AudioService extends IAudioService.Stub implements OnFinished { dumpFocusStack(pw); dumpRCStack(pw); dumpStreamStates(pw); + pw.println("\nAudio routes:"); + pw.print(" mMainType=0x"); pw.println(Integer.toHexString(mCurAudioRoutes.mMainType)); + pw.print(" mBluetoothName="); pw.println(mCurAudioRoutes.mBluetoothName); } } diff --git a/media/java/android/media/IAudioRoutesObserver.aidl b/media/java/android/media/IAudioRoutesObserver.aidl new file mode 100644 index 0000000000000..c269b83451a40 --- /dev/null +++ b/media/java/android/media/IAudioRoutesObserver.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2012 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.media.AudioRoutesInfo; + +/** + * AIDL for the AudioService to report changes in available audio routes. + * @hide + */ +oneway interface IAudioRoutesObserver { + void dispatchAudioRoutesChanged(in AudioRoutesInfo newRoutes); +} diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 70fc62346e093..fc5b8f1254a0f 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -19,7 +19,9 @@ package android.media; import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; import android.content.ComponentName; +import android.media.AudioRoutesInfo; import android.media.IAudioFocusDispatcher; +import android.media.IAudioRoutesObserver; import android.media.IRemoteControlClient; import android.media.IRemoteControlDisplay; import android.media.IRingtonePlayer; @@ -137,4 +139,6 @@ interface IAudioService { void setWiredDeviceConnectionState(int device, int state, String name); int setBluetoothA2dpDeviceConnectionState(in BluetoothDevice device, int state); + + AudioRoutesInfo startWatchingRoutes(in IAudioRoutesObserver observer); } diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java index 9e70b7f056d34..10865030dbd80 100644 --- a/media/java/android/media/MediaRouter.java +++ b/media/java/android/media/MediaRouter.java @@ -16,14 +16,14 @@ package android.media; -import android.bluetooth.BluetoothA2dp; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; @@ -46,7 +46,7 @@ public class MediaRouter { static class Static { final Resources mResources; - final AudioManager mAudioManager; + final IAudioService mAudioService; final Handler mHandler; final ArrayList mCallbacks = new ArrayList(); @@ -54,38 +54,89 @@ public class MediaRouter { final ArrayList mCategories = new ArrayList(); final RouteCategory mSystemCategory; - final HeadphoneChangedBroadcastReceiver mHeadphoneChangedReceiver; + + final AudioRoutesInfo mCurRoutesInfo = new AudioRoutesInfo(); RouteInfo mDefaultAudio; RouteInfo mBluetoothA2dpRoute; RouteInfo mSelectedRoute; + final IAudioRoutesObserver.Stub mRoutesObserver = new IAudioRoutesObserver.Stub() { + public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) { + mHandler.post(new Runnable() { + @Override public void run() { + updateRoutes(newRoutes); + } + }); + } + }; + Static(Context appContext) { mResources = Resources.getSystem(); mHandler = new Handler(appContext.getMainLooper()); - mAudioManager = (AudioManager)appContext.getSystemService(Context.AUDIO_SERVICE); + IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); + mAudioService = IAudioService.Stub.asInterface(b); // XXX this doesn't deal with locale changes! mSystemCategory = new RouteCategory(mResources.getText( com.android.internal.R.string.default_audio_route_category_name), ROUTE_TYPE_LIVE_AUDIO, false); - - final IntentFilter speakerFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); - speakerFilter.addAction(Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG); - speakerFilter.addAction(Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG); - speakerFilter.addAction(Intent.ACTION_HDMI_AUDIO_PLUG); - mHeadphoneChangedReceiver = new HeadphoneChangedBroadcastReceiver(); - appContext.registerReceiver(mHeadphoneChangedReceiver, speakerFilter); } // Called after sStatic is initialized - void initDefaultRoutes() { + void startMonitoringRoutes() { mDefaultAudio = new RouteInfo(mSystemCategory); mDefaultAudio.mNameResId = com.android.internal.R.string.default_audio_route_name; mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO; addRoute(mDefaultAudio); + + AudioRoutesInfo newRoutes = null; + try { + newRoutes = mAudioService.startWatchingRoutes(mRoutesObserver); + } catch (RemoteException e) { + } + if (newRoutes != null) { + updateRoutes(newRoutes); + } + } + + void updateRoutes(AudioRoutesInfo newRoutes) { + if (newRoutes.mMainType != mCurRoutesInfo.mMainType) { + mCurRoutesInfo.mMainType = newRoutes.mMainType; + int name; + if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADPHONES) != 0 + || (newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADSET) != 0) { + name = com.android.internal.R.string.default_audio_route_name_headphones; + } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { + name = com.android.internal.R.string.default_audio_route_name_dock_speakers; + } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HDMI) != 0) { + name = com.android.internal.R.string.default_audio_route_name_hdmi; + } else { + name = com.android.internal.R.string.default_audio_route_name; + } + sStatic.mDefaultAudio.mNameResId = name; + dispatchRouteChanged(sStatic.mDefaultAudio); + } + if (!TextUtils.equals(newRoutes.mBluetoothName, mCurRoutesInfo.mBluetoothName)) { + mCurRoutesInfo.mBluetoothName = newRoutes.mBluetoothName; + if (mCurRoutesInfo.mBluetoothName != null) { + if (sStatic.mBluetoothA2dpRoute == null) { + final RouteInfo info = new RouteInfo(sStatic.mSystemCategory); + info.mName = mCurRoutesInfo.mBluetoothName; + info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO; + sStatic.mBluetoothA2dpRoute = info; + addRoute(sStatic.mBluetoothA2dpRoute); + } else { + sStatic.mBluetoothA2dpRoute.mName = mCurRoutesInfo.mBluetoothName; + dispatchRouteChanged(sStatic.mBluetoothA2dpRoute); + } + } else if (sStatic.mBluetoothA2dpRoute != null) { + removeRoute(sStatic.mBluetoothA2dpRoute); + sStatic.mBluetoothA2dpRoute = null; + } + } } } @@ -132,7 +183,7 @@ public class MediaRouter { synchronized (Static.class) { if (sStatic == null) { sStatic = new Static(context.getApplicationContext()); - sStatic.initDefaultRoutes(); + sStatic.startMonitoringRoutes(); } } } @@ -154,19 +205,6 @@ public class MediaRouter { return sStatic.mSelectedRoute; } - static void onHeadphonesPlugged(boolean headphonesPresent, String headphonesName) { - if (headphonesPresent) { - sStatic.mDefaultAudio.mName = headphonesName; - sStatic.mDefaultAudio.mNameResId = 0; - } else { - sStatic.mDefaultAudio.mName = null; - sStatic.mDefaultAudio.mNameResId = - com.android.internal.R.string.default_audio_route_name; - } - - dispatchRouteChanged(sStatic.mDefaultAudio); - } - /** * Add a callback to listen to events about specific kinds of media routes. * If the specified callback is already registered, its registration will be updated for any @@ -528,18 +566,6 @@ public class MediaRouter { } } - static void onA2dpDeviceConnected() { - final RouteInfo info = new RouteInfo(sStatic.mSystemCategory); - info.mNameResId = com.android.internal.R.string.bluetooth_a2dp_audio_route_name; - sStatic.mBluetoothA2dpRoute = info; - addRoute(sStatic.mBluetoothA2dpRoute); - } - - static void onA2dpDeviceDisconnected() { - removeRoute(sStatic.mBluetoothA2dpRoute); - sStatic.mBluetoothA2dpRoute = null; - } - /** * Information about a media route. */ @@ -1158,44 +1184,4 @@ public class MediaRouter { } } - - class BtChangedBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { - final int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1); - if (state == BluetoothA2dp.STATE_CONNECTED) { - onA2dpDeviceConnected(); - } else if (state == BluetoothA2dp.STATE_DISCONNECTING || - state == BluetoothA2dp.STATE_DISCONNECTED) { - onA2dpDeviceDisconnected(); - } - } - } - } - - static class HeadphoneChangedBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (Intent.ACTION_HEADSET_PLUG.equals(action)) { - final boolean plugged = intent.getIntExtra("state", 0) != 0; - final String name = sStatic.mResources.getString( - com.android.internal.R.string.default_audio_route_name_headphones); - onHeadphonesPlugged(plugged, name); - } else if (Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG.equals(action) || - Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG.equals(action)) { - final boolean plugged = intent.getIntExtra("state", 0) != 0; - final String name = sStatic.mResources.getString( - com.android.internal.R.string.default_audio_route_name_dock_speakers); - onHeadphonesPlugged(plugged, name); - } else if (Intent.ACTION_HDMI_AUDIO_PLUG.equals(action)) { - final boolean plugged = intent.getIntExtra("state", 0) != 0; - final String name = sStatic.mResources.getString( - com.android.internal.R.string.default_audio_route_name_hdmi); - onHeadphonesPlugged(plugged, name); - } - } - } }