diff options
| author | Adam Powell <adamp@google.com> | 2012-06-03 14:14:29 -0700 |
|---|---|---|
| committer | Android Git Automerger <android-git-automerger@android.com> | 2012-06-03 14:14:29 -0700 |
| commit | 58e29c06610054419339bb0a75c44ab30d6fe89a (patch) | |
| tree | a19ae80a64850b5dd6c52fe05d936e9d4f8a7009 | |
| parent | dafffc35023df8b22a1a7c294f79da89199dc4c2 (diff) | |
| parent | 7c86958d73e7216a92bdfd84fce4440e1def7eaa (diff) | |
| download | frameworks_base-58e29c06610054419339bb0a75c44ab30d6fe89a.zip frameworks_base-58e29c06610054419339bb0a75c44ab30d6fe89a.tar.gz frameworks_base-58e29c06610054419339bb0a75c44ab30d6fe89a.tar.bz2 | |
am 7c86958d: Merge "Add MediaRouter API." into jb-dev
* commit '7c86958d73e7216a92bdfd84fce4440e1def7eaa':
Add MediaRouter API.
| -rw-r--r-- | api/current.txt | 66 | ||||
| -rw-r--r-- | core/java/android/view/ActionProvider.java | 31 | ||||
| -rw-r--r-- | core/res/AndroidManifest.xml | 6 | ||||
| -rw-r--r-- | core/res/res/values/public.xml | 5 | ||||
| -rwxr-xr-x | core/res/res/values/strings.xml | 26 | ||||
| -rw-r--r-- | media/java/android/media/MediaRouter.java | 875 |
6 files changed, 992 insertions, 17 deletions
diff --git a/api/current.txt b/api/current.txt index ec35e21..43128ee 100644 --- a/api/current.txt +++ b/api/current.txt @@ -97,6 +97,7 @@ package android { field public static final java.lang.String RECORD_AUDIO = "android.permission.RECORD_AUDIO"; field public static final java.lang.String REORDER_TASKS = "android.permission.REORDER_TASKS"; field public static final deprecated java.lang.String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES"; + field public static final java.lang.String ROUTE_MEDIA_OUTPUT = "android.permission.ROUTE_MEDIA_OUTPUT"; field public static final java.lang.String SEND_SMS = "android.permission.SEND_SMS"; field public static final java.lang.String SET_ACTIVITY_WATCHER = "android.permission.SET_ACTIVITY_WATCHER"; field public static final java.lang.String SET_ALARM = "com.android.alarm.permission.SET_ALARM"; @@ -11486,6 +11487,71 @@ package android.media { field public static final int DEFAULT = 0; // 0x0 } + public class MediaRouter { + method public void addCallback(int, android.media.MediaRouter.Callback); + method public void addUserRoute(android.media.MediaRouter.UserRouteInfo); + method public android.media.MediaRouter.RouteCategory createRouteCategory(java.lang.CharSequence, boolean); + method public android.media.MediaRouter.UserRouteInfo createUserRoute(android.media.MediaRouter.RouteCategory); + method public static android.media.MediaRouter forApplication(android.content.Context); + method public android.media.MediaRouter.RouteCategory getCategoryAt(int); + method public int getCategoryCount(); + method public android.media.MediaRouter.RouteInfo getRouteAt(int); + method public int getRouteCount(); + method public void removeCallback(android.media.MediaRouter.Callback); + method public void removeUserRoute(android.media.MediaRouter.UserRouteInfo); + method public void selectRoute(int, android.media.MediaRouter.RouteInfo); + method public void setRouteVolume(int, float); + field public static final int ROUTE_TYPE_LIVE_AUDIO = 1; // 0x1 + field public static final int ROUTE_TYPE_USER = 8388608; // 0x800000 + } + + public static abstract interface MediaRouter.Callback { + method public abstract void onRouteAdded(int, android.media.MediaRouter.RouteInfo); + method public abstract void onRouteChanged(android.media.MediaRouter.RouteInfo); + method public abstract void onRouteRemoved(int, android.media.MediaRouter.RouteInfo); + method public abstract void onRouteSelected(int, android.media.MediaRouter.RouteInfo); + method public abstract void onRouteUnselected(int, android.media.MediaRouter.RouteInfo); + method public abstract void onVolumeChanged(int, float); + } + + public class MediaRouter.RouteCategory { + method public java.lang.CharSequence getName(); + method public android.media.MediaRouter.RouteInfo getRouteAt(int); + method public int getRouteCount(); + method public int getSupportedTypes(); + method public boolean isGroupable(); + } + + public class MediaRouter.RouteGroup extends android.media.MediaRouter.RouteInfo { + method public void addRoute(android.media.MediaRouter.RouteInfo); + method public void addRoute(android.media.MediaRouter.RouteInfo, int); + method public void removeRoute(android.media.MediaRouter.RouteInfo); + method public void removeRoute(int); + } + + public class MediaRouter.RouteInfo { + method public android.media.MediaRouter.RouteCategory getCategory(); + method public android.media.MediaRouter.RouteGroup getGroup(); + method public java.lang.CharSequence getName(); + method public java.lang.CharSequence getStatus(); + method public int getSupportedTypes(); + } + + public static class MediaRouter.SimpleCallback implements android.media.MediaRouter.Callback { + ctor public MediaRouter.SimpleCallback(); + method public void onRouteAdded(int, android.media.MediaRouter.RouteInfo); + method public void onRouteChanged(android.media.MediaRouter.RouteInfo); + method public void onRouteRemoved(int, android.media.MediaRouter.RouteInfo); + method public void onRouteSelected(int, android.media.MediaRouter.RouteInfo); + method public void onRouteUnselected(int, android.media.MediaRouter.RouteInfo); + method public void onVolumeChanged(int, float); + } + + public class MediaRouter.UserRouteInfo extends android.media.MediaRouter.RouteInfo { + method public void setName(java.lang.CharSequence); + method public void setStatus(java.lang.CharSequence); + } + public class MediaScannerConnection implements android.content.ServiceConnection { ctor public MediaScannerConnection(android.content.Context, android.media.MediaScannerConnection.MediaScannerConnectionClient); method public void connect(); diff --git a/core/java/android/view/ActionProvider.java b/core/java/android/view/ActionProvider.java index ed976ab..9150d19 100644 --- a/core/java/android/view/ActionProvider.java +++ b/core/java/android/view/ActionProvider.java @@ -19,28 +19,25 @@ package android.view; import android.content.Context; /** - * This class is a mediator for accomplishing a given task, for example sharing a file. - * It is responsible for creating a view that performs an action that accomplishes the task. - * This class also implements other functions such a performing a default action. - * <p> - * An ActionProvider can be optionally specified for a {@link MenuItem} and in such a - * case it will be responsible for creating the action view that appears in the - * {@link android.app.ActionBar} as a substitute for the menu item when the item is - * displayed as an action item. Also the provider is responsible for performing a - * default action if a menu item placed on the overflow menu of the ActionBar is - * selected and none of the menu item callbacks has handled the selection. For this - * case the provider can also optionally provide a sub-menu for accomplishing the - * task at hand. - * </p> - * <p> - * There are two ways for using an action provider for creating and handling of action views: + * An ActionProvider defines rich menu interaction in a single component. + * ActionProvider can generate action views for use in the action bar, + * dynamically populate submenus of a MenuItem, and handle default menu + * item invocations. + * + * <p>An ActionProvider can be optionally specified for a {@link MenuItem} and will be + * responsible for creating the action view that appears in the {@link android.app.ActionBar} + * in place of a simple button in the bar. When the menu item is presented in a way that + * does not allow custom action views, (e.g. in an overflow menu,) the ActionProvider + * can perform a default action.</p> + * + * <p>There are two ways to use an action provider: * <ul> * <li> - * Setting the action provider on a {@link MenuItem} directly by calling + * Set the action provider on a {@link MenuItem} directly by calling * {@link MenuItem#setActionProvider(ActionProvider)}. * </li> * <li> - * Declaring the action provider in the menu XML resource. For example: + * Declare the action provider in an XML menu resource. For example: * <pre> * <code> * <item android:id="@+id/my_menu_item" diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 9b417a2..d9d87c1 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -636,6 +636,12 @@ android:permissionGroup="android.permission-group.SYSTEM_TOOLS" android:protectionLevel="signature" /> + <!-- Allows an application to route media output to other devices. --> + <permission android:name="android.permission.ROUTE_MEDIA_OUTPUT" + android:permissionGroup="android.permission-group.SYSTEM_TOOLS" + android:label="@string/permlab_route_media_output" + android:description="@string/permdesc_route_media_output" /> + <!-- =========================================== --> <!-- Permissions associated with telephony state --> <!-- =========================================== --> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 51be22d..801fdf6 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -881,6 +881,11 @@ <java-symbol type="string" name="granularity_label_word" /> <java-symbol type="string" name="granularity_label_link" /> <java-symbol type="string" name="granularity_label_line" /> + <java-symbol type="string" name="default_audio_route_name" /> + <java-symbol type="string" name="default_audio_route_name_headphones" /> + <java-symbol type="string" name="default_audio_route_name_dock_speakers" /> + <java-symbol type="string" name="default_audio_route_name_hdmi" /> + <java-symbol type="string" name="default_audio_route_category_name" /> <java-symbol type="plurals" name="abbrev_in_num_days" /> <java-symbol type="plurals" name="abbrev_in_num_hours" /> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index d7386fa..2ffb20b 100755 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3149,6 +3149,11 @@ <!-- Description of an application permission, used to invoke default container service to copy content. --> <string name="permdesc_copyProtectedData">Allows the app to invoke default container service to copy content. Not for use by normal apps.</string> + <!-- Title of an application permission that lets an application route media output. --> + <string name="permlab_route_media_output">Route media output</string> + <!-- Description of an application permission that lets an application route media output. --> + <string name="permdesc_route_media_output">Allows an application to route media output to other external devices.</string> + <!-- Shown in the tutorial for tap twice for zoom control. --> <string name="tutorial_double_tap_to_zoom_message_short">Touch twice for zoom control</string> @@ -3563,4 +3568,25 @@ from the activity resolver to use just this once. [CHAR LIMIT=25] --> <string name="activity_resolver_use_once">Just once</string> + <!-- Name of the default audio route when nothing is connected to + a headphone or other wired audio output jack. [CHAR LIMIT=25] --> + <string name="default_audio_route_name">Phone speaker</string> + + <!-- Name of the default audio route for tablets when nothing + is connected to a headphone or other wired audio output jack. [CHAR LIMIT=25] --> + <string name="default_audio_route_name" product="tablet">Tablet speakers</string> + + <!-- Name of the default audio route when wired headphones are + connected. [CHAR LIMIT=25] --> + <string name="default_audio_route_name_headphones">Headphones</string> + + <!-- Name of the default audio route when an audio dock is connected. [CHAR LIMIT=25] --> + <string name="default_audio_route_name_dock_speakers">Dock speakers</string> + + <!-- Name of the default audio route when HDMI is connected. [CHAR LIMIT=25] --> + <string name="default_audio_route_name_hdmi">HDMI audio</string> + + <!-- Name of the default audio route category. [CHAR LIMIT=25] --> + <string name="default_audio_route_category_name">System</string> + </resources> diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java new file mode 100644 index 0000000..b23443d --- /dev/null +++ b/media/java/android/media/MediaRouter.java @@ -0,0 +1,875 @@ +/* + * 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.bluetooth.BluetoothA2dp; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * MediaRouter allows applications to control the routing of media channels + * and streams from the current device to external speakers and destination devices. + * + * <p>Media routes should only be modified on your application's main thread.</p> + */ +public class MediaRouter { + private static final String TAG = "MediaRouter"; + + private Context mAppContext; + private AudioManager mAudioManager; + private Handler mHandler; + private final ArrayList<CallbackInfo> mCallbacks = new ArrayList<CallbackInfo>(); + + private final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); + private final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>(); + + private final RouteCategory mSystemCategory; + private RouteInfo mDefaultAudio; + private RouteInfo mBluetoothA2dpRoute; + + private RouteInfo mSelectedRoute; + + // These get removed when an activity dies + final ArrayList<BroadcastReceiver> mRegisteredReceivers = new ArrayList<BroadcastReceiver>(); + + /** + * Route type flag for live audio. + * + * <p>A device that supports live audio routing will allow the media audio stream + * to be routed to supported destinations. This can include internal speakers or + * audio jacks on the device itself, A2DP devices, and more.</p> + * + * <p>Once initiated this routing is transparent to the application. All audio + * played on the media stream will be routed to the selected destination.</p> + */ + public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1; + + /** + * Route type flag for application-specific usage. + * + * <p>Unlike other media route types, user routes are managed by the application. + * The MediaRouter will manage and dispatch events for user routes, but the application + * is expected to interpret the meaning of these events and perform the requested + * routing tasks.</p> + */ + public static final int ROUTE_TYPE_USER = 0x00800000; + + // Maps application contexts + static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>(); + + /** + * Return a MediaRouter for the application that the specified Context belongs to. + * The behavior or availability of media routing may depend on + * various parameters of the context. + * + * @param context Context for the desired router + * @return Router for the supplied Context + */ + public static MediaRouter forApplication(Context context) { + final Context appContext = context.getApplicationContext(); + if (!sRouters.containsKey(appContext)) { + final MediaRouter r = new MediaRouter(appContext); + sRouters.put(appContext, r); + return r; + } else { + return sRouters.get(appContext); + } + } + + static String typesToString(int types) { + final StringBuilder result = new StringBuilder(); + if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) { + result.append("ROUTE_TYPE_LIVE_AUDIO "); + } + if ((types & ROUTE_TYPE_USER) != 0) { + result.append("ROUTE_TYPE_USER "); + } + return result.toString(); + } + + private MediaRouter(Context context) { + mAppContext = context; + mHandler = new Handler(mAppContext.getMainLooper()); + + mAudioManager = (AudioManager) mAppContext.getSystemService(Context.AUDIO_SERVICE); + mSystemCategory = new RouteCategory(mAppContext.getText( + com.android.internal.R.string.default_audio_route_category_name), + ROUTE_TYPE_LIVE_AUDIO, false); + + registerReceivers(); + + createDefaultRoutes(); + } + + private void registerReceivers() { + final BroadcastReceiver volumeReceiver = new VolumeChangedBroadcastReceiver(); + mAppContext.registerReceiver(volumeReceiver, + new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION)); + mRegisteredReceivers.add(volumeReceiver); + + 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); + final BroadcastReceiver plugReceiver = new HeadphoneChangedBroadcastReceiver(); + mAppContext.registerReceiver(plugReceiver, speakerFilter); + mRegisteredReceivers.add(plugReceiver); + } + + void unregisterReceivers() { + final int count = mRegisteredReceivers.size(); + for (int i = 0; i < count; i++) { + final BroadcastReceiver r = mRegisteredReceivers.get(i); + mAppContext.unregisterReceiver(r); + } + mRegisteredReceivers.clear(); + } + + private void createDefaultRoutes() { + mDefaultAudio = new RouteInfo(mSystemCategory); + mDefaultAudio.mName = mAppContext.getText( + com.android.internal.R.string.default_audio_route_name); + mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO; + addRoute(mDefaultAudio); + } + + void onHeadphonesPlugged(boolean headphonesPresent, String headphonesName) { + mDefaultAudio.mName = headphonesPresent ? headphonesName : mAppContext.getText( + com.android.internal.R.string.default_audio_route_name); + dispatchRouteChanged(mDefaultAudio); + } + + /** + * Set volume for the specified route types. + * + * @param types Volume will be set for these route types + * @param volume Volume to set in the range 0.f (inaudible) to 1.f (full volume). + */ + public void setRouteVolume(int types, float volume) { + if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) { + final int index = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, index, 0); + } + if ((types & ROUTE_TYPE_USER) != 0) { + dispatchVolumeChanged(ROUTE_TYPE_USER, volume); + } + } + + /** + * 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 + * additional route types specified. + * + * @param types Types of routes this callback is interested in + * @param cb Callback to add + */ + public void addCallback(int types, Callback cb) { + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + final CallbackInfo info = mCallbacks.get(i); + if (info.cb == cb) { + info.type &= types; + return; + } + } + mCallbacks.add(new CallbackInfo(cb, types)); + } + + /** + * Remove the specified callback. It will no longer receive events about media routing. + * + * @param cb Callback to remove + */ + public void removeCallback(Callback cb) { + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + if (mCallbacks.get(i).cb == cb) { + mCallbacks.remove(i); + return; + } + } + Log.w(TAG, "removeCallback(" + cb + "): callback not registered"); + } + + public void selectRoute(int types, RouteInfo route) { + if (mSelectedRoute == route) return; + + if (mSelectedRoute != null) { + // TODO filter types properly + dispatchRouteUnselected(types & mSelectedRoute.getSupportedTypes(), mSelectedRoute); + } + mSelectedRoute = route; + if (route != null) { + // TODO filter types properly + dispatchRouteSelected(types & route.getSupportedTypes(), route); + } + } + + /** + * Add an app-specified route for media to the MediaRouter. + * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)} + * + * @param info Definition of the route to add + * @see #createUserRoute() + * @see #removeUserRoute(UserRouteInfo) + */ + public void addUserRoute(UserRouteInfo info) { + addRoute(info); + } + + void addRoute(RouteInfo info) { + final RouteCategory cat = info.getCategory(); + if (!mCategories.contains(cat)) { + mCategories.add(cat); + } + if (info.getCategory().isGroupable() && !(info instanceof RouteGroup)) { + // Enforce that any added route in a groupable category must be in a group. + final RouteGroup group = new RouteGroup(info.getCategory()); + group.addRoute(info); + info = group; + } + final boolean onlyRoute = mRoutes.isEmpty(); + mRoutes.add(info); + dispatchRouteAdded(info); + if (onlyRoute) { + selectRoute(info.getSupportedTypes(), info); + } + } + + /** + * Remove an app-specified route for media from the MediaRouter. + * + * @param info Definition of the route to remove + * @see #addUserRoute(UserRouteInfo) + */ + public void removeUserRoute(UserRouteInfo info) { + removeRoute(info); + } + + void removeRoute(RouteInfo info) { + if (mRoutes.remove(info)) { + final RouteCategory removingCat = info.getCategory(); + final int count = mRoutes.size(); + boolean found = false; + for (int i = 0; i < count; i++) { + final RouteCategory cat = mRoutes.get(i).getCategory(); + if (removingCat == cat) { + found = true; + break; + } + } + if (!found) { + mCategories.remove(removingCat); + } + dispatchRouteRemoved(info); + } + } + + /** + * Return the number of {@link MediaRouter.RouteCategory categories} currently + * represented by routes known to this MediaRouter. + * + * @return the number of unique categories represented by this MediaRouter's known routes + */ + public int getCategoryCount() { + return mCategories.size(); + } + + /** + * Return the {@link MediaRouter.RouteCategory category} at the given index. + * Valid indices are in the range [0-getCategoryCount). + * + * @param index which category to return + * @return the category at index + */ + public RouteCategory getCategoryAt(int index) { + return mCategories.get(index); + } + + /** + * Return the number of {@link MediaRouter.RouteInfo routes} currently known + * to this MediaRouter. + * + * @return the number of routes tracked by this router + */ + public int getRouteCount() { + return mRoutes.size(); + } + + /** + * Return the route at the specified index. + * + * @param index index of the route to return + * @return the route at index + */ + public RouteInfo getRouteAt(int index) { + return mRoutes.get(index); + } + + /** + * Create a new user route that may be modified and registered for use by the application. + * + * @param category The category the new route will belong to + * @return A new UserRouteInfo for use by the application + * + * @see #addUserRoute(UserRouteInfo) + * @see #removeUserRoute(UserRouteInfo) + * @see #createRouteCategory(CharSequence) + */ + public UserRouteInfo createUserRoute(RouteCategory category) { + return new UserRouteInfo(category); + } + + /** + * Create a new route category. Each route must belong to a category. + * + * @param name Name of the new category + * @param isGroupable true if routes in this category may be grouped with one another + * @return the new RouteCategory + */ + public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) { + return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable); + } + + void updateRoute(final RouteInfo info) { + dispatchRouteChanged(info); + } + + void dispatchRouteSelected(int type, RouteInfo info) { + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + final CallbackInfo cbi = mCallbacks.get(i); + if ((cbi.type & type) != 0) { + cbi.cb.onRouteSelected(type, info); + } + } + } + + void dispatchRouteUnselected(int type, RouteInfo info) { + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + final CallbackInfo cbi = mCallbacks.get(i); + if ((cbi.type & type) != 0) { + cbi.cb.onRouteUnselected(type, info); + } + } + } + + void dispatchRouteChanged(RouteInfo info) { + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + final CallbackInfo cbi = mCallbacks.get(i); + if ((cbi.type & info.mSupportedTypes) != 0) { + cbi.cb.onRouteChanged(info); + } + } + } + + void dispatchRouteAdded(RouteInfo info) { + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + final CallbackInfo cbi = mCallbacks.get(i); + if ((cbi.type & info.mSupportedTypes) != 0) { + cbi.cb.onRouteAdded(info.mSupportedTypes, info); + } + } + } + + void dispatchRouteRemoved(RouteInfo info) { + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + final CallbackInfo cbi = mCallbacks.get(i); + if ((cbi.type & info.mSupportedTypes) != 0) { + cbi.cb.onRouteRemoved(info.mSupportedTypes, info); + } + } + } + + void dispatchVolumeChanged(int type, float volume) { + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + final CallbackInfo cbi = mCallbacks.get(i); + if ((cbi.type & type) != 0) { + cbi.cb.onVolumeChanged(type, volume); + } + } + } + + void onA2dpDeviceConnected() { + final RouteInfo info = new RouteInfo(mSystemCategory); + info.mName = "Bluetooth"; // TODO Fetch the real name of the device + mBluetoothA2dpRoute = info; + addRoute(mBluetoothA2dpRoute); + } + + void onA2dpDeviceDisconnected() { + removeRoute(mBluetoothA2dpRoute); + mBluetoothA2dpRoute = null; + } + + /** + * Information about a media route. + */ + public class RouteInfo { + CharSequence mName; + private CharSequence mStatus; + int mSupportedTypes; + RouteGroup mGroup; + final RouteCategory mCategory; + + RouteInfo(RouteCategory category) { + mCategory = category; + category.mRoutes.add(this); + } + + /** + * @return The user-friendly name of a media route. This is the string presented + * to users who may select this as the active route. + */ + public CharSequence getName() { + return mName; + } + + /** + * @return The user-friendly status for a media route. This may include a description + * of the currently playing media, if available. + */ + public CharSequence getStatus() { + return mStatus; + } + + /** + * @return A media type flag set describing which types this route supports. + */ + public int getSupportedTypes() { + return mSupportedTypes; + } + + /** + * @return The group that this route belongs to. + */ + public RouteGroup getGroup() { + return mGroup; + } + + /** + * @return the category this route belongs to. + */ + public RouteCategory getCategory() { + return mCategory; + } + + void setStatusInt(CharSequence status) { + if (!status.equals(mStatus)) { + mStatus = status; + routeUpdated(); + if (mGroup != null) { + mGroup.memberStatusChanged(this, status); + } + routeUpdated(); + } + } + + void routeUpdated() { + updateRoute(this); + } + + @Override + public String toString() { + String supportedTypes = typesToString(mSupportedTypes); + return "RouteInfo{ name=" + mName + ", status=" + mStatus + + " category=" + mCategory + + " supportedTypes=" + supportedTypes + "}"; + } + } + + /** + * Information about a route that the application may define and modify. + * + * @see MediaRouter.RouteInfo + */ + public class UserRouteInfo extends RouteInfo { + + UserRouteInfo(RouteCategory category) { + super(category); + mSupportedTypes = ROUTE_TYPE_USER; + } + + /** + * Set the user-visible name of this route. + * @param name Name to display to the user to describe this route + */ + public void setName(CharSequence name) { + mName = name; + routeUpdated(); + } + + /** + * Set the current user-visible status for this route. + * @param status Status to display to the user to describe what the endpoint + * of this route is currently doing + */ + public void setStatus(CharSequence status) { + setStatusInt(status); + } + } + + /** + * Information about a route that consists of multiple other routes in a group. + */ + public class RouteGroup extends RouteInfo { + final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); + private boolean mUpdateName; + + RouteGroup(RouteCategory category) { + super(category); + mGroup = this; + } + + public CharSequence getName() { + if (mUpdateName) updateName(); + return super.getName(); + } + + /** + * Add a route to this group. The route must not currently belong to another group. + * + * @param route route to add to this group + */ + public void addRoute(RouteInfo route) { + if (route.getGroup() != null) { + throw new IllegalStateException("Route " + route + " is already part of a group."); + } + if (route.getCategory() != mCategory) { + throw new IllegalArgumentException( + "Route cannot be added to a group with a different category. " + + "(Route category=" + route.getCategory() + + " group category=" + mCategory + ")"); + } + mRoutes.add(route); + mUpdateName = true; + routeUpdated(); + } + + /** + * Add a route to this group before the specified index. + * + * @param route route to add + * @param insertAt insert the new route before this index + */ + public void addRoute(RouteInfo route, int insertAt) { + if (route.getGroup() != null) { + throw new IllegalStateException("Route " + route + " is already part of a group."); + } + if (route.getCategory() != mCategory) { + throw new IllegalArgumentException( + "Route cannot be added to a group with a different category. " + + "(Route category=" + route.getCategory() + + " group category=" + mCategory + ")"); + } + mRoutes.add(insertAt, route); + mUpdateName = true; + routeUpdated(); + } + + /** + * Remove a route from this group. + * + * @param route route to remove + */ + public void removeRoute(RouteInfo route) { + if (route.getGroup() != this) { + throw new IllegalArgumentException("Route " + route + + " is not a member of this group."); + } + mRoutes.remove(route); + mUpdateName = true; + routeUpdated(); + } + + /** + * Remove the route at the specified index from this group. + * + * @param index index of the route to remove + */ + public void removeRoute(int index) { + mRoutes.remove(index); + mUpdateName = true; + routeUpdated(); + } + + void memberNameChanged(RouteInfo info, CharSequence name) { + mUpdateName = true; + routeUpdated(); + } + + void memberStatusChanged(RouteInfo info, CharSequence status) { + setStatusInt(status); + } + + void updateName() { + final StringBuilder sb = new StringBuilder(); + final int count = mRoutes.size(); + for (int i = 0; i < count; i++) { + final RouteInfo info = mRoutes.get(i); + if (i > 0) sb.append(", "); + sb.append(info.mName); + } + mName = sb.toString(); + mUpdateName = false; + } + } + + /** + * Definition of a category of routes. All routes belong to a category. + */ + public class RouteCategory { + final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); + CharSequence mName; + int mTypes; + final boolean mGroupable; + + RouteCategory(CharSequence name, int types, boolean groupable) { + mName = name; + mTypes = types; + mGroupable = groupable; + } + + /** + * @return the name of this route category + */ + public CharSequence getName() { + return mName; + } + + /** + * @return the number of routes in this category + */ + public int getRouteCount() { + return mRoutes.size(); + } + + /** + * Return a route from this category + * + * @param index Index from [0-getRouteCount) + * @return the route at the given index + */ + public RouteInfo getRouteAt(int index) { + return mRoutes.get(index); + } + + /** + * @return Flag set describing the route types supported by this category + */ + public int getSupportedTypes() { + return mTypes; + } + + /** + * Return whether or not this category supports grouping. + * + * <p>If this method returns true, all routes obtained from this category + * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s. + * + * @return true if this category supports + */ + public boolean isGroupable() { + return mGroupable; + } + + public String toString() { + return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + + " groupable=" + mGroupable + " routes=" + mRoutes.size() + " }"; + } + } + + static class CallbackInfo { + public int type; + public Callback cb; + + public CallbackInfo(Callback cb, int type) { + this.cb = cb; + this.type = type; + } + } + + /** + * Interface for receiving events about media routing changes. + * All methods of this interface will be called from the application's main thread. + * + * <p>A Callback will only receive events relevant to routes that the callback + * was registered for.</p> + * + * @see MediaRouter#addCallback(int, Callback) + * @see MediaRouter#removeCallback(Callback) + */ + public interface Callback { + /** + * Called when the supplied route becomes selected as the active route + * for the given route type. + * + * @param type Type flag set indicating the routes that have been selected + * @param info Route that has been selected for the given route types + */ + public void onRouteSelected(int type, RouteInfo info); + + /** + * Called when the supplied route becomes unselected as the active route + * for the given route type. + * + * @param type Type flag set indicating the routes that have been unselected + * @param info Route that has been unselected for the given route types + */ + public void onRouteUnselected(int type, RouteInfo info); + + /** + * Called when the volume is changed for the specified route types. + * + * @param type Type flags indicating which volume type was changed + * @param volume New volume value in the range 0 (inaudible) to 1 (full) + */ + public void onVolumeChanged(int type, float volume); + + /** + * Called when a route for the specified type was added. + * + * @param type Type flags indicating which types the added route supports + * @param info Route that has become available for use + */ + public void onRouteAdded(int type, RouteInfo info); + + /** + * Called when a route for the specified type was removed. + * + * @param type Type flags indicating which types the removed route supported + * @param info Route that has been removed from availability + */ + public void onRouteRemoved(int type, RouteInfo info); + + /** + * Called when an aspect of the indicated route has changed. + * + * <p>This will not indicate that the types supported by this route have + * changed, only that cosmetic info such as name or status have been updated.</p> + * + * @param info The route that was changed + */ + public void onRouteChanged(RouteInfo info); + } + + /** + * Stub implementation of the {@link MediaRouter.Callback} interface. + * Each interface method is defined as a no-op. Override just the ones + * you need. + */ + public static class SimpleCallback implements Callback { + + @Override + public void onRouteSelected(int type, RouteInfo info) { + + } + + @Override + public void onRouteUnselected(int type, RouteInfo info) { + + } + + @Override + public void onVolumeChanged(int type, float volume) { + + } + + @Override + public void onRouteAdded(int type, RouteInfo info) { + + } + + @Override + public void onRouteRemoved(int type, RouteInfo info) { + + } + + @Override + public void onRouteChanged(RouteInfo info) { + + } + + } + + class VolumeChangedBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (AudioManager.VOLUME_CHANGED_ACTION.equals(action) && + AudioManager.STREAM_MUSIC == intent.getIntExtra( + AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1)) { + final int maxVol = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + final int volExtra = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); + final float volume = (float) volExtra / maxVol; + dispatchVolumeChanged(ROUTE_TYPE_LIVE_AUDIO, volume); + } + } + } + + 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(); + } + } + } + } + + 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 = mAppContext.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 = mAppContext.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 = mAppContext.getString( + com.android.internal.R.string.default_audio_route_name_hdmi); + onHeadphonesPlugged(plugged, name); + } + } + } +} |
