• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.systemui.tv.media;
18 
19 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE;
20 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE;
21 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE;
22 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE;
23 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE;
24 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE;
25 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE;
26 
27 import android.app.KeyguardManager;
28 import android.content.Context;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.content.res.Resources;
31 import android.media.AudioManager;
32 import android.media.session.MediaSessionManager;
33 import android.os.PowerExemptionManager;
34 import android.text.TextUtils;
35 import android.util.Log;
36 
37 import com.android.settingslib.bluetooth.LocalBluetoothManager;
38 import com.android.settingslib.media.MediaDevice;
39 import com.android.systemui.animation.DialogTransitionAnimator;
40 import com.android.systemui.flags.FeatureFlags;
41 import com.android.systemui.media.dialog.MediaItem;
42 import com.android.systemui.media.dialog.MediaSwitchingController;
43 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
44 import com.android.systemui.plugins.ActivityStarter;
45 import com.android.systemui.settings.UserTracker;
46 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
47 import com.android.systemui.tv.res.R;
48 import com.android.systemui.volume.panel.domain.interactor.VolumePanelGlobalStateInteractor;
49 
50 import org.jetbrains.annotations.NotNull;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 /**
56  * Extends {@link MediaSwitchingController} to create a TV specific ordering and grouping of devices
57  * which are shown in the {@link TvMediaOutputDialogActivity}.
58  */
59 public class TvMediaOutputController extends MediaSwitchingController {
60 
61     private static final String TAG = TvMediaOutputController.class.getSimpleName();
62     private static final String SETTINGS_PACKAGE = "com.android.tv.settings";
63 
64     private final Context mContext;
65     private final AudioManager mAudioManager;
66 
TvMediaOutputController( @otNull Context context, String packageName, MediaSessionManager mediaSessionManager, LocalBluetoothManager lbm, ActivityStarter starter, CommonNotifCollection notifCollection, DialogTransitionAnimator dialogTransitionAnimator, NearbyMediaDevicesManager nearbyMediaDevicesManager, AudioManager audioManager, PowerExemptionManager powerExemptionManager, KeyguardManager keyGuardManager, FeatureFlags featureFlags, VolumePanelGlobalStateInteractor volumePanelGlobalStateInteractor, UserTracker userTracker)67     public TvMediaOutputController(
68             @NotNull Context context,
69             String packageName,
70             MediaSessionManager mediaSessionManager,
71             LocalBluetoothManager lbm,
72             ActivityStarter starter,
73             CommonNotifCollection notifCollection,
74             DialogTransitionAnimator dialogTransitionAnimator,
75             NearbyMediaDevicesManager nearbyMediaDevicesManager,
76             AudioManager audioManager,
77             PowerExemptionManager powerExemptionManager,
78             KeyguardManager keyGuardManager,
79             FeatureFlags featureFlags,
80             VolumePanelGlobalStateInteractor volumePanelGlobalStateInteractor,
81             UserTracker userTracker) {
82         super(
83                 context,
84                 packageName,
85                 /* userHandle= */ null,
86                 /* token= */ null,
87                 mediaSessionManager,
88                 lbm,
89                 starter,
90                 notifCollection,
91                 dialogTransitionAnimator,
92                 nearbyMediaDevicesManager,
93                 audioManager,
94                 powerExemptionManager,
95                 keyGuardManager,
96                 featureFlags,
97                 volumePanelGlobalStateInteractor,
98                 userTracker);
99         mContext = context;
100         mAudioManager = audioManager;
101     }
102 
showVolumeDialog()103     void showVolumeDialog() {
104         mAudioManager.adjustVolume(AudioManager.ADJUST_SAME, AudioManager.FLAG_SHOW_UI);
105     }
106 
107     /**
108      * Assigns lower priorities to devices that should be shown higher up in the list.
109      */
getDevicePriorityGroup(MediaDevice mediaDevice)110     private int getDevicePriorityGroup(MediaDevice mediaDevice) {
111         int mediaDeviceType = mediaDevice.getDeviceType();
112         return switch (mediaDeviceType) {
113             case TYPE_PHONE_DEVICE -> 1;
114             case TYPE_USB_C_AUDIO_DEVICE -> 2;
115             case TYPE_3POINT5_MM_AUDIO_DEVICE -> 3;
116             case TYPE_CAST_DEVICE, TYPE_CAST_GROUP_DEVICE, TYPE_BLUETOOTH_DEVICE,
117                     TYPE_FAST_PAIR_BLUETOOTH_DEVICE -> 5;
118             default -> 4;
119         };
120     }
121 
sortMediaDevices(List<MediaDevice> mediaDevices)122     private void sortMediaDevices(List<MediaDevice> mediaDevices) {
123         mediaDevices.sort((device1, device2) -> {
124             int priority1 = getDevicePriorityGroup(device1);
125             int priority2 = getDevicePriorityGroup(device2);
126 
127             if (priority1 != priority2) {
128                 return (priority1 < priority2) ? -1 : 1;
129             }
130             // Show connected before disconnected devices
131             if (device1.isConnected() != device2.isConnected()) {
132                 return device1.isConnected() ? -1 : 1;
133             }
134             return device1.getName().compareToIgnoreCase(device2.getName());
135         });
136     }
137 
138     @Override
buildMediaItems(List<MediaItem> oldMediaItems, List<MediaDevice> devices)139     protected List<MediaItem> buildMediaItems(List<MediaItem> oldMediaItems,
140             List<MediaDevice> devices) {
141         synchronized (mMediaDevicesLock) {
142             if (oldMediaItems.isEmpty()) {
143                 return buildInitialList(devices);
144             }
145             return buildBetterSubsequentList(oldMediaItems, devices);
146         }
147     }
148 
buildInitialList(List<MediaDevice> devices)149     private List<MediaItem> buildInitialList(List<MediaDevice> devices) {
150         sortMediaDevices(devices);
151 
152         List<MediaItem> finalMediaItems = new ArrayList<>();
153         boolean disconnectedDevicesAdded = false;
154         for (MediaDevice device : devices) {
155             // Add divider before first disconnected device
156             if (!device.isConnected() && !disconnectedDevicesAdded) {
157                 addOtherDevicesDivider(finalMediaItems);
158                 disconnectedDevicesAdded = true;
159             }
160             finalMediaItems.add(MediaItem.createDeviceMediaItem(device));
161         }
162         addConnectAnotherDeviceItem(finalMediaItems);
163         return finalMediaItems;
164     }
165 
166     /**
167      * Keep devices that have not changed their connection state in the same order.
168      * If there is a new connected device, put it at the *bottom* of the connected devices list and
169      * if there is a newly disconnected device, add it at the *top* of the disconnected devices.
170      */
buildBetterSubsequentList(List<MediaItem> previousMediaItems, List<MediaDevice> devices)171     private List<MediaItem> buildBetterSubsequentList(List<MediaItem> previousMediaItems,
172             List<MediaDevice> devices) {
173 
174         final List<MediaItem> targetMediaItems = new ArrayList<>();
175         // Only use the actual devices, not the dividers etc.
176         List<MediaItem> oldMediaItems = previousMediaItems.stream()
177                 .filter(mediaItem -> mediaItem.getMediaDevice().isPresent()).toList();
178         addItemsBasedOnConnection(targetMediaItems, oldMediaItems, devices,
179                 /* isConnected= */ true);
180         addItemsBasedOnConnection(targetMediaItems, oldMediaItems, devices,
181                 /* isConnected= */ false);
182 
183         addConnectAnotherDeviceItem(targetMediaItems);
184         return targetMediaItems;
185     }
186 
addItemsBasedOnConnection(List<MediaItem> targetMediaItems, List<MediaItem> oldMediaItems, List<MediaDevice> devices, boolean isConnected)187     private void addItemsBasedOnConnection(List<MediaItem> targetMediaItems,
188             List<MediaItem> oldMediaItems, List<MediaDevice> devices, boolean isConnected) {
189 
190         List<MediaDevice> matchingMediaDevices = new ArrayList<>();
191         for (MediaItem originalMediaItem : oldMediaItems) {
192             // Only go through the device items
193             MediaDevice oldDevice = originalMediaItem.getMediaDevice().get();
194 
195             for (MediaDevice newDevice : devices) {
196                 if (TextUtils.equals(oldDevice.getId(), newDevice.getId())
197                         && oldDevice.isConnected() == isConnected
198                         && newDevice.isConnected() == isConnected) {
199                     matchingMediaDevices.add(newDevice);
200                     break;
201                 }
202             }
203         }
204         devices.removeAll(matchingMediaDevices);
205 
206         List<MediaDevice> newMediaDevices = new ArrayList<>();
207         for (MediaDevice remainingDevice : devices) {
208             if (remainingDevice.isConnected() == isConnected) {
209                 newMediaDevices.add(remainingDevice);
210             }
211         }
212         devices.removeAll(newMediaDevices);
213 
214         // Add new connected devices at the end, add new disconnected devices at the start
215         if (isConnected) {
216             targetMediaItems.addAll(
217                     matchingMediaDevices.stream().map(MediaItem::createDeviceMediaItem).toList());
218             targetMediaItems.addAll(
219                     newMediaDevices.stream().map(MediaItem::createDeviceMediaItem).toList());
220         } else {
221             if (!matchingMediaDevices.isEmpty() || !newMediaDevices.isEmpty()) {
222                 addOtherDevicesDivider(targetMediaItems);
223             }
224             targetMediaItems.addAll(
225                     newMediaDevices.stream().map(MediaItem::createDeviceMediaItem).toList());
226             targetMediaItems.addAll(
227                     matchingMediaDevices.stream().map(MediaItem::createDeviceMediaItem).toList());
228         }
229     }
230 
addOtherDevicesDivider(List<MediaItem> mediaItems)231     private void addOtherDevicesDivider(List<MediaItem> mediaItems) {
232         mediaItems.add(
233                 MediaItem.createGroupDividerMediaItem(
234                         mContext.getString(R.string.media_output_dialog_other_devices)));
235     }
236 
addConnectAnotherDeviceItem(List<MediaItem> mediaItems)237     private void addConnectAnotherDeviceItem(List<MediaItem> mediaItems) {
238         if (getBluetoothSettingsSliceUri() == null) {
239             Log.d(TAG, "No bluetooth slice set.");
240             return;
241         }
242         mediaItems.add(MediaItem.createGroupDividerMediaItem(/* title */ null));
243         mediaItems.add(MediaItem.createPairNewDeviceMediaItem());
244     }
245 
getBluetoothSettingsSliceUri()246     String getBluetoothSettingsSliceUri() {
247         String uri = null;
248         Resources res;
249 
250         try {
251             res = mContext.getPackageManager().getResourcesForApplication(SETTINGS_PACKAGE);
252             int resourceId = res.getIdentifier(
253                     SETTINGS_PACKAGE + ":string/connected_devices_slice_uri", null, null);
254             if (resourceId != 0) {
255                 uri = res.getString(resourceId);
256             }
257         } catch (NameNotFoundException exception) {
258             Log.e(TAG, "Could not find TvSettings package: " + exception);
259         }
260         return uri;
261     }
262 
263     @Override
start(@otNull Callback cb)264     protected void start(@NotNull Callback cb) {
265         super.start(cb);
266     }
267 
268     @Override
stop()269     protected void stop() {
270         super.stop();
271     }
272 
273     @Override
setTemporaryAllowListExceptionIfNeeded(MediaDevice targetDevice)274     protected void setTemporaryAllowListExceptionIfNeeded(MediaDevice targetDevice) {
275         super.setTemporaryAllowListExceptionIfNeeded(targetDevice);
276     }
277 
278     @Override
connectDevice(MediaDevice mediaDevice)279     protected void connectDevice(MediaDevice mediaDevice) {
280         super.connectDevice(mediaDevice);
281     }
282 }
283