1 /* 2 * Copyright (C) 2024 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.app.KeyguardManager; 20 import android.content.Context; 21 import android.graphics.Rect; 22 import android.graphics.drawable.Drawable; 23 import android.media.AudioManager; 24 import android.media.session.MediaSessionManager; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.os.PowerExemptionManager; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 34 import androidx.annotation.NonNull; 35 import androidx.fragment.app.Fragment; 36 import androidx.fragment.app.FragmentManager; 37 import androidx.recyclerview.widget.LinearLayoutManager; 38 import androidx.recyclerview.widget.RecyclerView; 39 40 import com.android.settingslib.bluetooth.LocalBluetoothManager; 41 import com.android.settingslib.media.MediaDevice; 42 import com.android.systemui.animation.DialogTransitionAnimator; 43 import com.android.systemui.flags.FeatureFlags; 44 import com.android.systemui.media.dialog.MediaSwitchingController; 45 import com.android.systemui.media.nearby.NearbyMediaDevicesManager; 46 import com.android.systemui.plugins.ActivityStarter; 47 import com.android.systemui.settings.UserTracker; 48 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; 49 import com.android.systemui.tv.res.R; 50 import com.android.systemui.volume.panel.domain.interactor.VolumePanelGlobalStateInteractor; 51 52 import javax.annotation.Nullable; 53 import javax.inject.Inject; 54 55 public class OutputDevicesFragment extends Fragment 56 implements MediaSwitchingController.Callback, TvMediaOutputAdapter.PanelCallback { 57 58 private static final String TAG = OutputDevicesFragment.class.getSimpleName(); 59 private static final boolean DEBUG = false; 60 61 private TvMediaOutputController mMediaOutputController; 62 private TvMediaOutputAdapter mAdapter; 63 private RecyclerView mDevicesRecyclerView; 64 65 private final MediaSessionManager mMediaSessionManager; 66 private final LocalBluetoothManager mLocalBluetoothManager; 67 private final ActivityStarter mActivityStarter; 68 private final CommonNotifCollection mCommonNotifCollection; 69 private final DialogTransitionAnimator mDialogTransitionAnimator; 70 private final NearbyMediaDevicesManager mNearbyMediaDevicesManager; 71 private final AudioManager mAudioManager; 72 private final PowerExemptionManager mPowerExemptionManager; 73 private final KeyguardManager mKeyguardManager; 74 private final FeatureFlags mFeatureFlags; 75 private final VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor; 76 private final UserTracker mUserTracker; 77 78 protected final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 79 private String mActiveDeviceId; 80 81 @Inject OutputDevicesFragment( MediaSessionManager mediaSessionManager, @Nullable LocalBluetoothManager localBluetoothManager, ActivityStarter activityStarter, CommonNotifCollection commonNotifCollection, DialogTransitionAnimator dialogTransitionAnimator, NearbyMediaDevicesManager nearbyMediaDevicesManager, AudioManager audioManager, PowerExemptionManager powerExemptionManager, KeyguardManager keyguardManager, FeatureFlags featureFlags, VolumePanelGlobalStateInteractor volumePanelGlobalStateInteractor, UserTracker userTracker)82 public OutputDevicesFragment( 83 MediaSessionManager mediaSessionManager, 84 @Nullable LocalBluetoothManager localBluetoothManager, 85 ActivityStarter activityStarter, 86 CommonNotifCollection commonNotifCollection, 87 DialogTransitionAnimator dialogTransitionAnimator, 88 NearbyMediaDevicesManager nearbyMediaDevicesManager, 89 AudioManager audioManager, 90 PowerExemptionManager powerExemptionManager, 91 KeyguardManager keyguardManager, 92 FeatureFlags featureFlags, 93 VolumePanelGlobalStateInteractor volumePanelGlobalStateInteractor, 94 UserTracker userTracker) { 95 mMediaSessionManager = mediaSessionManager; 96 mLocalBluetoothManager = localBluetoothManager; 97 mActivityStarter = activityStarter; 98 mCommonNotifCollection = commonNotifCollection; 99 mDialogTransitionAnimator = dialogTransitionAnimator; 100 mNearbyMediaDevicesManager = nearbyMediaDevicesManager; 101 mAudioManager = audioManager; 102 mPowerExemptionManager = powerExemptionManager; 103 mKeyguardManager = keyguardManager; 104 mFeatureFlags = featureFlags; 105 mVolumePanelGlobalStateInteractor = volumePanelGlobalStateInteractor; 106 mUserTracker = userTracker; 107 } 108 109 @Override onCreate(Bundle savedInstanceState)110 public void onCreate(Bundle savedInstanceState) { 111 super.onCreate(savedInstanceState); 112 mMediaOutputController = 113 new TvMediaOutputController( 114 getContext(), 115 getContext().getPackageName(), 116 mMediaSessionManager, 117 mLocalBluetoothManager, 118 mActivityStarter, 119 mCommonNotifCollection, 120 mDialogTransitionAnimator, 121 mNearbyMediaDevicesManager, 122 mAudioManager, 123 mPowerExemptionManager, 124 mKeyguardManager, 125 mFeatureFlags, 126 mVolumePanelGlobalStateInteractor, 127 mUserTracker); 128 mAdapter = new TvMediaOutputAdapter(getContext(), mMediaOutputController, this); 129 } 130 131 @Nullable 132 @Override onCreateView( @onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)133 public View onCreateView( 134 @NonNull LayoutInflater inflater, 135 @Nullable ViewGroup container, 136 @Nullable Bundle savedInstanceState) { 137 View view = inflater.inflate(R.layout.media_output_main_fragment, null); 138 139 mDevicesRecyclerView = view.requireViewById(R.id.device_list); 140 mDevicesRecyclerView.setLayoutManager(new LayoutManagerWrapper(view.getContext())); 141 mDevicesRecyclerView.setAdapter(mAdapter); 142 143 mDevicesRecyclerView.addOnScrollListener( 144 new RecyclerView.OnScrollListener() { 145 @Override 146 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 147 super.onScrolled(recyclerView, dx, dy); 148 Drawable foreground = FadingEdgeUtil.getForegroundDrawable( 149 recyclerView, requireContext()); 150 if (foreground != recyclerView.getForeground()) { 151 recyclerView.setForeground(foreground); 152 } 153 } 154 }); 155 156 int itemSpacingPx = getResources().getDimensionPixelSize(R.dimen.media_dialog_item_spacing); 157 mDevicesRecyclerView.addItemDecoration(new SpacingDecoration(itemSpacingPx)); 158 159 return view; 160 } 161 162 @Override onStart()163 public void onStart() { 164 super.onStart(); 165 mMediaOutputController.start(this); 166 } 167 168 @Override onStop()169 public void onStop() { 170 mMediaOutputController.stop(); 171 super.onStop(); 172 } 173 174 @Override onResume()175 public void onResume() { 176 super.onResume(); 177 if (DEBUG) Log.d(TAG, "resuming OutputDevicesFragment"); 178 int position = mAdapter.getFocusPosition(); 179 mDevicesRecyclerView.getLayoutManager().scrollToPosition(position); 180 // Ensure layout is complete before requesting focus. 181 mDevicesRecyclerView.post(() -> { 182 View itemToFocus = mDevicesRecyclerView.getLayoutManager().findViewByPosition(position); 183 if (itemToFocus != null) { 184 itemToFocus.requestFocus(); 185 } 186 }); 187 } 188 refresh(boolean deviceSetChanged)189 private void refresh(boolean deviceSetChanged) { 190 if (DEBUG) Log.d(TAG, "refresh: deviceSetChanged " + deviceSetChanged); 191 // If the dialog is going away or is already refreshing, do nothing. 192 if (mMediaOutputController.isRefreshing()) { 193 return; 194 } 195 mMediaOutputController.setRefreshing(true); 196 mAdapter.updateItems(); 197 } 198 199 @Override onMediaChanged()200 public void onMediaChanged() { 201 // NOOP 202 } 203 204 @Override onMediaStoppedOrPaused()205 public void onMediaStoppedOrPaused() { 206 // NOOP 207 } 208 209 @Override onRouteChanged()210 public void onRouteChanged() { 211 mMainThreadHandler.post(() -> refresh(/* deviceSetChanged= */ false)); 212 MediaDevice activeDevice = mMediaOutputController.getCurrentConnectedMediaDevice(); 213 if (mActiveDeviceId != null && !mActiveDeviceId.equals(activeDevice.getId())) { 214 mMediaOutputController.showVolumeDialog(); 215 } 216 mActiveDeviceId = activeDevice.getId(); 217 } 218 219 @Override onDeviceListChanged()220 public void onDeviceListChanged() { 221 mMainThreadHandler.post(() -> refresh(/* deviceSetChanged= */ true)); 222 if (mActiveDeviceId == null 223 && mMediaOutputController.getCurrentConnectedMediaDevice() != null) { 224 mActiveDeviceId = mMediaOutputController.getCurrentConnectedMediaDevice().getId(); 225 } 226 } 227 228 @Override dismissDialog()229 public void dismissDialog() { 230 if (DEBUG) Log.d(TAG, "dismissDialog"); 231 if (getActivity() != null) { 232 getActivity().finish(); 233 } 234 } 235 236 @Override openDeviceSettings( String uri, CharSequence title, CharSequence subtitle, String id)237 public void openDeviceSettings( 238 String uri, CharSequence title, CharSequence subtitle, String id) { 239 FragmentManager fragmentManager = getParentFragmentManager(); 240 Bundle deviceInfo = new Bundle(); 241 deviceInfo.putString("uri", uri); 242 deviceInfo.putCharSequence("title", title); 243 deviceInfo.putCharSequence("subtitle", subtitle); 244 deviceInfo.putString("deviceId", id); 245 fragmentManager.setFragmentResult("deviceSettings", deviceInfo); 246 } 247 248 private class LayoutManagerWrapper extends LinearLayoutManager { LayoutManagerWrapper(Context context)249 LayoutManagerWrapper(Context context) { 250 super(context); 251 } 252 253 @Override onLayoutCompleted(RecyclerView.State state)254 public void onLayoutCompleted(RecyclerView.State state) { 255 super.onLayoutCompleted(state); 256 mMediaOutputController.setRefreshing(false); 257 mMediaOutputController.refreshDataSetIfNeeded(); 258 } 259 } 260 261 private static class SpacingDecoration extends RecyclerView.ItemDecoration { 262 private final int mMarginPx; 263 SpacingDecoration(int marginPx)264 SpacingDecoration(int marginPx) { 265 mMarginPx = marginPx; 266 } 267 268 @Override getItemOffsets( Rect outRect, View view, RecyclerView parent, RecyclerView.State state)269 public void getItemOffsets( 270 Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 271 if (parent.getChildAdapterPosition(view) == 0) { 272 outRect.top = mMarginPx; 273 } 274 outRect.bottom = mMarginPx; 275 } 276 } 277 } 278