Expand volume APIs for MediaRouter

Allow applications to set a requested volume level on RouteInfo
objects. If requested for a user route, the app-supplied callback will
be invoked to perform actual volume adjustment.

Change-Id: I856990a0da7292492aa15e6562dbc3d055b848a0
diff --git a/api/current.txt b/api/current.txt
index 3069221..f25b11e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -11538,6 +11538,7 @@
     method public abstract void onRouteSelected(android.media.MediaRouter, int, android.media.MediaRouter.RouteInfo);
     method public abstract void onRouteUngrouped(android.media.MediaRouter, android.media.MediaRouter.RouteInfo, android.media.MediaRouter.RouteGroup);
     method public abstract void onRouteUnselected(android.media.MediaRouter, int, android.media.MediaRouter.RouteInfo);
+    method public abstract void onRouteVolumeChanged(android.media.MediaRouter, android.media.MediaRouter.RouteInfo);
   }
 
   public static class MediaRouter.RouteCategory {
@@ -11573,6 +11574,8 @@
     method public int getVolume();
     method public int getVolumeHandling();
     method public int getVolumeMax();
+    method public void requestSetVolume(int);
+    method public void requestUpdateVolume(int);
     method public void setTag(java.lang.Object);
     field public static final int PLAYBACK_TYPE_LOCAL = 0; // 0x0
     field public static final int PLAYBACK_TYPE_REMOTE = 1; // 0x1
@@ -11589,6 +11592,7 @@
     method public void onRouteSelected(android.media.MediaRouter, int, android.media.MediaRouter.RouteInfo);
     method public void onRouteUngrouped(android.media.MediaRouter, android.media.MediaRouter.RouteInfo, android.media.MediaRouter.RouteGroup);
     method public void onRouteUnselected(android.media.MediaRouter, int, android.media.MediaRouter.RouteInfo);
+    method public void onRouteVolumeChanged(android.media.MediaRouter, android.media.MediaRouter.RouteInfo);
   }
 
   public static class MediaRouter.UserRouteInfo extends android.media.MediaRouter.RouteInfo {
diff --git a/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java b/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java
index b615b9c..b747694 100644
--- a/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java
+++ b/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java
@@ -30,8 +30,6 @@
 import android.media.MediaRouter.RouteCategory;
 import android.media.MediaRouter.RouteGroup;
 import android.media.MediaRouter.RouteInfo;
-import android.media.MediaRouter.UserRouteInfo;
-import android.media.RemoteControlClient;
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.view.KeyEvent;
@@ -85,7 +83,8 @@
 
     final RouteComparator mComparator = new RouteComparator();
     final MediaRouterCallback mCallback = new MediaRouterCallback();
-    private boolean mIgnoreVolumeChanges;
+    private boolean mIgnoreSliderVolumeChanges;
+    private boolean mIgnoreCallbackVolumeChanges;
 
     public MediaRouteChooserDialogFragment() {
         setStyle(STYLE_NO_TITLE, R.style.Theme_DeviceDefault_Dialog);
@@ -126,52 +125,34 @@
 
     void updateVolume() {
         final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
-        final boolean defaultAudioSelected = selectedRoute == mRouter.getSystemAudioRoute();
-        final boolean selectedSystemRoute =
-                selectedRoute.getCategory() == mRouter.getSystemAudioCategory();
-        mVolumeIcon.setImageResource(defaultAudioSelected ?
+        mVolumeIcon.setImageResource(
+                selectedRoute.getPlaybackType() == RouteInfo.PLAYBACK_TYPE_LOCAL ?
                 R.drawable.ic_audio_vol : R.drawable.ic_media_route_on_holo_dark);
 
-        mIgnoreVolumeChanges = true;
-        mVolumeSlider.setEnabled(true);
-        if (selectedSystemRoute) {
-            // Use the standard media audio stream
-            mVolumeSlider.setMax(mAudio.getStreamMaxVolume(AudioManager.STREAM_MUSIC));
-            mVolumeSlider.setProgress(mAudio.getStreamVolume(AudioManager.STREAM_MUSIC));
+        mIgnoreSliderVolumeChanges = true;
+
+        if (selectedRoute.getVolumeHandling() == RouteInfo.PLAYBACK_VOLUME_FIXED) {
+            // Disable the slider and show it at max volume.
+            mVolumeSlider.setMax(1);
+            mVolumeSlider.setProgress(1);
+            mVolumeSlider.setEnabled(false);
         } else {
-            final RouteInfo firstSelected;
-            if (selectedRoute instanceof RouteGroup) {
-                firstSelected = ((RouteGroup) selectedRoute).getRouteAt(0);
-            } else {
-                firstSelected = selectedRoute;
-            }
-
-            RemoteControlClient rcc = null;
-            if (firstSelected instanceof UserRouteInfo) {
-                rcc = ((UserRouteInfo) firstSelected).getRemoteControlClient();
-            }
-
-            if (rcc == null) {
-                // No RemoteControlClient? Assume volume can't be controlled.
-                // Disable the slider and show it at max volume.
-                mVolumeSlider.setMax(1);
-                mVolumeSlider.setProgress(1);
-                mVolumeSlider.setEnabled(false);
-            } else {
-                // TODO: Connect this to the remote control volume
-            }
+            mVolumeSlider.setEnabled(true);
+            mVolumeSlider.setMax(selectedRoute.getVolumeMax());
+            mVolumeSlider.setProgress(selectedRoute.getVolume());
         }
-        mIgnoreVolumeChanges = false;
+
+        mIgnoreSliderVolumeChanges = false;
     }
 
     void changeVolume(int newValue) {
-        if (mIgnoreVolumeChanges) return;
+        if (mIgnoreSliderVolumeChanges) return;
 
-        RouteCategory selectedCategory = mRouter.getSelectedRoute(mRouteTypes).getCategory();
-        if (selectedCategory == mRouter.getSystemAudioCategory()) {
+        final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
+        if (selectedRoute.getVolumeHandling() == RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
             final int maxVolume = mAudio.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
             newValue = Math.max(0, Math.min(newValue, maxVolume));
-            mAudio.setStreamVolume(AudioManager.STREAM_MUSIC, newValue, 0);
+            selectedRoute.requestSetVolume(newValue);
         }
     }
 
@@ -595,7 +576,6 @@
         @Override
         public void onRouteAdded(MediaRouter router, RouteInfo info) {
             mAdapter.update();
-            updateVolume();
         }
 
         @Override
@@ -604,7 +584,6 @@
                 mAdapter.finishGrouping();
             }
             mAdapter.update();
-            updateVolume();
         }
 
         @Override
@@ -622,6 +601,13 @@
         public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
             mAdapter.update();
         }
+
+        @Override
+        public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) {
+            if (!mIgnoreCallbackVolumeChanges) {
+                updateVolume();
+            }
+        }
     }
 
     class RouteComparator implements Comparator<RouteInfo> {
@@ -648,15 +634,25 @@
         
         public boolean onKeyDown(int keyCode, KeyEvent event) {
             if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && mVolumeSlider.isEnabled()) {
-                mVolumeSlider.incrementProgressBy(-1);
+                mRouter.getSelectedRoute(mRouteTypes).requestUpdateVolume(-1);
                 return true;
             } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mVolumeSlider.isEnabled()) {
-                mVolumeSlider.incrementProgressBy(1);
+                mRouter.getSelectedRoute(mRouteTypes).requestUpdateVolume(1);
                 return true;
             } else {
                 return super.onKeyDown(keyCode, event);
             }
         }
