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