Add SliceWorker for remote volume controller

Also update some methods to fix issues in panel.

Bug: 126199571
Test: RunSettingsRoboTests
Change-Id: I9b93ae594d41cb71b984b06267161455373bf121
diff --git a/src/com/android/settings/notification/RemoteVolumePreferenceController.java b/src/com/android/settings/notification/RemoteVolumePreferenceController.java
index dece928..0ad307e 100644
--- a/src/com/android/settings/notification/RemoteVolumePreferenceController.java
+++ b/src/com/android/settings/notification/RemoteVolumePreferenceController.java
@@ -20,16 +20,20 @@
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.MediaSessionManager;
+import android.net.Uri;
 import android.os.Looper;
 import android.text.TextUtils;
 
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.OnLifecycleEvent;
+import androidx.preference.PreferenceScreen;
 
 import com.android.settings.R;
+import com.android.settings.slices.SliceBackgroundWorker;
 import com.android.settingslib.core.lifecycle.Lifecycle;
 import com.android.settingslib.volume.MediaSessions;
 
+import java.io.IOException;
 import java.util.List;
 
 public class RemoteVolumePreferenceController extends
@@ -41,32 +45,41 @@
 
     private MediaSessionManager mMediaSessionManager;
     private MediaSessions mMediaSessions;
-    private MediaSession.Token mActiveToken;
+    @VisibleForTesting
+    MediaSession.Token mActiveToken;
+    @VisibleForTesting
+    MediaController mMediaController;
 
-    private MediaSessions.Callbacks mCallbacks = new MediaSessions.Callbacks() {
+    @VisibleForTesting
+    MediaSessions.Callbacks mCallbacks = new MediaSessions.Callbacks() {
         @Override
         public void onRemoteUpdate(MediaSession.Token token, String name,
                 MediaController.PlaybackInfo pi) {
-            mActiveToken = token;
-            mPreference.setMax(pi.getMaxVolume());
-            mPreference.setVisible(true);
-            setSliderPosition(pi.getCurrentVolume());
+            if (mActiveToken == null) {
+                updateToken(token);
+            }
+            if (mActiveToken == token) {
+                updatePreference(mPreference, mActiveToken, pi);
+            }
         }
 
         @Override
         public void onRemoteRemoved(MediaSession.Token t) {
             if (mActiveToken == t) {
-                mActiveToken = null;
-                mPreference.setVisible(false);
+                updateToken(null);
+                if (mPreference != null) {
+                    mPreference.setVisible(false);
+                }
             }
         }
 
         @Override
         public void onRemoteVolumeChanged(MediaSession.Token token, int flags) {
             if (mActiveToken == token) {
-                final MediaController mediaController = new MediaController(mContext, token);
-                final MediaController.PlaybackInfo pi = mediaController.getPlaybackInfo();
-                setSliderPosition(pi.getCurrentVolume());
+                final MediaController.PlaybackInfo pi = mMediaController.getPlaybackInfo();
+                if (pi != null) {
+                    setSliderPosition(pi.getCurrentVolume());
+                }
             }
         }
     };
@@ -83,7 +96,7 @@
         for (MediaController mediaController : controllers) {
             final MediaController.PlaybackInfo pi = mediaController.getPlaybackInfo();
             if (isRemote(pi)) {
-                mActiveToken = mediaController.getSessionToken();
+                updateToken(mediaController.getSessionToken());
                 return AVAILABLE;
             }
         }
@@ -92,16 +105,24 @@
         return CONDITIONALLY_UNAVAILABLE;
     }
 
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        if (mMediaController != null) {
+            updatePreference(mPreference, mActiveToken, mMediaController.getPlaybackInfo());
+        }
+    }
+
     @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
     public void onResume() {
         super.onResume();
-        mMediaSessions.init();
+        //TODO(b/126199571): register callback once b/126890783 is fixed
     }
 
     @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
     public void onPause() {
         super.onPause();
-        mMediaSessions.destroy();
+        //TODO(b/126199571): unregister callback once b/126890783 is fixed
     }
 
     @Override
@@ -109,8 +130,11 @@
         if (mPreference != null) {
             return mPreference.getProgress();
         }
-        //TODO(b/126199571): get it from media controller
-        return 0;
+        if (mMediaController == null) {
+            return 0;
+        }
+        final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
+        return playbackInfo != null ? playbackInfo.getCurrentVolume() : 0;
     }
 
     @Override
@@ -118,8 +142,11 @@
         if (mPreference != null) {
             mPreference.setProgress(position);
         }
-        //TODO(b/126199571): set it through media controller
-        return false;
+        if (mMediaController == null) {
+            return false;
+        }
+        mMediaController.setVolumeTo(position, 0);
+        return true;
     }
 
     @Override
@@ -127,8 +154,11 @@
         if (mPreference != null) {
             return mPreference.getMax();
         }
