• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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