Minimum work to make volume handling work with sessions

This is the minimum change to make adjusting volume work with
MediaSessions. This only affects adjusting the volume and adjusting
the volume with a suggested stream. Adjusting a specific stream or
setting a specific stream will still use the same code.

This does not fix existing remote volume handling in RCC, which
will require a separate change to MediaController.

Change-Id: I5b957ff4bece1ee11e2364e1f216e1c08343c983
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index f4affa0..215615f 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -635,7 +635,12 @@
             if (mUseMasterVolume) {
                 service.adjustMasterVolume(direction, flags, mContext.getOpPackageName());
             } else {
-                service.adjustVolume(direction, flags, mContext.getOpPackageName());
+                if (USE_SESSIONS) {
+                    MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext);
+                    helper.sendAdjustVolumeBy(USE_DEFAULT_STREAM_TYPE, direction, flags);
+                } else {
+                    service.adjustVolume(direction, flags, mContext.getOpPackageName());
+                }
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Dead object in adjustVolume", e);
@@ -665,8 +670,13 @@
             if (mUseMasterVolume) {
                 service.adjustMasterVolume(direction, flags, mContext.getOpPackageName());
             } else {
-                service.adjustSuggestedStreamVolume(direction, suggestedStreamType, flags,
-                        mContext.getOpPackageName());
+                if (USE_SESSIONS) {
+                    MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext);
+                    helper.sendAdjustVolumeBy(suggestedStreamType, direction, flags);
+                } else {
+                    service.adjustSuggestedStreamVolume(direction, suggestedStreamType, flags,
+                            mContext.getOpPackageName());
+                }
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Dead object in adjustSuggestedStreamVolume", e);
diff --git a/media/java/android/media/session/ISession.aidl b/media/java/android/media/session/ISession.aidl
index c4233c3..1cfc5bc 100644
--- a/media/java/android/media/session/ISession.aidl
+++ b/media/java/android/media/session/ISession.aidl
@@ -47,4 +47,8 @@
     void setMetadata(in MediaMetadata metadata);
     void setPlaybackState(in PlaybackState state);
     void setRatingType(int type);
+
+    // These commands relate to volume handling
+    void configureVolumeHandling(int type, int arg1, int arg2);
+    void setCurrentVolume(int currentVolume);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/ISessionCallback.aidl b/media/java/android/media/session/ISessionCallback.aidl
index 103c3f1..0316d1fa 100644
--- a/media/java/android/media/session/ISessionCallback.aidl
+++ b/media/java/android/media/session/ISessionCallback.aidl
@@ -45,4 +45,8 @@
     void onRewind();
     void onSeekTo(long pos);
     void onRate(in Rating rating);
+
+    // These callbacks are for volume handling
+    void onAdjustVolumeBy(int delta);
+    void onSetVolumeTo(int value);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl
index 38b9293..6d9888f 100644
--- a/media/java/android/media/session/ISessionManager.aidl
+++ b/media/java/android/media/session/ISessionManager.aidl
@@ -29,4 +29,5 @@
     ISession createSession(String packageName, in ISessionCallback cb, String tag, int userId);
     List<IBinder> getSessions(in ComponentName compName, int userId);
     void dispatchMediaKeyEvent(in KeyEvent keyEvent, boolean needWakeLock);
+    void dispatchAdjustVolumeBy(int suggestedStream, int delta, int flags);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index 90ccf68..7972639 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -124,9 +124,21 @@
      */
     public static final int DISCONNECT_REASON_SESSION_DESTROYED = 5;
 
-    private static final String KEY_COMMAND = "command";
-    private static final String KEY_EXTRAS = "extras";
-    private static final String KEY_CALLBACK = "callback";
+    /**
+     * The session uses local playback. Used for configuring volume handling
+     * with the system.
+     *
+     * @hide
+     */
+    public static final int VOLUME_TYPE_LOCAL = 1;
+
+    /**
+     * The session uses remote playback. Used for configuring volume handling
+     * with the system.
+     *
+     * @hide
+     */
+    public static final int VOLUME_TYPE_REMOTE = 2;
 
     private final Object mLock = new Object();
 