-        //TODO(b/126199571): get it from media controller
-        return 0;
+        if (mMediaController == null) {
+            return 0;
+        }
+        final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
+        return playbackInfo != null ? playbackInfo.getMaxVolume() : 0;
     }
 
     @Override
@@ -156,4 +186,76 @@
         return pi != null
                 && pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE;
     }
+
+    @Override
+    public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() {
+        //TODO(b/126199571): return RemoteVolumeSliceWorker once b/126890783 is fixed
+        return null;
+    }
+
+    private void updatePreference(VolumeSeekBarPreference seekBarPreference,
+            MediaSession.Token token, MediaController.PlaybackInfo playbackInfo) {
+        if (seekBarPreference == null || token == null || playbackInfo == null) {
+            return;
+        }
+
+        seekBarPreference.setMax(playbackInfo.getMaxVolume());
+        seekBarPreference.setVisible(true);
+        setSliderPosition(playbackInfo.getCurrentVolume());
+    }
+
+    private void updateToken(MediaSession.Token token) {
+        mActiveToken = token;
+        if (token != null) {
+            mMediaController = new MediaController(mContext, mActiveToken);
+        } else {
+            mMediaController = null;
+        }
+    }
+
+    /**
+     * Listener for background change to remote volume, which listens callback
+     * from {@code MediaSessions}
+     */
+    public static class RemoteVolumeSliceWorker extends SliceBackgroundWorker<Void> implements
+            MediaSessions.Callbacks {
+
+        private MediaSessions mMediaSessions;
+
+        public RemoteVolumeSliceWorker(Context context, Uri uri) {
+            super(context, uri);
+            mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), this);
+        }
+
+        @Override
+        protected void onSlicePinned() {
+            mMediaSessions.init();
+        }
+
+        @Override
+        protected void onSliceUnpinned() {
+            mMediaSessions.destroy();
+        }
+
+        @Override
+        public void close() throws IOException {
+            mMediaSessions = null;
+        }
+
+        @Override
+        public void onRemoteUpdate(MediaSession.Token token, String name,
+                MediaController.PlaybackInfo pi) {
+            notifySliceChange();
+        }
+
+        @Override
+        public void onRemoteRemoved(MediaSession.Token t) {
+            notifySliceChange();
+        }
+
+        @Override
+        public void onRemoteVolumeChanged(MediaSession.Token token, int flags) {
+            notifySliceChange();
+        }
+    }
 }
diff --git a/src/com/android/settings/panel/VolumePanel.java b/src/com/android/settings/panel/VolumePanel.java
index 20c2272..62eca53 100644
--- a/src/com/android/settings/panel/VolumePanel.java
+++ b/src/com/android/settings/panel/VolumePanel.java
@@ -19,6 +19,7 @@
 import static com.android.settings.slices.CustomSliceRegistry.VOLUME_ALARM_URI;
 import static com.android.settings.slices.CustomSliceRegistry.VOLUME_CALL_URI;
 import static com.android.settings.slices.CustomSliceRegistry.VOLUME_MEDIA_URI;
+import static com.android.settings.slices.CustomSliceRegistry.VOLUME_REMOTE_MEDIA_URI;
 import static com.android.settings.slices.CustomSliceRegistry.VOLUME_RINGER_URI;
 
 import android.app.settings.SettingsEnums;
@@ -52,6 +53,7 @@
     @Override
     public List<Uri> getSlices() {
         final List<Uri> uris = new ArrayList<>();
+        uris.add(VOLUME_REMOTE_MEDIA_URI);
         uris.add(VOLUME_MEDIA_URI);
         uris.add(VOLUME_CALL_URI);
         uris.add(VOLUME_RINGER_URI);
diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java
index ab1b248..3a1db69 100644
--- a/src/com/android/settings/slices/CustomSliceRegistry.java
+++ b/src/com/android/settings/slices/CustomSliceRegistry.java
@@ -219,6 +219,17 @@
             .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
             .appendPath("media_volume")
             .build();
+
+    /**
+     * Full {@link Uri} for the Remote Media Volume Slice.
+     */
+    public static final Uri VOLUME_REMOTE_MEDIA_URI = new Uri.Builder()
+            .scheme(ContentResolver.SCHEME_CONTENT)
+            .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+            .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+            .appendPath("remote_volume")
+            .build();
+
     /**
      * Full {@link Uri} for the Ringer volume Slice.
      */
diff --git a/tests/robotests/src/com/android/settings/notification/RemoteVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/RemoteVolumePreferenceControllerTest.java
index a34d4d7..1bf2fd8 100644
--- a/tests/robotests/src/com/android/settings/notification/RemoteVolumePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/RemoteVolumePreferenceControllerTest.java
@@ -19,10 +19,13 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.media.session.ControllerLink;
 import android.media.session.MediaController;
+import android.media.session.MediaSession;
 import android.media.session.MediaSessionManager;
 
 import com.android.settings.R;
@@ -40,26 +43,41 @@
 
 @RunWith(RobolectricTestRunner.class)
 public class RemoteVolumePreferenceControllerTest {
+    private static final int CURRENT_POS = 5;
+    private static final int MAX_POS = 10;
 
     @Mock
     private MediaSessionManager mMediaSessionManager;
     @Mock
     private MediaController mMediaController;
+    @Mock
+    private ControllerLink.ControllerStub mStub;
+    @Mock
+    private ControllerLink.ControllerStub mStub2;
+    private MediaSession.Token mToken;
+    private MediaSession.Token mToken2;
     private RemoteVolumePreferenceController mController;
     private Context mContext;
     private List<MediaController> mActiveSessions;
+    private MediaController.PlaybackInfo mPlaybackInfo;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+
         mContext = spy(RuntimeEnvironment.application);
         when(mContext.getSystemService(MediaSessionManager.class)).thenReturn(mMediaSessionManager);
         mActiveSessions = new ArrayList<>();
         mActiveSessions.add(mMediaController);
         when(mMediaSessionManager.getActiveSessions(null)).thenReturn(
                 mActiveSessions);
+        mToken = new MediaSession.Token(new ControllerLink(mStub));
+        mToken2 = new MediaSession.Token(new ControllerLink(mStub2));
 
         mController = new RemoteVolumePreferenceController(mContext);
+        mPlaybackInfo = new MediaController.PlaybackInfo(
+                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, 0, MAX_POS, CURRENT_POS, null);
+        when(mMediaController.getPlaybackInfo()).thenReturn(mPlaybackInfo);
     }
 
     @Test
@@ -88,4 +106,92 @@
         assertThat(mController.getAudioStream()).isEqualTo(
                 RemoteVolumePreferenceController.REMOTE_VOLUME);
     }
