1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.developeroptions.notification;
18 
19 import android.content.Context;
20 import android.media.session.MediaController;
21 import android.media.session.MediaSession;
22 import android.media.session.MediaSessionManager;
23 import android.net.Uri;
24 import android.os.Looper;
25 import android.text.TextUtils;
26 
27 import androidx.annotation.VisibleForTesting;
28 import androidx.lifecycle.OnLifecycleEvent;
29 import androidx.preference.PreferenceScreen;
30 
31 import com.android.car.developeroptions.R;
32 import com.android.car.developeroptions.slices.SliceBackgroundWorker;
33 import com.android.settingslib.core.lifecycle.Lifecycle;
34 import com.android.settingslib.volume.MediaSessions;
35 
36 import java.io.IOException;
37 import java.util.List;
38 import java.util.Objects;
39 
40 public class RemoteVolumePreferenceController extends VolumeSeekBarPreferenceController {
41 
42     private static final String KEY_REMOTE_VOLUME = "remote_volume";
43     @VisibleForTesting
44     static final int REMOTE_VOLUME = 100;
45 
46     private MediaSessionManager mMediaSessionManager;
47     private MediaSessions mMediaSessions;
48     @VisibleForTesting
49     MediaSession.Token mActiveToken;
50     @VisibleForTesting
51     MediaController mMediaController;
52 
53     @VisibleForTesting
54     MediaSessions.Callbacks mCallbacks = new MediaSessions.Callbacks() {
55         @Override
56         public void onRemoteUpdate(MediaSession.Token token, String name,
57                 MediaController.PlaybackInfo pi) {
58             if (mActiveToken == null) {
59                 updateToken(token);
60             }
61             if (Objects.equals(mActiveToken, token)) {
62                 updatePreference(mPreference, mActiveToken, pi);
63             }
64         }
65 
66         @Override
67         public void onRemoteRemoved(MediaSession.Token t) {
68             if (Objects.equals(mActiveToken, t)) {
69                 updateToken(null);
70                 if (mPreference != null) {
71                     mPreference.setVisible(false);
72                 }
73             }
74         }
75 
76         @Override
77         public void onRemoteVolumeChanged(MediaSession.Token token, int flags) {
78             if (Objects.equals(mActiveToken, token)) {
79                 final MediaController.PlaybackInfo pi = mMediaController.getPlaybackInfo();
80                 if (pi != null) {
81                     setSliderPosition(pi.getCurrentVolume());
82                 }
83             }
84         }
85     };
86 
RemoteVolumePreferenceController(Context context)87     public RemoteVolumePreferenceController(Context context) {
88         super(context, KEY_REMOTE_VOLUME);
89         mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
90         mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), mCallbacks);
91     }
92 
93     @Override
getAvailabilityStatus()94     public int getAvailabilityStatus() {
95         final List<MediaController> controllers = mMediaSessionManager.getActiveSessions(null);
96         for (MediaController mediaController : controllers) {
97             final MediaController.PlaybackInfo pi = mediaController.getPlaybackInfo();
98             if (isRemote(pi)) {
99                 updateToken(mediaController.getSessionToken());
100                 return AVAILABLE;
101             }
102         }
103 
104         // No active remote media at this point
105         return CONDITIONALLY_UNAVAILABLE;
106     }
107 
108     @Override
displayPreference(PreferenceScreen screen)109     public void displayPreference(PreferenceScreen screen) {
110         super.displayPreference(screen);
111         if (mMediaController != null) {
112             updatePreference(mPreference, mActiveToken, mMediaController.getPlaybackInfo());
113         }
114     }
115 
116     @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
onResume()117     public void onResume() {
118         super.onResume();
119         mMediaSessions.init();
120     }
121 
122     @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
onPause()123     public void onPause() {
124         super.onPause();
125         mMediaSessions.destroy();
126     }
127 
128     @Override
getSliderPosition()129     public int getSliderPosition() {
130         if (mPreference != null) {
131             return mPreference.getProgress();
132         }
133         if (mMediaController == null) {
134             return 0;
135         }
136         final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
137         return playbackInfo != null ? playbackInfo.getCurrentVolume() : 0;
138     }
139 
140     @Override
setSliderPosition(int position)141     public boolean setSliderPosition(int position) {
142         if (mPreference != null) {
143             mPreference.setProgress(position);
144         }
145         if (mMediaController == null) {
146             return false;
147         }
148         mMediaController.setVolumeTo(position, 0);
149         return true;
150     }
151 
152     @Override
getMaxSteps()153     public int getMaxSteps() {
154         if (mPreference != null) {
155             return mPreference.getMax();
156         }
157         if (mMediaController == null) {
158             return 0;
159         }
160         final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
161         return playbackInfo != null ? playbackInfo.getMaxVolume() : 0;
162     }
163 
164     @Override
isSliceable()165     public boolean isSliceable() {
166         return TextUtils.equals(getPreferenceKey(), KEY_REMOTE_VOLUME);
167     }
168 
169     @Override
useDynamicSliceSummary()170     public boolean useDynamicSliceSummary() {
171         return true;
172     }
173 
174     @Override
getPreferenceKey()175     public String getPreferenceKey() {
176         return KEY_REMOTE_VOLUME;
177     }
178 
179     @Override
getAudioStream()180     public int getAudioStream() {
181         // This can be anything because remote volume controller doesn't rely on it.
182         return REMOTE_VOLUME;
183     }
184 
185     @Override
getMuteIcon()186     public int getMuteIcon() {
187         return R.drawable.ic_volume_remote_mute;
188     }
189 
isRemote(MediaController.PlaybackInfo pi)190     public static boolean isRemote(MediaController.PlaybackInfo pi) {
191         return pi != null
192                 && pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE;
193     }
194 
195     @Override
getBackgroundWorkerClass()196     public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() {
197         return RemoteVolumeSliceWorker.class;
198     }
199 
updatePreference(VolumeSeekBarPreference seekBarPreference, MediaSession.Token token, MediaController.PlaybackInfo playbackInfo)200     private void updatePreference(VolumeSeekBarPreference seekBarPreference,
201             MediaSession.Token token, MediaController.PlaybackInfo playbackInfo) {
202         if (seekBarPreference == null || token == null || playbackInfo == null) {
203             return;
204         }
205 
206         seekBarPreference.setMax(playbackInfo.getMaxVolume());
207         seekBarPreference.setVisible(true);
208         setSliderPosition(playbackInfo.getCurrentVolume());
209     }
210 
updateToken(MediaSession.Token token)211     private void updateToken(MediaSession.Token token) {
212         mActiveToken = token;
213         if (token != null) {
214             mMediaController = new MediaController(mContext, mActiveToken);
215         } else {
216             mMediaController = null;
217         }
218     }
219 
220     /**
221      * Listener for background change to remote volume, which listens callback
222      * from {@code MediaSessions}
223      */
224     public static class RemoteVolumeSliceWorker extends SliceBackgroundWorker<Void> implements
225             MediaSessions.Callbacks {
226 
227         private MediaSessions mMediaSessions;
228 
RemoteVolumeSliceWorker(Context context, Uri uri)229         public RemoteVolumeSliceWorker(Context context, Uri uri) {
230             super(context, uri);
231             mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), this);
232         }
233 
234         @Override
onSlicePinned()235         protected void onSlicePinned() {
236             mMediaSessions.init();
237         }
238 
239         @Override
onSliceUnpinned()240         protected void onSliceUnpinned() {
241             mMediaSessions.destroy();
242         }
243 
244         @Override
close()245         public void close() throws IOException {
246             mMediaSessions = null;
247         }
248 
249         @Override
onRemoteUpdate(MediaSession.Token token, String name, MediaController.PlaybackInfo pi)250         public void onRemoteUpdate(MediaSession.Token token, String name,
251                 MediaController.PlaybackInfo pi) {
252             notifySliceChange();
253         }
254 
255         @Override
onRemoteRemoved(MediaSession.Token t)256         public void onRemoteRemoved(MediaSession.Token t) {
257             notifySliceChange();
258         }
259 
260         @Override
onRemoteVolumeChanged(MediaSession.Token token, int flags)261         public void onRemoteVolumeChanged(MediaSession.Token token, int flags) {
262             notifySliceChange();
263         }
264     }
265 }
266