@@ -143,6 +155,7 @@
             = new ArrayMap<String, RouteInterface.EventListener>();
 
     private Route mRoute;
+    private RemoteVolumeProvider mVolumeProvider;
 
     private boolean mActive = false;;
 
@@ -242,7 +255,11 @@
      * @param stream The {@link AudioManager} stream this session is playing on.
      */
     public void setPlaybackToLocal(int stream) {
-        // TODO
+        try {
+            mBinder.configureVolumeHandling(VOLUME_TYPE_LOCAL, stream, 0);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Failure in setPlaybackToLocal.", e);
+        }
     }
 
     /**
@@ -259,7 +276,14 @@
         if (volumeProvider == null) {
             throw new IllegalArgumentException("volumeProvider may not be null!");
         }
-        // TODO
+        mVolumeProvider = volumeProvider;
+
+        try {
+            mBinder.configureVolumeHandling(VOLUME_TYPE_REMOTE, volumeProvider.getVolumeControl(),
+                    volumeProvider.getMaxVolume());
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Failure in setPlaybackToRemote.", e);
+        }
     }
 
     /**
@@ -942,6 +966,26 @@
 
         }
 
+        /*
+         * (non-Javadoc)
+         * @see android.media.session.ISessionCallback#onAdjustVolumeBy(int)
+         */
+        @Override
+        public void onAdjustVolumeBy(int delta) throws RemoteException {
+            // TODO(epastern): Auto-generated method stub
+
+        }
+
+        /*
+         * (non-Javadoc)
+         * @see android.media.session.ISessionCallback#onSetVolumeTo(int)
+         */
+        @Override
+        public void onSetVolumeTo(int value) throws RemoteException {
+            // TODO(epastern): Auto-generated method stub
+
+        }
+
     }
 
     private class CallbackMessageHandler extends Handler {
diff --git a/media/java/android/media/session/MediaSessionLegacyHelper.java b/media/java/android/media/session/MediaSessionLegacyHelper.java
index c303e77..099f601 100644
--- a/media/java/android/media/session/MediaSessionLegacyHelper.java
+++ b/media/java/android/media/session/MediaSessionLegacyHelper.java
@@ -76,6 +76,13 @@
         }
     }
 
+    public void sendAdjustVolumeBy(int suggestedStream, int delta, int flags) {
+        mSessionManager.dispatchAdjustVolumeBy(suggestedStream, delta, flags);
+        if (DEBUG) {
+            Log.d(TAG, "dispatched volume adjustment");
+        }
+    }
+
     public void addRccListener(PendingIntent pi,
             MediaSession.TransportControlsCallback listener) {
         if (pi == null) {
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index 8d5e338..9e8b0d3 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -166,4 +166,22 @@
             Log.e(TAG, "Failed to send key event.", e);
         }
     }