+
+    @Test
+    public void getSliderPosition_controllerNull_returnZero() {
+        mController.mMediaController = null;
+
+        assertThat(mController.getSliderPosition()).isEqualTo(0);
+    }
+
+    @Test
+    public void getSliderPosition_controllerExists_returnValue() {
+        mController.mMediaController = mMediaController;
+
+        assertThat(mController.getSliderPosition()).isEqualTo(CURRENT_POS);
+    }
+
+    @Test
+    public void getMaxSteps_controllerNull_returnZero() {
+        mController.mMediaController = null;
+
+        assertThat(mController.getMaxSteps()).isEqualTo(0);
+    }
+
+    @Test
+    public void getMaxSteps_controllerExists_returnValue() {
+        mController.mMediaController = mMediaController;
+
+        assertThat(mController.getMaxSteps()).isEqualTo(MAX_POS);
+    }
+
+    @Test
+    public void setSliderPosition_controllerNull_returnFalse() {
+        mController.mMediaController = null;
+
+        assertThat(mController.setSliderPosition(CURRENT_POS)).isFalse();
+    }
+
+    @Test
+    public void setSliderPosition_controllerExists_returnTrue() {
+        mController.mMediaController = mMediaController;
+
+        assertThat(mController.setSliderPosition(CURRENT_POS)).isTrue();
+        verify(mMediaController).setVolumeTo(CURRENT_POS, 0 /* flags */);
+    }
+
+    @Test
+    public void onRemoteUpdate_firstToken_updateTokenAndPreference() {
+        mController.mPreference = new VolumeSeekBarPreference(mContext);
+        mController.mActiveToken = null;
+
+        mController.mCallbacks.onRemoteUpdate(mToken, "token", mPlaybackInfo);
+
+        assertThat(mController.mActiveToken).isEqualTo(mToken);
+        assertThat(mController.mPreference.isVisible()).isTrue();
+        assertThat(mController.mPreference.getMax()).isEqualTo(MAX_POS);
+        assertThat(mController.mPreference.getProgress()).isEqualTo(CURRENT_POS);
+    }
+
+    @Test
+    public void onRemoteUpdate_differentToken_doNothing() {
+        mController.mActiveToken = mToken;
+
+        mController.mCallbacks.onRemoteUpdate(mToken2, "token2", mPlaybackInfo);
+
+        assertThat(mController.mActiveToken).isEqualTo(mToken);
+    }
+
+    @Test
+    public void onRemoteRemoved_tokenRemoved_setInvisible() {
+        mController.mPreference = new VolumeSeekBarPreference(mContext);
+        mController.mActiveToken = mToken;
+
+        mController.mCallbacks.onRemoteRemoved(mToken);
+
+        assertThat(mController.mActiveToken).isNull();
+        assertThat(mController.mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void onRemoteVolumeChanged_volumeChanged_updateIt() {
+        mController.mPreference = new VolumeSeekBarPreference(mContext);
+        mController.mPreference.setMax(MAX_POS);
+        mController.mActiveToken = mToken;
+        mController.mMediaController = mMediaController;
+
+        mController.mCallbacks.onRemoteVolumeChanged(mToken, 0 /* flags */);
+
+        assertThat(mController.mPreference.getProgress()).isEqualTo(CURRENT_POS);
+    }
 }