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.annotation.SuppressLint;
20 import android.app.Activity;
21 import android.app.KeyguardManager;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.graphics.Rect;
25 import android.media.AudioManager;
26 import android.media.MediaRouter2;
27 import android.media.session.MediaSessionManager;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.os.PowerExemptionManager;
32 import android.util.DisplayMetrics;
33 import android.util.Log;
34 import android.view.Gravity;
35 import android.view.View;
36 import android.view.Window;
37 import android.view.WindowManager;
38 
39 import com.android.internal.widget.LinearLayoutManager;
40 import com.android.internal.widget.RecyclerView;
41 import com.android.settingslib.bluetooth.LocalBluetoothManager;
42 import com.android.settingslib.media.MediaDevice;
43 import com.android.settingslib.media.flags.Flags;
44 import com.android.systemui.animation.DialogTransitionAnimator;
45 import com.android.systemui.flags.FeatureFlags;
46 import com.android.systemui.media.dialog.MediaOutputController;
47 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
48 import com.android.systemui.plugins.ActivityStarter;
49 import com.android.systemui.settings.UserTracker;
50 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
51 import com.android.systemui.tv.res.R;
52 
53 import java.util.Collections;
54 
55 import javax.annotation.Nullable;
56 import javax.inject.Inject;
57 
58 /**
59  * A TV specific variation of the {@link com.android.systemui.media.dialog.MediaOutputDialog}.
60  * This activity allows the user to select a default audio output, which is not based on the
61  * currently playing media.
62  * There are two entry points for the dialog, either by sending a broadcast via the
63  * {@link com.android.systemui.media.dialog.MediaOutputDialogReceiver} or by calling
64  * {@link MediaRouter2#showSystemOutputSwitcher()}
65  */
66 public class TvMediaOutputDialogActivity extends Activity
67         implements MediaOutputController.Callback {
68     private static final String TAG = TvMediaOutputDialogActivity.class.getSimpleName();
69     private static final boolean DEBUG = false;
70 
71     private TvMediaOutputController mMediaOutputController;
72     private TvMediaOutputAdapter mAdapter;
73 
74     private final MediaSessionManager mMediaSessionManager;
75     private final LocalBluetoothManager mLocalBluetoothManager;
76     private final ActivityStarter mActivityStarter;
77     private final CommonNotifCollection mCommonNotifCollection;
78     private final DialogTransitionAnimator mDialogTransitionAnimator;
79     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
80     private final AudioManager mAudioManager;
81     private final PowerExemptionManager mPowerExemptionManager;
82     private final KeyguardManager mKeyguardManager;
83     private final FeatureFlags mFeatureFlags;
84     private final UserTracker mUserTracker;
85 
86     protected final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
87     private String mActiveDeviceId;
88 
89     @Inject
TvMediaOutputDialogActivity( MediaSessionManager mediaSessionManager, @Nullable LocalBluetoothManager localBluetoothManager, ActivityStarter activityStarter, CommonNotifCollection commonNotifCollection, DialogTransitionAnimator dialogTransitionAnimator, NearbyMediaDevicesManager nearbyMediaDevicesManager, AudioManager audioManager, PowerExemptionManager powerExemptionManager, KeyguardManager keyguardManager, FeatureFlags featureFlags, UserTracker userTracker)90     public TvMediaOutputDialogActivity(
91             MediaSessionManager mediaSessionManager,
92             @Nullable LocalBluetoothManager localBluetoothManager,
93             ActivityStarter activityStarter,
94             CommonNotifCollection commonNotifCollection,
95             DialogTransitionAnimator dialogTransitionAnimator,
96             NearbyMediaDevicesManager nearbyMediaDevicesManager,
97             AudioManager audioManager,
98             PowerExemptionManager powerExemptionManager,
99             KeyguardManager keyguardManager,
100             FeatureFlags featureFlags,
101             UserTracker userTracker) {
102         mMediaSessionManager = mediaSessionManager;
103         mLocalBluetoothManager = localBluetoothManager;
104         mActivityStarter = activityStarter;
105         mCommonNotifCollection = commonNotifCollection;
106         mDialogTransitionAnimator = dialogTransitionAnimator;
107         mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
108         mAudioManager = audioManager;
109         mPowerExemptionManager = powerExemptionManager;
110         mKeyguardManager = keyguardManager;
111         mFeatureFlags = featureFlags;
112         mUserTracker = userTracker;
113     }
114 
115     @SuppressLint("MissingPermission")
116     @Override
onCreate(Bundle savedInstanceState)117     public void onCreate(Bundle savedInstanceState) {
118         super.onCreate(savedInstanceState);
119         if (DEBUG) Log.d(TAG, "package name: " + getPackageName());
120 
121         if (!Flags.enableTvMediaOutputDialog()) {
122             finish();
123             return;
124         }
125 
126         setContentView(R.layout.media_output_dialog);
127 
128         mMediaOutputController = new TvMediaOutputController(this, getPackageName(),
129                 mMediaSessionManager, mLocalBluetoothManager, mActivityStarter,
130                 mCommonNotifCollection, mDialogTransitionAnimator, mNearbyMediaDevicesManager,
131                 mAudioManager, mPowerExemptionManager, mKeyguardManager, mFeatureFlags,
132                 mUserTracker);
133         mAdapter = new TvMediaOutputAdapter(this, mMediaOutputController, this);
134 
135         Resources res = getResources();
136         DisplayMetrics metrics = res.getDisplayMetrics();
137         int screenWidth = metrics.widthPixels;
138         int screenHeight = metrics.heightPixels;
139         int marginVerticalPx = res.getDimensionPixelSize(R.dimen.media_dialog_margin_vertical);
140         int marginEndPx = res.getDimensionPixelSize(R.dimen.media_dialog_margin_end);
141 
142         final Window window = getWindow();
143         final WindowManager.LayoutParams lp = window.getAttributes();
144         lp.gravity = Gravity.getAbsoluteGravity(Gravity.TOP | Gravity.END,
145                 res.getConfiguration().getLayoutDirection());
146         lp.width = res.getDimensionPixelSize(R.dimen.media_dialog_width);
147         lp.height = screenHeight - 2 * marginVerticalPx;
148         lp.horizontalMargin = ((float) marginEndPx) / screenWidth;
149         lp.verticalMargin = ((float) marginVerticalPx) / screenHeight;
150         window.setBackgroundDrawableResource(R.drawable.media_dialog_background);
151         window.setAttributes(lp);
152         window.setElevation(getWindow().getElevation() + 5);
153         window.setTitle(getString(
154                 com.android.systemui.R.string.media_output_dialog_accessibility_title));
155 
156         window.getDecorView().addOnLayoutChangeListener(
157                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
158                         -> findViewById(android.R.id.content).setUnrestrictedPreferKeepClearRects(
159                         Collections.singletonList(new Rect(left, top, right, bottom))));
160 
161         RecyclerView devicesRecyclerView = requireViewById(R.id.device_list);
162         devicesRecyclerView.setLayoutManager(new LayoutManagerWrapper(this));
163         devicesRecyclerView.setAdapter(mAdapter);
164 
165         int itemSpacingPx = getResources().getDimensionPixelSize(R.dimen.media_dialog_item_spacing);
166         devicesRecyclerView.addItemDecoration(new SpacingDecoration(itemSpacingPx));
167     }
168 
169     @Override
onStart()170     public void onStart() {
171         super.onStart();
172         mMediaOutputController.start(this);
173     }
174 
175     @Override
onStop()176     public void onStop() {
177         mMediaOutputController.stop();
178         super.onStop();
179     }
180 
refresh(boolean deviceSetChanged)181     private void refresh(boolean deviceSetChanged) {
182         if (DEBUG) Log.d(TAG, "refresh: deviceSetChanged " + deviceSetChanged);
183         // If the dialog is going away or is already refreshing, do nothing.
184         if (mMediaOutputController.isRefreshing()) {
185             return;
186         }
187         mMediaOutputController.setRefreshing(true);
188         mAdapter.updateItems();
189     }
190 
191     @Override
onMediaChanged()192     public void onMediaChanged() {
193         // NOOP
194     }
195 
196     @Override
onMediaStoppedOrPaused()197     public void onMediaStoppedOrPaused() {
198         // NOOP
199     }
200 
201     @Override
onRouteChanged()202     public void onRouteChanged() {
203         mMainThreadHandler.post(() -> refresh(/* deviceSetChanged= */ false));
204         MediaDevice activeDevice = mMediaOutputController.getCurrentConnectedMediaDevice();
205         if (mActiveDeviceId != null && !mActiveDeviceId.equals(activeDevice.getId())) {
206             mMediaOutputController.showVolumeDialog();
207         }
208         mActiveDeviceId = activeDevice.getId();
209     }
210 
211     @Override
onDeviceListChanged()212     public void onDeviceListChanged() {
213         mMainThreadHandler.post(() -> refresh(/* deviceSetChanged= */ true));
214         if (mActiveDeviceId == null
215                 && mMediaOutputController.getCurrentConnectedMediaDevice() != null) {
216             mActiveDeviceId = mMediaOutputController.getCurrentConnectedMediaDevice().getId();
217         }
218     }
219 
220     @Override
dismissDialog()221     public void dismissDialog() {
222         if (DEBUG) Log.d(TAG, "dismissDialog");
223         finish();
224     }
225 
226     private class LayoutManagerWrapper extends LinearLayoutManager {
LayoutManagerWrapper(Context context)227         LayoutManagerWrapper(Context context) {
228             super(context);
229         }
230 
231         @Override
onLayoutCompleted(RecyclerView.State state)232         public void onLayoutCompleted(RecyclerView.State state) {
233             super.onLayoutCompleted(state);
234             mMediaOutputController.setRefreshing(false);
235             mMediaOutputController.refreshDataSetIfNeeded();
236         }
237     }
238 
239     private static class SpacingDecoration extends RecyclerView.ItemDecoration {
240         private final int mMarginPx;
241 
SpacingDecoration(int marginPx)242         SpacingDecoration(int marginPx) {
243             mMarginPx = marginPx;
244         }
245 
246         @Override
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)247         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
248                 RecyclerView.State state) {
249             if (parent.getChildAdapterPosition(view) == 0) {
250                 outRect.top = mMarginPx;
251             }
252             outRect.bottom = mMarginPx;
253         }
254     }
255 }
256