+
+    /**
+     * Dispatch an adjust volume request to the system. It will be routed to the
+     * most relevant stream/session.
+     *
+     * @param suggestedStream The stream to fall back to if there isn't a
+     *            relevant stream
+     * @param delta The amount to adjust the volume by.
+     * @param flags Any flags to include with the volume change.
+     * @hide
+     */
+    public void dispatchAdjustVolumeBy(int suggestedStream, int delta, int flags) {
+        try {
+            mService.dispatchAdjustVolumeBy(suggestedStream, delta, flags);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to send adjust volume.", e);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index c909a54..737ffda 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -25,6 +25,7 @@
 import android.media.session.ISession;
 import android.media.session.ISessionCallback;
 import android.media.session.MediaController;
+import android.media.session.RemoteVolumeProvider;
 import android.media.session.RouteCommand;
 import android.media.session.RouteInfo;
 import android.media.session.RouteOptions;
@@ -33,6 +34,7 @@
 import android.media.session.MediaSessionInfo;
 import android.media.session.RouteInterface;
 import android.media.session.PlaybackState;
+import android.media.AudioManager;
 import android.media.MediaMetadata;
 import android.media.Rating;
 import android.os.Bundle;
@@ -112,6 +114,14 @@
     private long mLastActiveTime;
     // End TransportPerformer fields
 
+    // Volume handling fields
+    private int mPlaybackType = MediaSession.VOLUME_TYPE_LOCAL;
+    private int mAudioStream = AudioManager.STREAM_MUSIC;
+    private int mVolumeControlType = RemoteVolumeProvider.VOLUME_CONTROL_ABSOLUTE;
+    private int mMaxVolume = 0;
+    private int mCurrentVolume = 0;
+    // End volume handling fields
+
     private boolean mIsActive = false;
     private boolean mDestroyed = false;
 
@@ -248,6 +258,27 @@
     }
 
     /**
+     * Send a volume adjustment to the session owner.
+     *
+     * @param delta The amount to adjust the volume by.
+     */
+    public void adjustVolumeBy(int delta) {
+        if (mVolumeControlType == RemoteVolumeProvider.VOLUME_CONTROL_FIXED) {
+            // Nothing to do, the volume cannot be changed
+            return;
+        }
+        mSessionCb.adjustVolumeBy(delta);
+    }
+
+    public void setVolumeTo(int value) {
+        if (mVolumeControlType != RemoteVolumeProvider.VOLUME_CONTROL_ABSOLUTE) {
+            // Nothing to do. The volume can't be set directly.
+            return;
+        }
+        mSessionCb.setVolumeTo(value);
+    }
+
+    /**
      * Set the connection to use for the selected route and notify the app it is
      * now connected.
      *
@@ -294,14 +325,16 @@
      * Check if the session is currently performing playback. This will also
      * return true if the session was recently paused.
      *
+     * @param includeRecentlyActive True if playback that was recently paused
+     *            should count, false if it shouldn't.
      * @return True if the session is performing playback, false otherwise.
      */
-    public boolean isPlaybackActive() {
+    public boolean isPlaybackActive(boolean includeRecentlyActive) {
         int state = mPlaybackState == null ? 0 : mPlaybackState.getState();
         if (isActiveState(state)) {
             return true;
         }
-        if (state == mPlaybackState.STATE_PAUSED) {
+        if (includeRecentlyActive && state == mPlaybackState.STATE_PAUSED) {
             long inactiveTime = SystemClock.uptimeMillis() - mLastActiveTime;
             if (inactiveTime < ACTIVE_BUFFER) {
                 return true;
@@ -311,6 +344,54 @@
     }
 
     /**
+     * Get the type of playback, either local or remote.
+     *
+     * @return The current type of playback.
+     */
+    public int getPlaybackType() {
+        return mPlaybackType;
+    }
+
+    /**
+     * Get the local audio stream being used. Only valid if playback type is
+     * local.
+     *
+     * @return The audio stream the session is using.
+     */
+    public int getAudioStream() {
+        return mAudioStream;
+    }
+
+    /**
+     * Get the type of volume control. Only valid if playback type is remote.
+     *
+     * @return The volume control type being used.
+     */
+    public int getVolumeControl() {
+        return mVolumeControlType;
+    }
+
+    /**
+     * Get the max volume that can be set. Only valid if playback type is
+     * remote.
+     *
+     * @return The max volume that can be set.
+     */
+    public int getMaxVolume() {
+        return mMaxVolume;
+    }
+
+    /**
+     * Get the current volume for this session. Only valid if playback type is
+     * remote.
+     *
+     * @return The current volume of the remote playback.
+     */
+    public int getCurrentVolume() {
+        return mCurrentVolume;
+    }
+
+    /**
      * @return True if this session is currently connected to a route.
      */
     public boolean isConnected() {
@@ -640,6 +721,40 @@
                 mRequests.add(request);
             }
         }
+
+        @Override
+        public void setCurrentVolume(int volume) {
+            mCurrentVolume = volume;
+        }
+
+        @Override
+        public void configureVolumeHandling(int type, int arg1, int arg2) throws RemoteException {
+            switch(type) {
+                case MediaSession.VOLUME_TYPE_LOCAL:
+                    mPlaybackType = type;
+                    int audioStream = arg1;
+                    if (isValidStream(audioStream)) {
+                        mAudioStream = audioStream;
+                    } else {
+                        Log.e(TAG, "Cannot set stream to " + audioStream + ". Using music stream");
+                        mAudioStream = AudioManager.STREAM_MUSIC;
+                    }
+                    break;
+                case MediaSession.VOLUME_TYPE_REMOTE:
+                    mPlaybackType = type;
+                    mVolumeControlType = arg1;
+                    mMaxVolume = arg2;
+                    break;
+                default:
+                    throw new IllegalArgumentException("Volume handling type " + type
+                            + " not recognized.");
+            }
+        }
+
+        private boolean isValidStream(int stream) {
+            return stream >= AudioManager.STREAM_VOICE_CALL
+                    && stream <= AudioManager.STREAM_NOTIFICATION;
+        }
     }
 
     class SessionCb {
@@ -780,6 +895,22 @@
                 Slog.e(TAG, "Remote failure in rate.", e);
             }
         }
+
+        public void adjustVolumeBy(int delta) {
+            try {
+                mCb.onAdjustVolumeBy(delta);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in adjustVolumeBy.", e);
+            }
+        }
+
+        public void setVolumeTo(int value) {
+            try {
+                mCb.onSetVolumeTo(value);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in adjustVolumeBy.", e);
+            }
+        }
     }
 
     class ControllerStub extends ISessionController.Stub {
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 9d85167..87665e1 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -26,6 +26,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.media.AudioManager;
+import android.media.IAudioService;
 import android.media.routeprovider.RouteRequest;
 import android.media.session.ISession;
 import android.media.session.ISessionCallback;
@@ -40,6 +42,7 @@
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.speech.RecognizerIntent;
@@ -79,6 +82,7 @@
     private final PowerManager.WakeLock mMediaEventWakeLock;
 
     private KeyguardManager mKeyguardManager;
+    private IAudioService mAudioService;
 
     private MediaSessionRecord mPrioritySession;
     private int mCurrentUserId = -1;
@@ -105,6 +109,12 @@
         updateUser();
         mKeyguardManager =
                 (KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE);
+        mAudioService = getAudioService();
+    }
+
+    private IAudioService getAudioService() {
+        IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
+        return IAudioService.Stub.asInterface(b);
     }
 
     /**
@@ -703,6 +713,23 @@
         }
 
         @Override
+        public void dispatchAdjustVolumeBy(int suggestedStream, int delta, int flags)
+                throws RemoteException {
+            final int pid = Binder.getCallingPid();
+            final int uid = Binder.getCallingUid();
+            final long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    MediaSessionRecord session = mPriorityStack
+                            .getDefaultVolumeSession(mCurrentUserId);
+                    dispatchAdjustVolumeByLocked(suggestedStream, delta, flags, session);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
         public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
             if (getContext().checkCallingOrSelfPermission(Manifest.permission.DUMP)
                     != PackageManager.PERMISSION_GRANTED) {
@@ -737,6 +764,49 @@
             }
         }
 
+        private void dispatchAdjustVolumeByLocked(int suggestedStream, int delta, int flags,
+                MediaSessionRecord session) {
+            int direction = 0;
+            int steps = delta;
+            if (delta > 0) {
+                direction = 1;
+            } else if (delta < 0) {
+                direction = -1;
+                steps = -delta;
+            }
+            if (DEBUG) {
+                String sessionInfo = session == null ? null : session.getSessionInfo().toString();
+                Log.d(TAG, "Adjusting session " + sessionInfo + " by " + delta + ". flags=" + flags
+                        + ", suggestedStream=" + suggestedStream);
+
+            }
+            if (session == null) {
+                for (int i = 0; i < steps; i++) {
+                    try {
+                        mAudioService.adjustSuggestedStreamVolume(direction, suggestedStream,
+                                flags, getContext().getOpPackageName());
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "Error adjusting default volume.", e);
+                    }
+                }
+            } else {
+                if (session.getPlaybackType() == MediaSession.VOLUME_TYPE_LOCAL) {
+                    for (int i = 0; i < steps; i++) {
+                        try {
+                            mAudioService.adjustSuggestedStreamVolume(direction,
+                                    session.getAudioStream(), flags,
+                                    getContext().getOpPackageName());
+                        } catch (RemoteException e) {
+                            Log.e(TAG, "Error adjusting volume for stream "
+                                    + session.getAudioStream(), e);
+                        }
+                    }
+                } else if (session.getPlaybackType() == MediaSession.VOLUME_TYPE_REMOTE) {
+                    session.adjustVolumeBy(delta);
+                }
+            }
+        }
+
         private void handleVoiceKeyEventLocked(KeyEvent keyEvent, boolean needWakeLock,
                 MediaSessionRecord session) {
             if (session != null && session.hasFlag(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY)) {
diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java
index 56236f8..803dee2 100644
--- a/services/core/java/com/android/server/media/MediaSessionStack.java
+++ b/services/core/java/com/android/server/media/MediaSessionStack.java
@@ -52,6 +52,7 @@
 
     private MediaSessionRecord mCachedButtonReceiver;
     private MediaSessionRecord mCachedDefault;
+    private MediaSessionRecord mCachedVolumeDefault;
     private ArrayList<MediaSessionRecord> mCachedActiveList;
     private ArrayList<MediaSessionRecord> mCachedTransportControlList;
 
@@ -93,6 +94,9 @@
             mSessions.remove(record);
             mSessions.add(0, record);
             clearCache();
+        } else if (newState == PlaybackState.STATE_PAUSED) {
+            // Just clear the volume cache in this case
+            mCachedVolumeDefault = null;
         }
     }
 
@@ -177,6 +181,25 @@
         return mCachedButtonReceiver;
     }
 
+    public MediaSessionRecord getDefaultVolumeSession(int userId) {
+        if (mGlobalPrioritySession != null && mGlobalPrioritySession.isActive()) {
+            return mGlobalPrioritySession;
+        }
+        if (mCachedVolumeDefault != null) {
+            return mCachedVolumeDefault;
+        }
+        ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId);
+        int size = records.size();
+        for (int i = 0; i < size; i++) {
+            MediaSessionRecord record = records.get(i);
+            if (record.isPlaybackActive(false)) {
+                mCachedVolumeDefault = record;
+                return record;
+            }
+        }
+        return null;
+    }
+
     public void dump(PrintWriter pw, String prefix) {
         ArrayList<MediaSessionRecord> sortedSessions = getPriorityListLocked(false, 0,
                 UserHandle.USER_ALL);
@@ -237,7 +260,7 @@
                 lastLocalIndex++;
                 lastActiveIndex++;
                 lastPublishedIndex++;
-            } else if (session.isPlaybackActive()) {
+            } else if (session.isPlaybackActive(true)) {
                 // TODO replace getRoute() == null with real local route check
                 if(session.getRoute() == null) {
                     // Active local sessions get top priority
@@ -284,6 +307,7 @@
 
     private void clearCache() {
         mCachedDefault = null;
+        mCachedVolumeDefault = null;
         mCachedButtonReceiver = null;
         mCachedActiveList = null;
         mCachedTransportControlList = null;
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
index c1fa74f..d6f8118 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
@@ -82,7 +82,7 @@
         Log.d(TAG, "Creating session for package " + mContext.getBasePackageName());
         mSession = man.createSession("OneMedia");
         mSession.addCallback(mCallback);
-        mSession.addTransportControlsCallback(new TransportListener());
+        mSession.addTransportControlsCallback(new TransportCallback());
         mSession.setPlaybackState(mPlaybackState);
         mSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
         mSession.setRouteOptions(mRouteOptions);
@@ -255,7 +255,7 @@
         }
     }
 
-    private class TransportListener extends MediaSession.TransportControlsCallback {
+    private class TransportCallback extends MediaSession.TransportControlsCallback {
         @Override
         public void onPlay() {
             mRenderer.onPlay();