• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.systemui.media.dialog;
18 
19 import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP;
20 import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE;
21 import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
22 import static com.android.media.flags.Flags.enableOutputSwitcherRedesign;
23 
24 import android.content.Context;
25 import android.graphics.drawable.Drawable;
26 import android.text.TextUtils;
27 import android.util.Log;
28 import android.view.View;
29 
30 import androidx.annotation.DoNotInline;
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.RequiresApi;
34 import androidx.recyclerview.widget.RecyclerView;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.media.flags.Flags;
38 import com.android.settingslib.media.LocalMediaManager.MediaDeviceState;
39 import com.android.settingslib.media.MediaDevice;
40 import com.android.systemui.res.R;
41 
42 import java.util.List;
43 import java.util.concurrent.CopyOnWriteArrayList;
44 
45 /**
46  * A parent RecyclerView adapter for the media output dialog device list. This class doesn't
47  * manipulate the layout directly.
48  */
49 public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
OngoingSessionStatus(boolean host)50     public record OngoingSessionStatus(boolean host) {}
51 
GroupStatus(Boolean selected, Boolean deselectable)52     public record GroupStatus(Boolean selected, Boolean deselectable) {}
53 
54     public enum ConnectionState {
55         CONNECTED,
56         CONNECTING,
57         DISCONNECTED,
58     }
59 
60     protected final MediaSwitchingController mController;
61     private int mCurrentActivePosition;
62     private boolean mIsDragging;
63     private static final String TAG = "MediaOutputAdapterBase";
64     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
65     protected final List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
66     private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherDeviceGrouping();
67 
MediaOutputAdapterBase(MediaSwitchingController controller)68     public MediaOutputAdapterBase(MediaSwitchingController controller) {
69         mController = controller;
70         mCurrentActivePosition = -1;
71         mIsDragging = false;
72         setHasStableIds(true);
73     }
74 
isCurrentlyConnected(MediaDevice device)75     boolean isCurrentlyConnected(MediaDevice device) {
76         return TextUtils.equals(device.getId(),
77                 mController.getCurrentConnectedMediaDevice().getId())
78                 || (mController.getSelectedMediaDevice().size() == 1
79                 && isDeviceIncluded(mController.getSelectedMediaDevice(), device));
80     }
81 
isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice)82     boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) {
83         for (MediaDevice device : deviceList) {
84             if (TextUtils.equals(device.getId(), targetDevice.getId())) {
85                 return true;
86             }
87         }
88         return false;
89     }
90 
isDragging()91     boolean isDragging() {
92         return mIsDragging;
93     }
94 
setIsDragging(boolean isDragging)95     void setIsDragging(boolean isDragging) {
96         mIsDragging = isDragging;
97     }
98 
getCurrentActivePosition()99     int getCurrentActivePosition() {
100         return mCurrentActivePosition;
101     }
102 
103     /** Refreshes the RecyclerView dataset and forces re-render. */
updateItems()104     public void updateItems() {
105         mMediaItemList.clear();
106         mMediaItemList.addAll(mController.getMediaItemList());
107         if (mShouldGroupSelectedMediaItems) {
108             if (mController.getSelectedMediaDevice().size() == 1) {
109                 // Don't group devices if initially there isn't more than one selected.
110                 mShouldGroupSelectedMediaItems = false;
111             }
112         }
113         notifyDataSetChanged();
114     }
115 
116     @Override
getItemId(int position)117     public long getItemId(int position) {
118         if (position >= mMediaItemList.size()) {
119             Log.d(TAG, "Incorrect position for item id: " + position);
120             return position;
121         }
122         MediaItem currentMediaItem = mMediaItemList.get(position);
123         return currentMediaItem.getMediaDevice().isPresent()
124                 ? currentMediaItem.getMediaDevice().get().getId().hashCode()
125                 : position;
126     }
127 
128     @Override
getItemViewType(int position)129     public int getItemViewType(int position) {
130         if (position >= mMediaItemList.size()) {
131             Log.d(TAG, "Incorrect position for item type: " + position);
132             return MediaItem.MediaItemType.TYPE_GROUP_DIVIDER;
133         }
134         return mMediaItemList.get(position).getMediaItemType();
135     }
136 
137     @Override
getItemCount()138     public int getItemCount() {
139         return mMediaItemList.size();
140     }
141 
142     public abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder {
143 
144         Context mContext;
145 
MediaDeviceViewHolderBase(View view, Context context)146         MediaDeviceViewHolderBase(View view, Context context) {
147             super(view);
148             mContext = context;
149         }
150 
renderItem(MediaItem mediaItem, int position)151         void renderItem(MediaItem mediaItem, int position) {
152             MediaDevice device = mediaItem.getMediaDevice().get();
153             boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice();
154             final boolean currentlyConnected = isCurrentlyConnected(device);
155             boolean isSelected = isDeviceIncluded(mController.getSelectedMediaDevice(), device);
156             boolean isDeselectable =
157                     isDeviceIncluded(mController.getDeselectableMediaDevice(), device);
158             boolean isSelectable = isDeviceIncluded(mController.getSelectableMediaDevice(), device);
159             boolean isTransferable =
160                     isDeviceIncluded(mController.getTransferableMediaDevices(), device);
161             boolean hasRouteListingPreferenceItem = device.hasRouteListingPreferenceItem();
162 
163             if (DEBUG) {
164                 Log.d(
165                         TAG,
166                         "["
167                                 + position
168                                 + "] "
169                                 + device.getName()
170                                 + " ["
171                                 + (isDeselectable ? "deselectable" : "")
172                                 + "] ["
173                                 + (isSelected ? "selected" : "")
174                                 + "] ["
175                                 + (isSelectable ? "selectable" : "")
176                                 + "] ["
177                                 + (isTransferable ? "transferable" : "")
178                                 + "] ["
179                                 + (hasRouteListingPreferenceItem ? "hasListingPreference" : "")
180                                 + "]");
181             }
182 
183             boolean isDeviceGroup = false;
184             boolean hideGroupItem = false;
185             GroupStatus groupStatus = null;
186             OngoingSessionStatus ongoingSessionStatus = null;
187             ConnectionState connectionState = ConnectionState.DISCONNECTED;
188             boolean restrictVolumeAdjustment = mController.hasAdjustVolumeUserRestriction();
189             String subtitle = null;
190             Drawable deviceStatusIcon = null;
191             boolean deviceDisabled = false;
192             View.OnClickListener clickListener = null;
193 
194             if (mCurrentActivePosition == position) {
195                 mCurrentActivePosition = -1;
196             }
197 
198             if (mController.isAnyDeviceTransferring()) {
199                 if (device.getState() == MediaDeviceState.STATE_CONNECTING) {
200                     connectionState = ConnectionState.CONNECTING;
201                 }
202             } else {
203                 // Set different layout for each device
204                 if (device.isMutingExpectedDevice()
205                         && !mController.isCurrentConnectedDeviceRemote()) {
206                     connectionState = ConnectionState.CONNECTED;
207                     restrictVolumeAdjustment = true;
208                     clickListener = v -> onItemClick(v, device);
209                 } else if (currentlyConnected && isMutingExpectedDeviceExist
210                         && !mController.isCurrentConnectedDeviceRemote()) {
211                     // mark as disconnected and set special click listener
212                     clickListener = v -> cancelMuteAwaitConnection();
213                 } else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
214                     connectionState = ConnectionState.CONNECTING;
215                 } else if (!enableOutputSwitcherRedesign() && mShouldGroupSelectedMediaItems
216                         && hasMultipleSelectedDevices()
217                         && isSelected) {
218                     if (mediaItem.isFirstDeviceInGroup()) {
219                         isDeviceGroup = true;
220                     } else {
221                         hideGroupItem = true;
222                     }
223                 } else { // A connected or disconnected device.
224                     subtitle = device.hasSubtext() ? device.getSubtextString() : null;
225                     ongoingSessionStatus = getOngoingSessionStatus(device);
226                     groupStatus = getGroupStatus(isSelected, isSelectable, isDeselectable);
227 
228                     if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
229                         deviceStatusIcon = mContext.getDrawable(
230                                 R.drawable.media_output_status_failed);
231                         subtitle = mContext.getString(R.string.media_output_dialog_connect_failed);
232                         clickListener = v -> onItemClick(v, device);
233                     } else if (currentlyConnected || isSelected) {
234                         connectionState = ConnectionState.CONNECTED;
235                     } else { // disconnected
236                         if (isSelectable) { // groupable device
237                             if (!Flags.disableTransferWhenAppsDoNotSupport() || isTransferable
238                                     || hasRouteListingPreferenceItem) {
239                                 clickListener = v -> onItemClick(v, device);
240                             }
241                         } else {
242                             deviceStatusIcon = getDeviceStatusIcon(device);
243                             clickListener = getClickListenerBasedOnSelectionBehavior(device);
244                         }
245                         deviceDisabled = clickListener == null;
246                     }
247                 }
248             }
249 
250             if (connectionState == ConnectionState.CONNECTED || isDeviceGroup) {
251                 mCurrentActivePosition = position;
252             }
253 
254             if (isDeviceGroup) {
255                 renderDeviceGroupItem();
256             } else {
257                 renderDeviceItem(hideGroupItem, device, connectionState, restrictVolumeAdjustment,
258                         groupStatus, ongoingSessionStatus, clickListener, deviceDisabled, subtitle,
259                         deviceStatusIcon);
260             }
261         }
262 
renderDeviceItem(boolean hideGroupItem, MediaDevice device, ConnectionState connectionState, boolean restrictVolumeAdjustment, GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus, View.OnClickListener clickListener, boolean deviceDisabled, String subtitle, Drawable deviceStatusIcon)263         protected abstract void renderDeviceItem(boolean hideGroupItem, MediaDevice device,
264                 ConnectionState connectionState, boolean restrictVolumeAdjustment,
265                 GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus,
266                 View.OnClickListener clickListener, boolean deviceDisabled, String subtitle,
267                 Drawable deviceStatusIcon);
268 
renderDeviceGroupItem()269         protected abstract void renderDeviceGroupItem();
270 
disableSeekBar()271         protected abstract void disableSeekBar();
272 
getOngoingSessionStatus(MediaDevice device)273         private OngoingSessionStatus getOngoingSessionStatus(MediaDevice device) {
274             return device.hasOngoingSession() ? new OngoingSessionStatus(
275                     device.isHostForOngoingSession()) : null;
276         }
277 
getGroupStatus(boolean isSelected, boolean isSelectable, boolean isDeselectable)278         private GroupStatus getGroupStatus(boolean isSelected, boolean isSelectable,
279                 boolean isDeselectable) {
280             // A device should either be selectable or, when the device selected, the list should
281             // have other selectable or selected devices.
282             boolean selectedWithOtherGroupDevices =
283                     isSelected && (hasMultipleSelectedDevices() || hasSelectableDevices());
284             if (isSelectable || selectedWithOtherGroupDevices) {
285                 return new GroupStatus(isSelected, isDeselectable);
286             }
287             return null;
288         }
289 
hasMultipleSelectedDevices()290         private boolean hasMultipleSelectedDevices() {
291             return mController.getSelectedMediaDevice().size() > 1;
292         }
293 
hasSelectableDevices()294         private boolean hasSelectableDevices() {
295             return !mController.getSelectableMediaDevice().isEmpty();
296         }
297 
298         @Nullable
getClickListenerBasedOnSelectionBehavior( @onNull MediaDevice device)299         private View.OnClickListener getClickListenerBasedOnSelectionBehavior(
300                 @NonNull MediaDevice device) {
301             return Api34Impl.getClickListenerBasedOnSelectionBehavior(
302                     device, mController, v -> onItemClick(v, device));
303         }
304 
305         @Nullable
getDeviceStatusIcon(MediaDevice device)306         private Drawable getDeviceStatusIcon(MediaDevice device) {
307             return Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(device, mContext);
308         }
309 
onExpandGroupButtonClicked()310         protected void onExpandGroupButtonClicked() {
311             mShouldGroupSelectedMediaItems = false;
312             notifyDataSetChanged();
313         }
314 
onGroupActionTriggered(boolean isChecked, MediaDevice device)315         protected void onGroupActionTriggered(boolean isChecked, MediaDevice device) {
316             disableSeekBar();
317             if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
318                 mController.addDeviceToPlayMedia(device);
319             } else if (!isChecked && isDeviceIncluded(mController.getDeselectableMediaDevice(),
320                     device)) {
321                 mController.removeDeviceFromPlayMedia(device);
322             }
323         }
324 
onItemClick(View view, MediaDevice device)325         private void onItemClick(View view, MediaDevice device) {
326             if (mController.isCurrentOutputDeviceHasSessionOngoing()) {
327                 showCustomEndSessionDialog(device);
328             } else {
329                 transferOutput(device);
330             }
331         }
332 
transferOutput(MediaDevice device)333         private void transferOutput(MediaDevice device) {
334             if (mController.isAnyDeviceTransferring()) {
335                 return;
336             }
337             if (isCurrentlyConnected(device)) {
338                 Log.d(TAG, "This device is already connected! : " + device.getName());
339                 return;
340             }
341             mController.setTemporaryAllowListExceptionIfNeeded(device);
342             mCurrentActivePosition = -1;
343             mController.connectDevice(device);
344             device.setState(MediaDeviceState.STATE_CONNECTING);
345             notifyDataSetChanged();
346         }
347 
348         @VisibleForTesting
showCustomEndSessionDialog(MediaDevice device)349         void showCustomEndSessionDialog(MediaDevice device) {
350             MediaSessionReleaseDialog mediaSessionReleaseDialog = new MediaSessionReleaseDialog(
351                     mContext, () -> transferOutput(device),
352                     mController.getColorSchemeLegacy().getColorButtonBackground(),
353                     mController.getColorSchemeLegacy().getColorItemContent());
354             mediaSessionReleaseDialog.show();
355         }
356 
cancelMuteAwaitConnection()357         private void cancelMuteAwaitConnection() {
358             mController.cancelMuteAwaitConnection();
359             notifyDataSetChanged();
360         }
361 
getDeviceItemContentDescription(@onNull MediaDevice device)362         protected String getDeviceItemContentDescription(@NonNull MediaDevice device) {
363             return mContext.getString(
364                     device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
365                             ? R.string.accessibility_bluetooth_name
366                             : R.string.accessibility_cast_name, device.getName());
367         }
368 
getGroupItemContentDescription(String sessionName)369         protected String getGroupItemContentDescription(String sessionName) {
370             return mContext.getString(R.string.accessibility_cast_name, sessionName);
371         }
372     }
373 
374     @RequiresApi(34)
375     private static class Api34Impl {
376         @DoNotInline
getClickListenerBasedOnSelectionBehavior( MediaDevice device, MediaSwitchingController controller, View.OnClickListener defaultTransferListener)377         static View.OnClickListener getClickListenerBasedOnSelectionBehavior(
378                 MediaDevice device,
379                 MediaSwitchingController controller,
380                 View.OnClickListener defaultTransferListener) {
381             switch (device.getSelectionBehavior()) {
382                 case SELECTION_BEHAVIOR_NONE:
383                     return null;
384                 case SELECTION_BEHAVIOR_TRANSFER:
385                     return defaultTransferListener;
386                 case SELECTION_BEHAVIOR_GO_TO_APP:
387                     return v -> controller.tryToLaunchInAppRoutingIntent(device.getId(), v);
388             }
389             return defaultTransferListener;
390         }
391 
392         @DoNotInline
393         @Nullable
getDeviceStatusIconBasedOnSelectionBehavior(MediaDevice device, Context context)394         static Drawable getDeviceStatusIconBasedOnSelectionBehavior(MediaDevice device,
395                 Context context) {
396             switch (device.getSelectionBehavior()) {
397                 case SELECTION_BEHAVIOR_NONE:
398                     return context.getDrawable(R.drawable.media_output_status_failed);
399                 case SELECTION_BEHAVIOR_TRANSFER:
400                     return null;
401                 case SELECTION_BEHAVIOR_GO_TO_APP:
402                     return context.getDrawable(R.drawable.media_output_status_help);
403             }
404             return null;
405         }
406     }
407 }
408