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