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