+
+        public boolean onKeyUp(int keyCode, KeyEvent event) {
+            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && mVolumeSlider.isEnabled()) {
+                return true;
+            } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mVolumeSlider.isEnabled()) {
+                return true;
+            } else {
+                return super.onKeyUp(keyCode, event);
+            }
+        }
     }
 
     /**
@@ -675,10 +671,13 @@
 
         @Override
         public void onStartTrackingTouch(SeekBar seekBar) {
+            mIgnoreCallbackVolumeChanges = true;
         }
 
         @Override
         public void onStopTrackingTouch(SeekBar seekBar) {
+            mIgnoreCallbackVolumeChanges = false;
+            updateVolume();
         }
 
     }
diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java
index b6187da..3de2db2 100644
--- a/media/java/android/media/MediaRouter.java
+++ b/media/java/android/media/MediaRouter.java
@@ -16,7 +16,10 @@
 
 package android.media;
 
+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;
@@ -87,12 +90,15 @@
         }
 
         // Called after sStatic is initialized
-        void startMonitoringRoutes() {
+        void startMonitoringRoutes(Context appContext) {
             mDefaultAudio = new RouteInfo(mSystemCategory);
             mDefaultAudio.mNameResId = com.android.internal.R.string.default_audio_route_name;
             mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
             addRoute(mDefaultAudio);
 
+            appContext.registerReceiver(new VolumeChangeReceiver(),
+                    new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
+
             AudioRoutesInfo newRoutes = null;
             try {
                 newRoutes = mAudioService.startWatchingRoutes(mRoutesObserver);
@@ -190,8 +196,9 @@
     public MediaRouter(Context context) {
         synchronized (Static.class) {
             if (sStatic == null) {
-                sStatic = new Static(context.getApplicationContext());
-                sStatic.startMonitoringRoutes();
+                final Context appContext = context.getApplicationContext();
+                sStatic = new Static(appContext);
+                sStatic.startMonitoringRoutes(appContext);
             }
         }
     }
@@ -578,6 +585,33 @@
         }
     }
 
+    static void dispatchRouteVolumeChanged(RouteInfo info) {
+        for (CallbackInfo cbi : sStatic.mCallbacks) {
+            if ((cbi.type & info.mSupportedTypes) != 0) {
+                cbi.cb.onRouteVolumeChanged(cbi.router, info);
+            }
+        }
+    }
+
+    static void systemVolumeChanged(int newValue) {
+        final RouteInfo selectedRoute = sStatic.mSelectedRoute;
+        if (selectedRoute == null) return;
+
+        if (selectedRoute == sStatic.mBluetoothA2dpRoute ||
+                selectedRoute == sStatic.mDefaultAudio) {
+            dispatchRouteVolumeChanged(selectedRoute);
+        } else if (sStatic.mBluetoothA2dpRoute != null) {
+            try {
+                dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ?
+                        sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudio);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e);
+            }
+        } else {
+            dispatchRouteVolumeChanged(sStatic.mDefaultAudio);
+        }
+    }
+
     /**
      * Information about a media route.
      */
