1 /*
2  * Copyright (C) 2020 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.settings.notification;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.media.MediaRouter2Manager;
24 import android.media.RoutingSessionInfo;
25 import android.text.TextUtils;
26 
27 import androidx.annotation.VisibleForTesting;
28 import androidx.preference.Preference;
29 import androidx.preference.PreferenceCategory;
30 import androidx.preference.PreferenceScreen;
31 
32 import com.android.settings.R;
33 import com.android.settings.Utils;
34 import com.android.settings.core.BasePreferenceController;
35 import com.android.settingslib.core.lifecycle.LifecycleObserver;
36 import com.android.settingslib.core.lifecycle.events.OnDestroy;
37 import com.android.settingslib.media.LocalMediaManager;
38 import com.android.settingslib.media.MediaDevice;
39 import com.android.settingslib.media.MediaOutputConstants;
40 import com.android.settingslib.utils.ThreadUtils;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 
45 /**
46  * A group preference controller to add/remove/update preference
47  * {@link com.android.settings.notification.RemoteVolumeSeekBarPreference}
48  **/
49 public class RemoteVolumeGroupController extends BasePreferenceController implements
50         Preference.OnPreferenceChangeListener, LifecycleObserver, OnDestroy,
51         LocalMediaManager.DeviceCallback {
52 
53     private static final String KEY_REMOTE_VOLUME_GROUP = "remote_media_group";
54     private static final String TAG = "RemoteVolumePrefCtr";
55     @VisibleForTesting
56     static final String SWITCHER_PREFIX = "OUTPUT_SWITCHER";
57 
58     @Nullable
59     private PreferenceCategory mPreferenceCategory;
60     private final List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>();
61 
62     @VisibleForTesting
63     LocalMediaManager mLocalMediaManager;
64     @VisibleForTesting
65     MediaRouter2Manager mRouterManager;
66 
67     // Called via reflection from BasePreferenceController#createInstance().
RemoteVolumeGroupController(Context context, String preferenceKey)68     public RemoteVolumeGroupController(Context context, String preferenceKey) {
69         super(context, preferenceKey);
70         if (mLocalMediaManager == null) {
71             mLocalMediaManager = new LocalMediaManager(mContext, /* packageName= */ null);
72             mLocalMediaManager.registerCallback(this);
73             mLocalMediaManager.startScan();
74         }
75         mRouterManager = MediaRouter2Manager.getInstance(context);
76     }
77 
78     @VisibleForTesting
RemoteVolumeGroupController( @onNull Context context, @NonNull String preferenceKey, @NonNull LocalMediaManager localMediaManager, @NonNull MediaRouter2Manager mediaRouter2Manager)79     /* package */ RemoteVolumeGroupController(
80             @NonNull Context context,
81             @NonNull String preferenceKey,
82             @NonNull LocalMediaManager localMediaManager,
83             @NonNull MediaRouter2Manager mediaRouter2Manager) {
84         super(context, preferenceKey);
85         mLocalMediaManager = localMediaManager;
86         mRouterManager = mediaRouter2Manager;
87         mLocalMediaManager.registerCallback(this);
88         mLocalMediaManager.startScan();
89     }
90 
91     @Override
getAvailabilityStatus()92     public int getAvailabilityStatus() {
93         if (mRoutingSessionInfos.isEmpty()) {
94             return CONDITIONALLY_UNAVAILABLE;
95         }
96         return AVAILABLE_UNSEARCHABLE;
97     }
98 
99     @Override
displayPreference(PreferenceScreen screen)100     public void displayPreference(PreferenceScreen screen) {
101         super.displayPreference(screen);
102         mPreferenceCategory = screen.findPreference(getPreferenceKey());
103         initRemoteMediaSession();
104         refreshPreference();
105     }
106 
initRemoteMediaSession()107     private void initRemoteMediaSession() {
108         mRoutingSessionInfos.clear();
109         mRoutingSessionInfos.addAll(mLocalMediaManager.getRemoteRoutingSessions());
110     }
111 
112     @Override
onDestroy()113     public void onDestroy() {
114         mLocalMediaManager.unregisterCallback(this);
115         mLocalMediaManager.stopScan();
116     }
117 
refreshPreference()118     private synchronized void refreshPreference() {
119         if (!isAvailable()) {
120             mPreferenceCategory.setVisible(false);
121             return;
122         }
123         final CharSequence castVolume = mContext.getText(R.string.remote_media_volume_option_title);
124         mPreferenceCategory.setVisible(true);
125         for (RoutingSessionInfo info : mRoutingSessionInfos) {
126             final CharSequence appName = Utils.getApplicationLabel(mContext,
127                     info.getClientPackageName());
128             RemoteVolumeSeekBarPreference seekBarPreference = mPreferenceCategory.findPreference(
129                     info.getId());
130             if (seekBarPreference != null) {
131                 // Update slider
132                 if (seekBarPreference.getProgress() != info.getVolume()) {
133                     seekBarPreference.setProgress(info.getVolume());
134                 }
135                 seekBarPreference.setEnabled(mLocalMediaManager.shouldEnableVolumeSeekBar(info));
136             } else {
137                 // Add slider
138                 seekBarPreference = new RemoteVolumeSeekBarPreference(mContext);
139                 seekBarPreference.setKey(info.getId());
140                 seekBarPreference.setTitle(castVolume);
141                 seekBarPreference.setMax(info.getVolumeMax());
142                 seekBarPreference.setProgress(info.getVolume());
143                 seekBarPreference.setMin(0);
144                 seekBarPreference.setOnPreferenceChangeListener(this);
145                 seekBarPreference.setIcon(com.android.settingslib.R.drawable.ic_volume_remote);
146                 seekBarPreference.setEnabled(mLocalMediaManager.shouldEnableVolumeSeekBar(info));
147                 mPreferenceCategory.addPreference(seekBarPreference);
148             }
149 
150             Preference switcherPreference = mPreferenceCategory.findPreference(
151                     SWITCHER_PREFIX + info.getId());
152 
153             // TODO: b/291277292 - Remove references to MediaRouter2Manager and implement long-term
154             //  solution in SettingsLib.
155             final boolean isMediaOutputDisabled =
156                     mRouterManager.getTransferableRoutes(info.getClientPackageName()).isEmpty();
157             final CharSequence outputTitle = mContext.getString(R.string.media_output_label_title,
158                     appName);
159             if (switcherPreference != null) {
160                 // Update output indicator
161                 switcherPreference.setTitle(isMediaOutputDisabled ? appName : outputTitle);
162                 switcherPreference.setSummary(info.getName());
163                 switcherPreference.setEnabled(!isMediaOutputDisabled);
164             } else {
165                 // Add output indicator
166                 switcherPreference = new Preference(mContext);
167                 switcherPreference.setKey(SWITCHER_PREFIX + info.getId());
168                 switcherPreference.setTitle(isMediaOutputDisabled ? appName : outputTitle);
169                 switcherPreference.setSummary(info.getName());
170                 switcherPreference.setEnabled(!isMediaOutputDisabled);
171                 mPreferenceCategory.addPreference(switcherPreference);
172             }
173         }
174 
175         // Check and remove non-active session preference
176         // There is a pair of preferences for each session. First one is a seekBar preference.
177         // The second one shows the session information and provide an entry-point to launch output
178         // switcher. It is unnecessary to go through all preferences. It is fine ignore the second
179         // preference and only to check the seekBar's key value.
180         for (int i = 0; i < mPreferenceCategory.getPreferenceCount(); i = i + 2) {
181             final Preference preference = mPreferenceCategory.getPreference(i);
182             boolean isActive = false;
183             for (RoutingSessionInfo info : mRoutingSessionInfos) {
184                 if (TextUtils.equals(preference.getKey(), info.getId())) {
185                     isActive = true;
186                     break;
187                 }
188             }
189             if (isActive) {
190                 continue;
191             }
192             final Preference switcherPreference = mPreferenceCategory.getPreference(i + 1);
193             if (switcherPreference != null) {
194                 mPreferenceCategory.removePreference(preference);
195                 mPreferenceCategory.removePreference(switcherPreference);
196             }
197         }
198     }
199 
200     @Override
onPreferenceChange(Preference preference, Object newValue)201     public boolean onPreferenceChange(Preference preference, Object newValue) {
202         ThreadUtils.postOnBackgroundThread(() -> {
203             mLocalMediaManager.adjustSessionVolume(preference.getKey(), (int) newValue);
204         });
205         return true;
206     }
207 
208     @Override
handlePreferenceTreeClick(Preference preference)209     public boolean handlePreferenceTreeClick(Preference preference) {
210         if (!preference.getKey().startsWith(SWITCHER_PREFIX)) {
211             return false;
212         }
213         for (RoutingSessionInfo info : mRoutingSessionInfos) {
214             if (TextUtils.equals(info.getId(),
215                     preference.getKey().substring(SWITCHER_PREFIX.length()))) {
216                 final Intent intent = new Intent()
217                         .setAction(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG)
218                         .setPackage(MediaOutputConstants.SYSTEMUI_PACKAGE_NAME)
219                         .putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME,
220                                 info.getClientPackageName());
221                 mContext.sendBroadcast(intent);
222                 return true;
223             }
224         }
225         return false;
226     }
227 
228     @Override
getPreferenceKey()229     public String getPreferenceKey() {
230         return KEY_REMOTE_VOLUME_GROUP;
231     }
232 
233     @Override
onDeviceListUpdate(List<MediaDevice> devices)234     public void onDeviceListUpdate(List<MediaDevice> devices) {
235         if (mPreferenceCategory == null) {
236             // Preference group is not ready.
237             return;
238         }
239         ThreadUtils.postOnMainThread(() -> {
240             initRemoteMediaSession();
241             refreshPreference();
242         });
243     }
244 
245     @Override
onSelectedDeviceStateChanged(MediaDevice device, int state)246     public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
247     }
248 }
249