• 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 android.animation.Animator;
20 import android.animation.ValueAnimator;
21 import android.annotation.DrawableRes;
22 import android.annotation.StringRes;
23 import android.content.Context;
24 import android.content.res.ColorStateList;
25 import android.graphics.drawable.AnimatedVectorDrawable;
26 import android.graphics.drawable.ClipDrawable;
27 import android.graphics.drawable.Drawable;
28 import android.graphics.drawable.GradientDrawable;
29 import android.graphics.drawable.Icon;
30 import android.graphics.drawable.LayerDrawable;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.animation.LinearInterpolator;
37 import android.widget.CheckBox;
38 import android.widget.FrameLayout;
39 import android.widget.ImageButton;
40 import android.widget.ImageView;
41 import android.widget.ProgressBar;
42 import android.widget.SeekBar;
43 import android.widget.TextView;
44 
45 import androidx.annotation.NonNull;
46 import androidx.annotation.Nullable;
47 import androidx.annotation.VisibleForTesting;
48 import androidx.recyclerview.widget.RecyclerView;
49 
50 import com.android.media.flags.Flags;
51 import com.android.settingslib.media.InputMediaDevice;
52 import com.android.settingslib.media.MediaDevice;
53 import com.android.systemui.dagger.qualifiers.Background;
54 import com.android.systemui.dagger.qualifiers.Main;
55 import com.android.systemui.res.R;
56 
57 import java.util.concurrent.Executor;
58 /**
59  * A RecyclerView adapter for the legacy UI media output dialog device list.
60  */
61 public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
62     private static final String TAG = "MediaOutputAdapterL";
63     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
64 
65     private static final int UNMUTE_DEFAULT_VOLUME = 2;
66     @VisibleForTesting static final float DEVICE_DISABLED_ALPHA = 0.5f;
67     @VisibleForTesting static final float DEVICE_ACTIVE_ALPHA = 1f;
68     private final Executor mMainExecutor;
69     private final Executor mBackgroundExecutor;
70     View mHolderView;
71 
MediaOutputAdapterLegacy( MediaSwitchingController controller, @Main Executor mainExecutor, @Background Executor backgroundExecutor )72     public MediaOutputAdapterLegacy(
73             MediaSwitchingController controller,
74             @Main Executor mainExecutor,
75             @Background Executor backgroundExecutor
76     ) {
77         super(controller);
78         mMainExecutor = mainExecutor;
79         mBackgroundExecutor = backgroundExecutor;
80     }
81 
82     @Override
onCreateViewHolder(@onNull ViewGroup viewGroup, int viewType)83     public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
84             int viewType) {
85 
86         Context context = viewGroup.getContext();
87         mHolderView = LayoutInflater.from(viewGroup.getContext()).inflate(
88                 MediaItem.getMediaLayoutId(viewType),
89                 viewGroup, false);
90 
91         switch (viewType) {
92             case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
93                 return new MediaGroupDividerViewHolderLegacy(mHolderView);
94             case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
95             case MediaItem.MediaItemType.TYPE_DEVICE:
96             default:
97                 return new MediaDeviceViewHolderLegacy(mHolderView, context);
98         }
99     }
100 
101     @Override
onBindViewHolder(@onNull RecyclerView.ViewHolder viewHolder, int position)102     public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
103         if (position >= getItemCount()) {
104             if (DEBUG) {
105                 Log.d(TAG, "Incorrect position: " + position + " list size: "
106                         + getItemCount());
107             }
108             return;
109         }
110         MediaItem currentMediaItem = mMediaItemList.get(position);
111         switch (currentMediaItem.getMediaItemType()) {
112             case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
113                 ((MediaGroupDividerViewHolderLegacy) viewHolder).onBind(
114                         currentMediaItem.getTitle());
115                 break;
116             case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
117                 ((MediaDeviceViewHolderLegacy) viewHolder).onBindPairNewDevice();
118                 break;
119             case MediaItem.MediaItemType.TYPE_DEVICE:
120                 ((MediaDeviceViewHolderLegacy) viewHolder).onBindDevice(currentMediaItem, position);
121                 break;
122             default:
123                 Log.d(TAG, "Incorrect position: " + position);
124         }
125     }
126 
getController()127     public MediaSwitchingController getController() {
128         return mController;
129     }
130 
131     /**
132      * ViewHolder for binding device view.
133      */
134     class MediaDeviceViewHolderLegacy extends MediaDeviceViewHolderBase {
135 
136         private static final int ANIM_DURATION = 500;
137 
138         final ViewGroup mContainerLayout;
139         final FrameLayout mItemLayout;
140         final FrameLayout mIconAreaLayout;
141         final ViewGroup mTextContent;
142         final TextView mTitleText;
143         final TextView mSubTitleText;
144         final TextView mVolumeValueText;
145         final ImageView mTitleIcon;
146         final ProgressBar mProgressBar;
147         final ImageView mStatusIcon;
148         final CheckBox mCheckBox;
149         final ViewGroup mEndTouchArea;
150         final ImageButton mEndClickIcon;
151         @VisibleForTesting
152         MediaOutputSeekbar mSeekBar;
153         private final float mInactiveRadius;
154         private final float mActiveRadius;
155         private String mDeviceId;
156         private ValueAnimator mCornerAnimator;
157         private ValueAnimator mVolumeAnimator;
158         private int mLatestUpdateVolume = -1;
159 
MediaDeviceViewHolderLegacy(View view, Context context)160         MediaDeviceViewHolderLegacy(View view, Context context) {
161             super(view, context);
162             mContainerLayout = view.requireViewById(R.id.device_container);
163             mItemLayout = view.requireViewById(R.id.item_layout);
164             mTextContent = view.requireViewById(R.id.text_content);
165             mTitleText = view.requireViewById(R.id.title);
166             mSubTitleText = view.requireViewById(R.id.subtitle);
167             mTitleIcon = view.requireViewById(R.id.title_icon);
168             mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress);
169             mSeekBar = view.requireViewById(R.id.volume_seekbar);
170             mStatusIcon = view.requireViewById(R.id.media_output_item_status);
171             mCheckBox = view.requireViewById(R.id.check_box);
172             mEndTouchArea = view.requireViewById(R.id.end_action_area);
173             mEndClickIcon = view.requireViewById(R.id.end_area_image_button);
174             mVolumeValueText = view.requireViewById(R.id.volume_value);
175             mIconAreaLayout = view.requireViewById(R.id.icon_area);
176             mInactiveRadius = mContext.getResources().getDimension(
177                     R.dimen.media_output_dialog_background_radius);
178             mActiveRadius = mContext.getResources().getDimension(
179                     R.dimen.media_output_dialog_active_background_radius);
180             initAnimator();
181         }
182 
onBindDevice(MediaItem mediaItem, int position)183         void onBindDevice(MediaItem mediaItem, int position) {
184             MediaDevice device = mediaItem.getMediaDevice().get();
185             mDeviceId = device.getId();
186             mItemLayout.setVisibility(View.VISIBLE);
187             mCheckBox.setVisibility(View.GONE);
188             mStatusIcon.setVisibility(View.GONE);
189             mEndTouchArea.setVisibility(View.GONE);
190             mEndClickIcon.setVisibility(View.GONE);
191             mContainerLayout.setOnClickListener(null);
192             mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
193             mSubTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
194             mVolumeValueText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
195             mIconAreaLayout.setBackground(null);
196             updateIconAreaClickListener(null);
197             updateSeekBarProgressColor();
198             updateContainerContentA11yImportance(true  /* isImportant */);
199             renderItem(mediaItem, position);
200         }
201 
202         /** Binds a ViewHolder for a "Connect a device" item. */
onBindPairNewDevice()203         void onBindPairNewDevice() {
204             mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
205             mCheckBox.setVisibility(View.GONE);
206             updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new));
207             updateItemBackground(ConnectionState.DISCONNECTED);
208             final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
209             mTitleIcon.setImageDrawable(addDrawable);
210             mTitleIcon.setImageTintList(ColorStateList.valueOf(
211                     mController.getColorSchemeLegacy().getColorItemContent()));
212             mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
213         }
214 
215         @Override
renderDeviceItem(boolean hideGroupItem, MediaDevice device, ConnectionState connectionState, boolean restrictVolumeAdjustment, GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus, View.OnClickListener clickListener, boolean deviceDisabled, String subtitle, Drawable deviceStatusIcon)216         protected void renderDeviceItem(boolean hideGroupItem, MediaDevice device,
217                 ConnectionState connectionState, boolean restrictVolumeAdjustment,
218                 GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus,
219                 View.OnClickListener clickListener, boolean deviceDisabled, String subtitle,
220                 Drawable deviceStatusIcon) {
221             if (hideGroupItem) {
222                 mItemLayout.setVisibility(View.GONE);
223                 return;
224             }
225             updateTitle(device.getName());
226             updateTitleIcon(device, connectionState, restrictVolumeAdjustment);
227             updateSeekBar(device, connectionState, restrictVolumeAdjustment,
228                     getDeviceItemContentDescription(device));
229             updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus);
230             updateLoadingIndicator(connectionState);
231             updateFullItemClickListener(clickListener);
232             updateContentAlpha(deviceDisabled);
233             updateSubtitle(subtitle);
234             updateDeviceStatusIcon(deviceStatusIcon, ongoingSessionStatus, connectionState);
235             updateItemBackground(connectionState);
236         }
237 
238         @Override
renderDeviceGroupItem()239         protected void renderDeviceGroupItem() {
240             String sessionName = mController.getSessionName() == null ? ""
241                     : mController.getSessionName().toString();
242             updateTitle(sessionName);
243             updateUnmutedVolumeIcon(null /* device */);
244             updateGroupSeekBar(getGroupItemContentDescription(sessionName));
245             updateEndAreaForDeviceGroup();
246             updateItemBackground(ConnectionState.CONNECTED);
247         }
248 
updateTitle(CharSequence title)249         void updateTitle(CharSequence title) {
250             mTitleText.setText(title);
251         }
252 
updateSeekBar(@onNull MediaDevice device, ConnectionState connectionState, boolean restrictVolumeAdjustment, String contentDescription)253         void updateSeekBar(@NonNull MediaDevice device, ConnectionState connectionState,
254                 boolean restrictVolumeAdjustment, String contentDescription) {
255             boolean showSeekBar =
256                     connectionState == ConnectionState.CONNECTED && !restrictVolumeAdjustment;
257             if (!mCornerAnimator.isRunning()) {
258                 if (showSeekBar) {
259                     updateSeekbarProgressBackground();
260                 }
261             }
262             mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
263             if (showSeekBar) {
264                 initSeekbar(device);
265                 updateContainerContentA11yImportance(false /* isImportant */);
266                 mSeekBar.setContentDescription(contentDescription);
267             } else {
268                 updateContainerContentA11yImportance(true /* isImportant */);
269             }
270         }
271 
updateGroupSeekBar(String contentDescription)272         void updateGroupSeekBar(String contentDescription) {
273             updateSeekbarProgressBackground();
274             mSeekBar.setVisibility(View.VISIBLE);
275             initGroupSeekbar();
276             updateContainerContentA11yImportance(false /* isImportant */);
277             mSeekBar.setContentDescription(contentDescription);
278         }
279 
280         /**
281          * Sets the a11y importance for the device container and it's text content. Making the
282          * container not important for a11y is required when the seekbar is visible.
283          */
updateContainerContentA11yImportance(boolean isImportant)284         private void updateContainerContentA11yImportance(boolean isImportant) {
285             mContainerLayout.setFocusable(isImportant);
286             mContainerLayout.setImportantForAccessibility(
287                     isImportant ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
288                             : View.IMPORTANT_FOR_ACCESSIBILITY_NO);
289             mTextContent.setImportantForAccessibility(
290                     isImportant ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
291                             : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
292         }
293 
updateSubtitle(@ullable String subtitle)294         void updateSubtitle(@Nullable String subtitle) {
295             if (subtitle == null) {
296                 mSubTitleText.setVisibility(View.GONE);
297             } else {
298                 mSubTitleText.setText(subtitle);
299                 mSubTitleText.setVisibility(View.VISIBLE);
300             }
301         }
302 
updateLoadingIndicator(ConnectionState connectionState)303         protected void updateLoadingIndicator(ConnectionState connectionState) {
304             if (connectionState == ConnectionState.CONNECTING) {
305                 mProgressBar.setVisibility(View.VISIBLE);
306                 mProgressBar.getIndeterminateDrawable().setTintList(ColorStateList.valueOf(
307                         mController.getColorSchemeLegacy().getColorItemContent()));
308             } else {
309                 mProgressBar.setVisibility(View.GONE);
310             }
311         }
312 
updateItemBackground(ConnectionState connectionState)313         protected void updateItemBackground(ConnectionState connectionState) {
314             boolean isConnected = connectionState == ConnectionState.CONNECTED;
315             boolean isConnecting = connectionState == ConnectionState.CONNECTING;
316 
317             // Increase corner radius for a connected state.
318             if (!mCornerAnimator.isRunning()) {  // FIXME(b/387576145): This is always True.
319                 int backgroundDrawableId =
320                         isConnected ? R.drawable.media_output_item_background_active
321                                 : R.drawable.media_output_item_background;
322                 mItemLayout.setBackground(mContext.getDrawable(backgroundDrawableId).mutate());
323             }
324 
325             // Connected or connecting state has a darker background.
326             int backgroundColor = isConnected || isConnecting
327                     ? mController.getColorSchemeLegacy().getColorConnectedItemBackground()
328                     : mController.getColorSchemeLegacy().getColorItemBackground();
329             mItemLayout.setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
330         }
331 
updateEndAreaVisibility(boolean showEndTouchArea, boolean isCheckbox)332         protected void updateEndAreaVisibility(boolean showEndTouchArea, boolean isCheckbox) {
333             mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE);
334             if (showEndTouchArea) {
335                 mCheckBox.setVisibility(isCheckbox ? View.VISIBLE : View.GONE);
336                 mEndClickIcon.setVisibility(!isCheckbox ? View.VISIBLE : View.GONE);
337             }
338         }
339 
updateSeekBarProgressColor()340         private void updateSeekBarProgressColor() {
341             mSeekBar.setProgressTintList(ColorStateList.valueOf(
342                     mController.getColorSchemeLegacy().getColorSeekbarProgress()));
343             final Drawable contrastDotDrawable =
344                     ((LayerDrawable) mSeekBar.getProgressDrawable()).findDrawableByLayerId(
345                             R.id.contrast_dot);
346             contrastDotDrawable.setTintList(ColorStateList.valueOf(
347                     mController.getColorSchemeLegacy().getColorItemContent()));
348         }
349 
updateSeekbarProgressBackground()350         void updateSeekbarProgressBackground() {
351             final ClipDrawable clipDrawable =
352                     (ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable())
353                             .findDrawableByLayerId(android.R.id.progress);
354             final GradientDrawable progressDrawable =
355                     (GradientDrawable) clipDrawable.getDrawable();
356             progressDrawable.setCornerRadii(
357                     new float[]{0, 0, mActiveRadius,
358                             mActiveRadius,
359                             mActiveRadius,
360                             mActiveRadius, 0, 0});
361         }
362 
initializeSeekbarVolume(@ullable MediaDevice device, int currentVolume)363         private void initializeSeekbarVolume(@Nullable MediaDevice device, int currentVolume) {
364             if (!isDragging()) {
365                 if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1
366                         || currentVolume == mLatestUpdateVolume)) {
367                     // Update only if volume of device and value of volume bar doesn't match.
368                     // Check if response volume match with the latest request, to ignore obsolete
369                     // response
370                     if (!mVolumeAnimator.isStarted()) {
371                         if (currentVolume == 0) {
372                             updateMutedVolumeIcon(device);
373                         } else {
374                             updateUnmutedVolumeIcon(device);
375                         }
376                         mSeekBar.setVolume(currentVolume);
377                         mLatestUpdateVolume = -1;
378                     }
379                 } else if (currentVolume == 0) {
380                     mSeekBar.resetVolume();
381                     updateMutedVolumeIcon(device);
382                 }
383                 if (currentVolume == mLatestUpdateVolume) {
384                     mLatestUpdateVolume = -1;
385                 }
386             }
387         }
388 
initSeekbar(@onNull MediaDevice device)389         void initSeekbar(@NonNull MediaDevice device) {
390             SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() {
391                 @Override
392                 public int getVolume() {
393                     return device.getCurrentVolume();
394                 }
395                 @Override
396                 public void setVolume(int volume) {
397                     mController.adjustVolume(device, volume);
398                 }
399 
400                 @Override
401                 public void onMute() {
402                     mController.logInteractionMuteDevice(device);
403                 }
404 
405                 @Override
406                 public void onUnmute() {
407                     mController.logInteractionUnmuteDevice(device);
408                 }
409             };
410 
411             if (!mController.isVolumeControlEnabled(device)) {
412                 disableSeekBar();
413             } else {
414                 enableSeekBar(volumeControl);
415             }
416             mSeekBar.setMaxVolume(device.getMaxVolume());
417             final int currentVolume = device.getCurrentVolume();
418             initializeSeekbarVolume(device, currentVolume);
419 
420             mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener(
421                     device, volumeControl) {
422                 @Override
423                 public void onStopTrackingTouch(SeekBar seekbar) {
424                     super.onStopTrackingTouch(seekbar);
425                     mController.logInteractionAdjustVolume(device);
426                 }
427             });
428         }
429 
430         // Initializes the seekbar for a group of devices.
initGroupSeekbar()431         void initGroupSeekbar() {
432             SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() {
433                 @Override
434                 public int getVolume() {
435                     return mController.getSessionVolume();
436                 }
437 
438                 @Override
439                 public void setVolume(int volume) {
440                     mController.adjustSessionVolume(volume);
441                 }
442 
443                 @Override
444                 public void onMute() {}
445 
446                 @Override
447                 public void onUnmute() {}
448             };
449 
450             if (!mController.isVolumeControlEnabledForSession()) {
451                 disableSeekBar();
452             } else {
453                 enableSeekBar(volumeControl);
454             }
455             mSeekBar.setMaxVolume(mController.getSessionVolumeMax());
456 
457             final int currentVolume = mController.getSessionVolume();
458             initializeSeekbarVolume(null, currentVolume);
459             mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener(
460                     null, volumeControl) {
461                 @Override
462                 protected boolean shouldHandleProgressChanged() {
463                     return true;
464                 }
465             });
466         }
467 
updateTitleIcon(@onNull MediaDevice device, ConnectionState connectionState, boolean restrictVolumeAdjustment)468         protected void updateTitleIcon(@NonNull MediaDevice device,
469                 ConnectionState connectionState, boolean restrictVolumeAdjustment) {
470             if (connectionState == ConnectionState.CONNECTED) {
471                 if (restrictVolumeAdjustment) {
472                     // Volume icon without a background that makes it looks like part of a seekbar.
473                     updateVolumeIcon(device, false /* isMutedIcon */);
474                 } else {
475                     updateUnmutedVolumeIcon(device);
476                 }
477             } else {
478                 setUpDeviceIcon(device);
479             }
480         }
481 
updateMutedVolumeIcon(@ullable MediaDevice device)482         void updateMutedVolumeIcon(@Nullable MediaDevice device) {
483             mIconAreaLayout.setBackground(
484                     mContext.getDrawable(R.drawable.media_output_item_background_active));
485             updateVolumeIcon(device, true /* isMutedVolumeIcon */);
486         }
487 
updateUnmutedVolumeIcon(@ullable MediaDevice device)488         void updateUnmutedVolumeIcon(@Nullable MediaDevice device) {
489             mIconAreaLayout.setBackground(
490                     mContext.getDrawable(R.drawable.media_output_title_icon_area)
491             );
492             updateVolumeIcon(device, false /* isMutedVolumeIcon */);
493         }
494 
updateVolumeIcon(@ullable MediaDevice device, boolean isMutedVolumeIcon)495         void updateVolumeIcon(@Nullable MediaDevice device, boolean isMutedVolumeIcon) {
496             boolean isInputMediaDevice = device instanceof InputMediaDevice;
497             int id = getDrawableId(isInputMediaDevice, isMutedVolumeIcon);
498             mTitleIcon.setImageDrawable(mContext.getDrawable(id));
499             mTitleIcon.setImageTintList(ColorStateList.valueOf(
500                     mController.getColorSchemeLegacy().getColorItemContent()));
501             mIconAreaLayout.setBackgroundTintList(ColorStateList.valueOf(
502                     mController.getColorSchemeLegacy().getColorSeekbarProgress()));
503         }
504 
505         @VisibleForTesting
getDrawableId(boolean isInputDevice, boolean isMutedVolumeIcon)506         int getDrawableId(boolean isInputDevice, boolean isMutedVolumeIcon) {
507             // Returns the microphone icon when the flag is enabled and the device is an input
508             // device.
509             if (Flags.enableAudioInputDeviceRoutingAndVolumeControl()
510                     && isInputDevice) {
511                 return isMutedVolumeIcon ? R.drawable.ic_mic_off : R.drawable.ic_mic_26dp;
512             }
513             return isMutedVolumeIcon
514                     ? R.drawable.media_output_icon_volume_off
515                     : R.drawable.media_output_icon_volume;
516         }
517 
updateContentAlpha(boolean deviceDisabled)518         private void updateContentAlpha(boolean deviceDisabled) {
519             float alphaValue = deviceDisabled ? DEVICE_DISABLED_ALPHA : DEVICE_ACTIVE_ALPHA;
520             mTitleIcon.setAlpha(alphaValue);
521             mTitleText.setAlpha(alphaValue);
522             mSubTitleText.setAlpha(alphaValue);
523             mStatusIcon.setAlpha(alphaValue);
524         }
525 
updateDeviceStatusIcon(@ullable Drawable deviceStatusIcon, @Nullable OngoingSessionStatus ongoingSessionStatus, ConnectionState connectionState)526         private void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon,
527                 @Nullable OngoingSessionStatus ongoingSessionStatus,
528                 ConnectionState connectionState) {
529             boolean showOngoingSession =
530                     ongoingSessionStatus != null && connectionState == ConnectionState.DISCONNECTED;
531             if (deviceStatusIcon == null && !showOngoingSession) {
532                 mStatusIcon.setVisibility(View.GONE);
533             } else {
534                 if (showOngoingSession) {
535                     mStatusIcon.setImageDrawable(
536                             mContext.getDrawable(R.drawable.ic_sound_bars_anim));
537                 } else {
538                     mStatusIcon.setImageDrawable(deviceStatusIcon);
539                 }
540                 mStatusIcon.setImageTintList(ColorStateList.valueOf(
541                         mController.getColorSchemeLegacy().getColorItemContent()));
542                 if (deviceStatusIcon instanceof AnimatedVectorDrawable) {
543                     ((AnimatedVectorDrawable) deviceStatusIcon).start();
544                 }
545                 mStatusIcon.setVisibility(View.VISIBLE);
546             }
547         }
548 
549 
550         /** Renders the right side round pill button / checkbox. */
updateEndArea(@onNull MediaDevice device, ConnectionState connectionState, @Nullable GroupStatus groupStatus, @Nullable OngoingSessionStatus ongoingSessionStatus)551         private void updateEndArea(@NonNull MediaDevice device, ConnectionState connectionState,
552                 @Nullable GroupStatus groupStatus,
553                 @Nullable OngoingSessionStatus ongoingSessionStatus) {
554             boolean showEndArea = false;
555             boolean isCheckbox = false;
556             // If both group status and the ongoing session status are present, only the ongoing
557             // session controls are displayed. The current layout design doesn't allow both group
558             // and ongoing session controls to be rendered simultaneously.
559             if (ongoingSessionStatus != null && connectionState == ConnectionState.CONNECTED) {
560                 showEndArea = true;
561                 updateEndAreaForOngoingSession(device, ongoingSessionStatus.host());
562             } else if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) {
563                 showEndArea = true;
564                 isCheckbox = true;
565                 updateEndAreaForGroupCheckBox(device, groupStatus);
566             }
567             updateEndAreaVisibility(showEndArea, isCheckbox);
568         }
569 
updateEndAreaForDeviceGroup()570         private void updateEndAreaForDeviceGroup() {
571             updateEndAreaWithIcon(
572                     v -> {
573                         onExpandGroupButtonClicked();
574                     },
575                     R.drawable.media_output_item_expand_group,
576                     R.string.accessibility_expand_group);
577             updateEndAreaVisibility(true /* showEndArea */, false /* isCheckbox */);
578         }
579 
updateEndAreaForOngoingSession(@onNull MediaDevice device, boolean isHost)580         private void updateEndAreaForOngoingSession(@NonNull MediaDevice device, boolean isHost) {
581             updateEndAreaWithIcon(
582                     v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v),
583                     isHost ? R.drawable.media_output_status_edit_session
584                             : R.drawable.ic_sound_bars_anim,
585                     R.string.accessibility_open_application);
586         }
587 
updateEndAreaWithIcon(View.OnClickListener clickListener, @DrawableRes int iconDrawableId, @StringRes int accessibilityStringId)588         private void updateEndAreaWithIcon(View.OnClickListener clickListener,
589                 @DrawableRes int iconDrawableId,
590                 @StringRes int accessibilityStringId) {
591             updateEndAreaColor(mController.getColorSchemeLegacy().getColorSeekbarProgress());
592             mEndClickIcon.setImageTintList(
593                     ColorStateList.valueOf(
594                             mController.getColorSchemeLegacy().getColorItemContent()));
595             mEndClickIcon.setOnClickListener(clickListener);
596             Drawable drawable = mContext.getDrawable(iconDrawableId);
597             mEndClickIcon.setImageDrawable(drawable);
598             if (drawable instanceof AnimatedVectorDrawable) {
599                 ((AnimatedVectorDrawable) drawable).start();
600             }
601             mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId));
602         }
603 
updateEndAreaForGroupCheckBox(@onNull MediaDevice device, @NonNull GroupStatus groupStatus)604         private void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device,
605                 @NonNull GroupStatus groupStatus) {
606             boolean isEnabled = isGroupCheckboxEnabled(groupStatus);
607             updateEndAreaColor(groupStatus.selected()
608                     ? mController.getColorSchemeLegacy().getColorSeekbarProgress()
609                     : mController.getColorSchemeLegacy().getColorItemBackground());
610             mCheckBox.setContentDescription(mContext.getString(
611                     groupStatus.selected() ? R.string.accessibility_remove_device_from_group
612                             : R.string.accessibility_add_device_to_group));
613             mCheckBox.setOnCheckedChangeListener(null);
614             mCheckBox.setChecked(groupStatus.selected());
615             mCheckBox.setOnCheckedChangeListener(
616                     isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered(
617                             !groupStatus.selected(), device) : null);
618             mCheckBox.setEnabled(isEnabled);
619             setCheckBoxColor(mCheckBox, mController.getColorSchemeLegacy().getColorItemContent());
620         }
621 
setCheckBoxColor(CheckBox checkBox, int color)622         private void setCheckBoxColor(CheckBox checkBox, int color) {
623             checkBox.setForegroundTintList(ColorStateList.valueOf(color));
624         }
625 
shouldShowGroupCheckbox(@onNull GroupStatus groupStatus)626         private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) {
627             if (Flags.enableOutputSwitcherDeviceGrouping()) {
628                 return isGroupCheckboxEnabled(groupStatus);
629             }
630             return true;
631         }
632 
isGroupCheckboxEnabled(@onNull GroupStatus groupStatus)633         private boolean isGroupCheckboxEnabled(@NonNull GroupStatus groupStatus) {
634             boolean disabled = groupStatus.selected() && !groupStatus.deselectable();
635             return !disabled;
636         }
637 
updateEndAreaColor(int color)638         private void updateEndAreaColor(int color) {
639             mEndTouchArea.setBackgroundTintList(
640                     ColorStateList.valueOf(color));
641         }
642 
updateFullItemClickListener(@ullable View.OnClickListener listener)643         private void updateFullItemClickListener(@Nullable View.OnClickListener listener) {
644             mContainerLayout.setOnClickListener(listener);
645         }
646 
updateIconAreaClickListener(@ullable View.OnClickListener listener)647         void updateIconAreaClickListener(@Nullable View.OnClickListener listener) {
648             mIconAreaLayout.setOnClickListener(listener);
649             if (listener == null) {
650                 mIconAreaLayout.setClickable(false); // clickable is not removed automatically.
651             }
652         }
653 
initAnimator()654         private void initAnimator() {
655             mCornerAnimator = ValueAnimator.ofFloat(mInactiveRadius, mActiveRadius);
656             mCornerAnimator.setDuration(ANIM_DURATION);
657             mCornerAnimator.setInterpolator(new LinearInterpolator());
658 
659             mVolumeAnimator = ValueAnimator.ofInt();
660             mVolumeAnimator.addUpdateListener(animation -> {
661                 int value = (int) animation.getAnimatedValue();
662                 mSeekBar.setProgress(value);
663             });
664             mVolumeAnimator.setDuration(ANIM_DURATION);
665             mVolumeAnimator.setInterpolator(new LinearInterpolator());
666             mVolumeAnimator.addListener(new Animator.AnimatorListener() {
667                 @Override
668                 public void onAnimationStart(Animator animation) {
669                     mSeekBar.setEnabled(false);
670                 }
671 
672                 @Override
673                 public void onAnimationEnd(Animator animation) {
674                     mSeekBar.setEnabled(true);
675                 }
676 
677                 @Override
678                 public void onAnimationCancel(Animator animation) {
679                     mSeekBar.setEnabled(true);
680                 }
681 
682                 @Override
683                 public void onAnimationRepeat(Animator animation) {
684 
685                 }
686             });
687         }
688 
689         @Override
disableSeekBar()690         protected void disableSeekBar() {
691             mSeekBar.setEnabled(false);
692             mSeekBar.setOnTouchListener((v, event) -> true);
693             updateIconAreaClickListener(null);
694         }
695 
enableSeekBar(SeekBarVolumeControl volumeControl)696         private void enableSeekBar(SeekBarVolumeControl volumeControl) {
697             mSeekBar.setEnabled(true);
698 
699             mSeekBar.setOnTouchListener((v, event) -> false);
700             updateIconAreaClickListener((v) -> {
701                 if (volumeControl.getVolume() == 0) {
702                     volumeControl.onUnmute();
703                     mSeekBar.setVolume(UNMUTE_DEFAULT_VOLUME);
704                     volumeControl.setVolume(UNMUTE_DEFAULT_VOLUME);
705                     updateUnmutedVolumeIcon(null);
706                     mIconAreaLayout.setOnTouchListener(((iconV, event) -> false));
707                 } else {
708                     volumeControl.onMute();
709                     mSeekBar.resetVolume();
710                     volumeControl.setVolume(0);
711                     updateMutedVolumeIcon(null);
712                     mIconAreaLayout.setOnTouchListener(((iconV, event) -> {
713                         mSeekBar.dispatchTouchEvent(event);
714                         return false;
715                     }));
716                 }
717             });
718 
719         }
720 
setUpDeviceIcon(@onNull MediaDevice device)721         protected void setUpDeviceIcon(@NonNull MediaDevice device) {
722             mBackgroundExecutor.execute(() -> {
723                 Icon icon = mController.getDeviceIconCompat(device).toIcon(mContext);
724                 mMainExecutor.execute(() -> {
725                     if (!TextUtils.equals(mDeviceId, device.getId())) {
726                         return;
727                     }
728                     mTitleIcon.setImageIcon(icon);
729                     mTitleIcon.setImageTintList(ColorStateList.valueOf(
730                             mController.getColorSchemeLegacy().getColorItemContent()));
731                 });
732             });
733         }
734 
735         interface SeekBarVolumeControl {
getVolume()736             int getVolume();
setVolume(int volume)737             void setVolume(int volume);
onMute()738             void onMute();
onUnmute()739             void onUnmute();
740         }
741 
742         private abstract class MediaSeekBarChangedListener
743                 implements SeekBar.OnSeekBarChangeListener {
744             boolean mStartFromMute = false;
745             private MediaDevice mMediaDevice;
746             private SeekBarVolumeControl mVolumeControl;
747 
MediaSeekBarChangedListener(MediaDevice device, SeekBarVolumeControl volumeControl)748             MediaSeekBarChangedListener(MediaDevice device, SeekBarVolumeControl volumeControl) {
749                 mMediaDevice = device;
750                 mVolumeControl = volumeControl;
751             }
752 
753             @Override
onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)754             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
755                 if (!shouldHandleProgressChanged() || !fromUser) {
756                     return;
757                 }
758 
759                 final String percentageString = mContext.getResources().getString(
760                         R.string.media_output_dialog_volume_percentage,
761                         mSeekBar.getPercentage());
762                 mVolumeValueText.setText(percentageString);
763 
764                 if (mStartFromMute) {
765                     updateUnmutedVolumeIcon(mMediaDevice);
766                     mStartFromMute = false;
767                 }
768 
769                 int seekBarVolume = MediaOutputSeekbar.scaleProgressToVolume(progress);
770                 if (seekBarVolume != mVolumeControl.getVolume()) {
771                     mLatestUpdateVolume = seekBarVolume;
772                     mVolumeControl.setVolume(seekBarVolume);
773                 }
774             }
775 
776             @Override
onStartTrackingTouch(SeekBar seekBar)777             public void onStartTrackingTouch(SeekBar seekBar) {
778                 mTitleIcon.setVisibility(View.INVISIBLE);
779                 mVolumeValueText.setVisibility(View.VISIBLE);
780                 int currentVolume = MediaOutputSeekbar.scaleProgressToVolume(
781                         seekBar.getProgress());
782                 mStartFromMute = (currentVolume == 0);
783                 setIsDragging(true);
784             }
785 
786             @Override
onStopTrackingTouch(SeekBar seekBar)787             public void onStopTrackingTouch(SeekBar seekBar) {
788                 int currentVolume = MediaOutputSeekbar.scaleProgressToVolume(
789                         seekBar.getProgress());
790                 if (currentVolume == 0) {
791                     seekBar.setProgress(0);
792                     updateMutedVolumeIcon(mMediaDevice);
793                 } else {
794                     updateUnmutedVolumeIcon(mMediaDevice);
795                 }
796                 mTitleIcon.setVisibility(View.VISIBLE);
797                 mVolumeValueText.setVisibility(View.GONE);
798                 setIsDragging(false);
799             }
shouldHandleProgressChanged()800             protected boolean shouldHandleProgressChanged() {
801                 return mMediaDevice != null;
802             }
803         };
804     }
805 
806     class MediaGroupDividerViewHolderLegacy extends RecyclerView.ViewHolder {
807         final TextView mTitleText;
808 
MediaGroupDividerViewHolderLegacy(@onNull View itemView)809         MediaGroupDividerViewHolderLegacy(@NonNull View itemView) {
810             super(itemView);
811             mTitleText = itemView.requireViewById(R.id.title);
812         }
813 
onBind(String groupDividerTitle)814         void onBind(String groupDividerTitle) {
815             mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
816             mTitleText.setText(groupDividerTitle);
817         }
818     }
819 }
820