@@ -735,6 +769,9 @@
         }
 
         /**
+         * Return the current volume for this route. Depending on the route, this may only
+         * be valid if the route is currently selected.
+         *
          * @return the volume at which the playback associated with this route is performed
          * @see UserRouteInfo#setVolume(int)
          */
@@ -753,6 +790,44 @@
         }
 
         /**
+         * Request a volume change for this route.
+         * @param volume value between 0 and getVolumeMax
+         */
+        public void requestSetVolume(int volume) {
+            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
+                try {
+                    sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error setting local stream volume", e);
+                }
+            } else {
+                Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " +
+                        "Non-local volume playback on system route? " +
+                        "Could not request volume change.");
+            }
+        }
+
+        /**
+         * Request an incremental volume update for this route.
+         * @param direction Delta to apply to the current volume
+         */
+        public void requestUpdateVolume(int direction) {
+            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
+                try {
+                    final int volume =
+                            Math.max(0, Math.min(getVolume() + direction, getVolumeMax()));
+                    sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error setting local stream volume", e);
+                }
+            } else {
+                Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " +
+                        "Non-local volume playback on system route? " +
+                        "Could not request volume change.");
+            }
+        }
+
+        /**
          * @return the maximum volume at which the playback associated with this route is performed
          * @see UserRouteInfo#setVolumeMax(int)
          */
@@ -821,6 +896,8 @@
 
     /**
      * Information about a route that the application may define and modify.
+     * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and
+     * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}.
      *
      * @see MediaRouter.RouteInfo
      */
