1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.tv.media; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.content.res.Resources; 22 import android.graphics.drawable.Drawable; 23 import android.os.Bundle; 24 import android.text.TextUtils; 25 import android.util.Log; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.ImageView; 30 import android.widget.RadioButton; 31 import android.widget.TextView; 32 33 import androidx.annotation.NonNull; 34 35 import com.android.internal.widget.RecyclerView; 36 import com.android.settingslib.media.LocalMediaManager; 37 import com.android.settingslib.media.MediaDevice; 38 import com.android.systemui.media.dialog.MediaItem; 39 import com.android.systemui.media.dialog.MediaOutputController; 40 import com.android.systemui.tv.res.R; 41 42 import java.util.List; 43 import java.util.concurrent.CopyOnWriteArrayList; 44 45 /** 46 * Adapter for showing the {@link MediaItem}s in the {@link TvMediaOutputDialogActivity}. 47 */ 48 public class TvMediaOutputAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { 49 50 private static final String TAG = TvMediaOutputAdapter.class.getSimpleName(); 51 private static final boolean DEBUG = false; 52 53 private final TvMediaOutputController mMediaOutputController; 54 private final MediaOutputController.Callback mCallback; 55 private final Context mContext; 56 protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>(); 57 58 private final int mFocusedRadioTint; 59 private final int mUnfocusedRadioTint; 60 private final int mCheckedRadioTint; 61 TvMediaOutputAdapter(Context context, TvMediaOutputController mediaOutputController, MediaOutputController.Callback callback)62 TvMediaOutputAdapter(Context context, TvMediaOutputController mediaOutputController, 63 MediaOutputController.Callback callback) { 64 mContext = context; 65 mMediaOutputController = mediaOutputController; 66 mCallback = callback; 67 68 Resources res = mContext.getResources(); 69 mFocusedRadioTint = res.getColor(R.color.media_dialog_radio_button_focused); 70 mUnfocusedRadioTint = res.getColor(R.color.media_dialog_radio_button_unfocused); 71 mCheckedRadioTint = res.getColor(R.color.media_dialog_radio_button_checked); 72 73 setHasStableIds(true); 74 } 75 76 @Override getItemViewType(int position)77 public int getItemViewType(int position) { 78 if (position >= mMediaItemList.size()) { 79 Log.e(TAG, "Incorrect position for item type: " + position); 80 return MediaItem.MediaItemType.TYPE_GROUP_DIVIDER; 81 } 82 return mMediaItemList.get(position).getMediaItemType(); 83 } 84 85 @Override onCreateViewHolder(ViewGroup parent, int viewType)86 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 87 View mHolderView = LayoutInflater.from(mContext) 88 .inflate(MediaItem.getMediaLayoutId(viewType), parent, false); 89 90 switch (viewType) { 91 case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: 92 return new DividerViewHolder(mHolderView); 93 case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: 94 case MediaItem.MediaItemType.TYPE_DEVICE: 95 return new DeviceViewHolder(mHolderView); 96 default: 97 Log.e(TAG, "unknown viewType: " + viewType); 98 return new DeviceViewHolder(mHolderView); 99 } 100 } 101 102 @Override onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position)103 public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { 104 if (position >= getItemCount()) { 105 Log.e(TAG, "Tried to bind at position > list size (" + getItemCount() + ")"); 106 } 107 108 MediaItem currentMediaItem = mMediaItemList.get(position); 109 switch (currentMediaItem.getMediaItemType()) { 110 case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER -> 111 ((DividerViewHolder) viewHolder).onBind(currentMediaItem.getTitle()); 112 case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE -> 113 ((DeviceViewHolder) viewHolder).onBindNewDevice(); 114 case MediaItem.MediaItemType.TYPE_DEVICE -> ((DeviceViewHolder) viewHolder).onBind( 115 currentMediaItem.getMediaDevice().get(), position); 116 default -> Log.d(TAG, "Incorrect position: " + position); 117 } 118 } 119 120 @Override getItemCount()121 public int getItemCount() { 122 return mMediaItemList.size(); 123 } 124 125 @Override getItemId(int position)126 public long getItemId(int position) { 127 MediaItem item = mMediaItemList.get(position); 128 if (item.getMediaDevice().isPresent()) { 129 return item.getMediaDevice().get().getId().hashCode(); 130 } 131 if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) { 132 if (item.getTitle() == null || item.getTitle().isEmpty()) { 133 return MediaItem.MediaItemType.TYPE_GROUP_DIVIDER; 134 } 135 return item.getTitle().hashCode(); 136 } 137 return item.getMediaItemType(); 138 } 139 updateItems()140 public void updateItems() { 141 mMediaItemList.clear(); 142 mMediaItemList.addAll(mMediaOutputController.getMediaItemList()); 143 if (DEBUG) { 144 Log.d(TAG, "updateItems"); 145 for (MediaItem mediaItem : mMediaItemList) { 146 Log.d(TAG, mediaItem.toString()); 147 } 148 } 149 notifyDataSetChanged(); 150 } 151 152 private class DeviceViewHolder extends RecyclerView.ViewHolder { 153 final ImageView mIcon; 154 final TextView mTitle; 155 final TextView mSubtitle; 156 final RadioButton mRadioButton; 157 DeviceViewHolder(View itemView)158 DeviceViewHolder(View itemView) { 159 super(itemView); 160 mIcon = itemView.requireViewById(R.id.media_output_item_icon); 161 mTitle = itemView.requireViewById(R.id.media_dialog_item_title); 162 mSubtitle = itemView.requireViewById(R.id.media_dialog_item_subtitle); 163 mRadioButton = itemView.requireViewById(R.id.media_dialog_radio_button); 164 } 165 onBind(MediaDevice mediaDevice, int position)166 void onBind(MediaDevice mediaDevice, int position) { 167 // Title 168 mTitle.setText(mediaDevice.getName()); 169 170 // Subtitle 171 setSummary(mediaDevice); 172 173 // Icon 174 Drawable icon; 175 if (mediaDevice.getState() 176 == LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED) { 177 icon = 178 mContext.getDrawable( 179 com.android.systemui.R.drawable.media_output_status_failed); 180 } else { 181 icon = mediaDevice.getIconWithoutBackground(); 182 } 183 if (icon == null) { 184 if (DEBUG) Log.d(TAG, "Using default icon for " + mediaDevice); 185 icon = mContext.getDrawable( 186 com.android.settingslib.R.drawable.ic_media_speaker_device); 187 } 188 mIcon.setImageDrawable(icon); 189 190 mRadioButton.setVisibility(mediaDevice.isConnected() ? View.VISIBLE : View.GONE); 191 mRadioButton.setChecked(isCurrentlyConnected(mediaDevice)); 192 setRadioButtonColor(); 193 194 itemView.setOnFocusChangeListener((view, focused) -> { 195 setSummary(mediaDevice); 196 setRadioButtonColor(); 197 mTitle.setSelected(focused); 198 mSubtitle.setSelected(focused); 199 }); 200 201 itemView.setOnClickListener(v -> transferOutput(mediaDevice)); 202 } 203 setRadioButtonColor()204 private void setRadioButtonColor() { 205 if (itemView.hasFocus()) { 206 mRadioButton.getButtonDrawable().setTint( 207 mRadioButton.isChecked() ? mCheckedRadioTint : mFocusedRadioTint); 208 } else { 209 mRadioButton.getButtonDrawable().setTint(mUnfocusedRadioTint); 210 } 211 } 212 setSummary(MediaDevice mediaDevice)213 private void setSummary(MediaDevice mediaDevice) { 214 CharSequence summary; 215 if (mediaDevice.getState() 216 == LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED) { 217 summary = mContext.getString( 218 com.android.systemui.R.string.media_output_dialog_connect_failed); 219 } else { 220 summary = mediaDevice.getSummaryForTv(itemView.hasFocus() 221 ? R.color.media_dialog_low_battery_focused 222 : R.color.media_dialog_low_battery_unfocused); 223 } 224 225 mSubtitle.setText(summary); 226 mSubtitle.setVisibility(summary == null || summary.isEmpty() 227 ? View.GONE : View.VISIBLE); 228 } 229 transferOutput(MediaDevice mediaDevice)230 private void transferOutput(MediaDevice mediaDevice) { 231 if (mMediaOutputController.isAnyDeviceTransferring()) { 232 // Don't interrupt ongoing transfer 233 return; 234 } 235 if (isCurrentlyConnected(mediaDevice)) { 236 if (DEBUG) Log.d(TAG, "Device is already selected as the active output"); 237 return; 238 } 239 mMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mediaDevice); 240 mMediaOutputController.connectDevice(mediaDevice); 241 mediaDevice.setState(LocalMediaManager.MediaDeviceState.STATE_CONNECTING); 242 notifyDataSetChanged(); 243 } 244 245 /** 246 * The single currentConnected device or the only selected device 247 */ isCurrentlyConnected(MediaDevice device)248 boolean isCurrentlyConnected(MediaDevice device) { 249 return TextUtils.equals(device.getId(), 250 mMediaOutputController.getCurrentConnectedMediaDevice().getId()) 251 || (mMediaOutputController.getSelectedMediaDevice().size() == 1 252 && isDeviceIncluded(mMediaOutputController.getSelectedMediaDevice(), device)); 253 } 254 onBindNewDevice()255 void onBindNewDevice() { 256 mIcon.setImageResource(com.android.systemui.R.drawable.ic_add); 257 mTitle.setText(R.string.media_output_dialog_pairing_new); 258 mSubtitle.setVisibility(View.GONE); 259 mRadioButton.setVisibility(View.GONE); 260 261 itemView.setOnClickListener(v -> launchBluetoothSettings()); 262 } 263 launchBluetoothSettings()264 private void launchBluetoothSettings() { 265 mCallback.dismissDialog(); 266 267 Intent bluetoothIntent = new Intent("android.settings.SLICE_SETTINGS"); 268 Bundle extra = new Bundle(); 269 extra.putString("slice_uri", 270 "content://com.google.android.tv.btservices.settings.sliceprovider/general"); 271 bluetoothIntent.putExtras(extra); 272 bluetoothIntent.addFlags( 273 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 274 mContext.startActivity(bluetoothIntent); 275 } 276 isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice)277 private boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) { 278 for (MediaDevice device : deviceList) { 279 if (TextUtils.equals(device.getId(), targetDevice.getId())) { 280 return true; 281 } 282 } 283 return false; 284 } 285 } 286 287 private static class DividerViewHolder extends RecyclerView.ViewHolder { 288 final TextView mHeaderText; 289 final View mDividerLine; 290 DividerViewHolder(@onNull View itemView)291 DividerViewHolder(@NonNull View itemView) { 292 super(itemView); 293 mHeaderText = itemView.requireViewById(R.id.media_output_group_header); 294 mDividerLine = itemView.requireViewById(R.id.media_output_divider_line); 295 } 296 onBind(String groupDividerTitle)297 void onBind(String groupDividerTitle) { 298 boolean hasText = groupDividerTitle != null && !groupDividerTitle.isEmpty(); 299 mHeaderText.setVisibility(hasText ? View.VISIBLE : View.GONE); 300 mDividerLine.setVisibility(hasText ? View.GONE : View.VISIBLE); 301 if (hasText) { 302 mHeaderText.setText(groupDividerTitle); 303 } 304 } 305 306 } 307 } 308