@@ -830,6 +907,8 @@
         UserRouteInfo(RouteCategory category) {
             super(category);
             mSupportedTypes = ROUTE_TYPE_USER;
+            mPlaybackType = PLAYBACK_TYPE_REMOTE;
+            mVolumeHandling = PLAYBACK_VOLUME_FIXED;
         }
 
         /**
@@ -949,9 +1028,33 @@
          * @param volume
          */
         public void setVolume(int volume) {
+            volume = Math.max(0, Math.min(volume, getVolumeMax()));
             if (mVolume != volume) {
                 mVolume = volume;
                 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume);
+                dispatchRouteVolumeChanged(this);
+            }
+        }
+
+        @Override
+        public void requestSetVolume(int volume) {
+            if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
+                if (mVcb == null) {
+                    Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set");
+                    return;
+                }
+                mVcb.vcb.onVolumeSetRequest(this, volume);
+            }
+        }
+
+        @Override
+        public void requestUpdateVolume(int direction) {
+            if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
+                if (mVcb == null) {
+                    Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set");
+                    return;
+                }
+                mVcb.vcb.onVolumeUpdateRequest(this, direction);
             }
         }
 
@@ -1018,6 +1121,7 @@
         RouteGroup(RouteCategory category) {
             super(category);
             mGroup = this;
+            mVolumeHandling = PLAYBACK_VOLUME_FIXED;
         }
 
         CharSequence getName(Resources res) {
@@ -1138,6 +1242,45 @@
             setIconDrawable(sStatic.mResources.getDrawable(resId));
         }
 
+        @Override
+        public void requestSetVolume(int volume) {
+            final int maxVol = getVolumeMax();
+            if (maxVol == 0) {
+                return;
+            }
+
+            final float scaledVolume = (float) volume / maxVol;
+            final int routeCount = getRouteCount();
+            for (int i = 0; i < routeCount; i++) {
+                final RouteInfo route = getRouteAt(i);
+                final int routeVol = (int) (scaledVolume * route.getVolumeMax());
+                route.requestSetVolume(routeVol);
+            }
+            if (volume != mVolume) {
+                mVolume = volume;
+                dispatchRouteVolumeChanged(this);
+            }
+        }
+
+        @Override
+        public void requestUpdateVolume(int direction) {
+            final int maxVol = getVolumeMax();
+            if (maxVol == 0) {
+                return;
+            }
+
+            final int routeCount = getRouteCount();
+            for (int i = 0; i < routeCount; i++) {
+                final RouteInfo route = getRouteAt(i);
+                route.requestUpdateVolume(direction);
+            }
+            final int volume = Math.max(0, Math.min(mVolume + direction, maxVol));
+            if (volume != mVolume) {
+                mVolume = volume;
+                dispatchRouteVolumeChanged(this);
+            }
+        }
+
         void memberNameChanged(RouteInfo info, CharSequence name) {
             mUpdateName = true;
             routeUpdated();
@@ -1157,10 +1300,23 @@
                 return;
             }
 
+            int maxVolume = 0;
+            boolean isLocal = true;
+            boolean isFixedVolume = true;
             for (int i = 0; i < count; i++) {
-                types |= mRoutes.get(i).mSupportedTypes;
+                final RouteInfo route = mRoutes.get(i);
+                types |= route.mSupportedTypes;
+                final int routeMaxVolume = route.getVolumeMax();
+                if (routeMaxVolume > maxVolume) {
+                    maxVolume = routeMaxVolume;
+                }
+                isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL;
+                isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED;
             }
+            mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE;
+            mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE;
             mSupportedTypes = types;
+            mVolumeMax = maxVolume;
             mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
             super.routeUpdated();
         }
@@ -1381,6 +1537,14 @@
          * @param group The group the route was removed from
          */
         public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
+
+        /**
+         * Called when a route's volume changes.
+         *
+         * @param router the MediaRouter reporting the event
+         * @param info The route with altered volume
+         */
+        public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info);
     }
 
     /**
@@ -1419,6 +1583,9 @@
         public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
         }
 
+        @Override
+        public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) {
+        }
     }
 
     static class VolumeCallbackInfo {
@@ -1459,4 +1626,25 @@
         public abstract void onVolumeSetRequest(RouteInfo info, int volume);
     }
 
+    static class VolumeChangeReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) {
+                final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+                        -1);
+                if (streamType != AudioManager.STREAM_MUSIC) {
+                    return;
+                }
+
+                final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
+                final int oldVolume = intent.getIntExtra(
+                        AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
+                if (newVolume != oldVolume) {
+                    systemVolumeChanged(newVolume);
+                }
+            }
+        }
+
+    }
 }