1 /* 2 * Copyright (C) 2014 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.qs.tiles; 18 19 import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY; 20 21 import static com.android.systemui.flags.Flags.SIGNAL_CALLBACK_DEPRECATION; 22 23 import android.annotation.NonNull; 24 import android.app.Dialog; 25 import android.content.Intent; 26 import android.media.MediaRouter.RouteInfo; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.provider.Settings; 30 import android.service.quicksettings.Tile; 31 import android.util.Log; 32 import android.view.View; 33 import android.widget.Button; 34 35 import androidx.annotation.Nullable; 36 37 import com.android.internal.app.MediaRouteDialogPresenter; 38 import com.android.internal.jank.InteractionJankMonitor; 39 import com.android.internal.logging.MetricsLogger; 40 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 41 import com.android.systemui.R; 42 import com.android.systemui.animation.ActivityLaunchAnimator; 43 import com.android.systemui.animation.DialogCuj; 44 import com.android.systemui.animation.DialogLaunchAnimator; 45 import com.android.systemui.dagger.qualifiers.Background; 46 import com.android.systemui.dagger.qualifiers.Main; 47 import com.android.systemui.flags.FeatureFlags; 48 import com.android.systemui.plugins.ActivityStarter; 49 import com.android.systemui.plugins.FalsingManager; 50 import com.android.systemui.plugins.qs.QSTile.BooleanState; 51 import com.android.systemui.plugins.statusbar.StatusBarStateController; 52 import com.android.systemui.qs.QSHost; 53 import com.android.systemui.qs.QsEventLogger; 54 import com.android.systemui.qs.logging.QSLogger; 55 import com.android.systemui.qs.tileimpl.QSTileImpl; 56 import com.android.systemui.statusbar.connectivity.NetworkController; 57 import com.android.systemui.statusbar.connectivity.SignalCallback; 58 import com.android.systemui.statusbar.connectivity.WifiIndicators; 59 import com.android.systemui.statusbar.phone.SystemUIDialog; 60 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor; 61 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel; 62 import com.android.systemui.statusbar.policy.CastController; 63 import com.android.systemui.statusbar.policy.CastController.CastDevice; 64 import com.android.systemui.statusbar.policy.HotspotController; 65 import com.android.systemui.statusbar.policy.KeyguardStateController; 66 67 import java.util.ArrayList; 68 import java.util.List; 69 import java.util.function.Consumer; 70 71 import javax.inject.Inject; 72 73 /** Quick settings tile: Cast **/ 74 public class CastTile extends QSTileImpl<BooleanState> { 75 76 public static final String TILE_SPEC = "cast"; 77 78 private static final String INTERACTION_JANK_TAG = TILE_SPEC; 79 80 private static final Intent CAST_SETTINGS = 81 new Intent(Settings.ACTION_CAST_SETTINGS); 82 83 private final CastController mController; 84 private final KeyguardStateController mKeyguard; 85 private final NetworkController mNetworkController; 86 private final DialogLaunchAnimator mDialogLaunchAnimator; 87 private final Callback mCallback = new Callback(); 88 private final WifiInteractor mWifiInteractor; 89 private final TileJavaAdapter mJavaAdapter; 90 private final FeatureFlags mFeatureFlags; 91 private boolean mWifiConnected; 92 private boolean mHotspotConnected; 93 94 @Inject CastTile( QSHost host, QsEventLogger uiEventLogger, @Background Looper backgroundLooper, @Main Handler mainHandler, FalsingManager falsingManager, MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, CastController castController, KeyguardStateController keyguardStateController, NetworkController networkController, HotspotController hotspotController, DialogLaunchAnimator dialogLaunchAnimator, WifiInteractor wifiInteractor, TileJavaAdapter javaAdapter, FeatureFlags featureFlags )95 public CastTile( 96 QSHost host, 97 QsEventLogger uiEventLogger, 98 @Background Looper backgroundLooper, 99 @Main Handler mainHandler, 100 FalsingManager falsingManager, 101 MetricsLogger metricsLogger, 102 StatusBarStateController statusBarStateController, 103 ActivityStarter activityStarter, 104 QSLogger qsLogger, 105 CastController castController, 106 KeyguardStateController keyguardStateController, 107 NetworkController networkController, 108 HotspotController hotspotController, 109 DialogLaunchAnimator dialogLaunchAnimator, 110 WifiInteractor wifiInteractor, 111 TileJavaAdapter javaAdapter, 112 FeatureFlags featureFlags 113 ) { 114 super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, 115 statusBarStateController, activityStarter, qsLogger); 116 mController = castController; 117 mKeyguard = keyguardStateController; 118 mNetworkController = networkController; 119 mDialogLaunchAnimator = dialogLaunchAnimator; 120 mWifiInteractor = wifiInteractor; 121 mJavaAdapter = javaAdapter; 122 mFeatureFlags = featureFlags; 123 mController.observe(this, mCallback); 124 mKeyguard.observe(this, mCallback); 125 if (!mFeatureFlags.isEnabled(SIGNAL_CALLBACK_DEPRECATION)) { 126 mNetworkController.observe(this, mSignalCallback); 127 } else { 128 mJavaAdapter.bind(this, mWifiInteractor.getWifiNetwork(), mNetworkModelConsumer); 129 } 130 hotspotController.observe(this, mHotspotCallback); 131 } 132 133 @Override newTileState()134 public BooleanState newTileState() { 135 BooleanState state = new BooleanState(); 136 state.handlesLongClick = false; 137 return state; 138 } 139 140 @Override handleSetListening(boolean listening)141 public void handleSetListening(boolean listening) { 142 super.handleSetListening(listening); 143 if (DEBUG) Log.d(TAG, "handleSetListening " + listening); 144 if (!listening) { 145 mController.setDiscovering(false); 146 } 147 } 148 149 @Override handleUserSwitch(int newUserId)150 protected void handleUserSwitch(int newUserId) { 151 super.handleUserSwitch(newUserId); 152 mController.setCurrentUserId(newUserId); 153 } 154 155 @Override getLongClickIntent()156 public Intent getLongClickIntent() { 157 return new Intent(Settings.ACTION_CAST_SETTINGS); 158 } 159 160 @Override handleLongClick(@ullable View view)161 protected void handleLongClick(@Nullable View view) { 162 handleClick(view); 163 } 164 165 @Override handleClick(@ullable View view)166 protected void handleClick(@Nullable View view) { 167 if (getState().state == Tile.STATE_UNAVAILABLE) { 168 return; 169 } 170 171 List<CastDevice> activeDevices = getActiveDevices(); 172 if (willPopDialog()) { 173 if (!mKeyguard.isShowing()) { 174 showDialog(view); 175 } else { 176 mActivityStarter.postQSRunnableDismissingKeyguard(() -> { 177 // Dismissing the keyguard will collapse the shade, so we don't animate from the 178 // view here as it would not look good. 179 showDialog(null /* view */); 180 }); 181 } 182 } else { 183 mController.stopCasting(activeDevices.get(0)); 184 } 185 } 186 187 // We want to pop up the media route selection dialog if we either have no active devices 188 // (neither routes nor projection), or if we have an active route. In other cases, we assume 189 // that a projection is active. This is messy, but this tile never correctly handled the 190 // case where multiple devices were active :-/. willPopDialog()191 private boolean willPopDialog() { 192 List<CastDevice> activeDevices = getActiveDevices(); 193 return activeDevices.isEmpty() || (activeDevices.get(0).tag instanceof RouteInfo); 194 } 195 getActiveDevices()196 private List<CastDevice> getActiveDevices() { 197 ArrayList<CastDevice> activeDevices = new ArrayList<>(); 198 for (CastDevice device : mController.getCastDevices()) { 199 if (device.state == CastDevice.STATE_CONNECTED 200 || device.state == CastDevice.STATE_CONNECTING) { 201 activeDevices.add(device); 202 } 203 } 204 205 return activeDevices; 206 } 207 208 private static class DialogHolder { 209 private Dialog mDialog; 210 init(Dialog dialog)211 private void init(Dialog dialog) { 212 mDialog = dialog; 213 } 214 } 215 showDialog(@ullable View view)216 private void showDialog(@Nullable View view) { 217 mUiHandler.post(() -> { 218 final DialogHolder holder = new DialogHolder(); 219 final Dialog dialog = MediaRouteDialogPresenter.createDialog( 220 mContext, 221 ROUTE_TYPE_REMOTE_DISPLAY, 222 v -> { 223 ActivityLaunchAnimator.Controller controller = 224 mDialogLaunchAnimator.createActivityLaunchController(v); 225 226 if (controller == null) { 227 holder.mDialog.dismiss(); 228 } 229 230 mActivityStarter 231 .postStartActivityDismissingKeyguard(getLongClickIntent(), 0, 232 controller); 233 }, R.style.Theme_SystemUI_Dialog_Cast, false /* showProgressBarWhenEmpty */); 234 holder.init(dialog); 235 SystemUIDialog.setShowForAllUsers(dialog, true); 236 SystemUIDialog.registerDismissListener(dialog); 237 SystemUIDialog.setWindowOnTop(dialog, mKeyguard.isShowing()); 238 SystemUIDialog.setDialogSize(dialog); 239 240 mUiHandler.post(() -> { 241 if (view != null) { 242 mDialogLaunchAnimator.showFromView(dialog, view, 243 new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, 244 INTERACTION_JANK_TAG)); 245 } else { 246 dialog.show(); 247 } 248 }); 249 }); 250 } 251 252 @Override getTileLabel()253 public CharSequence getTileLabel() { 254 return mContext.getString(R.string.quick_settings_cast_title); 255 } 256 257 @Override handleUpdateState(BooleanState state, Object arg)258 protected void handleUpdateState(BooleanState state, Object arg) { 259 state.label = mContext.getString(R.string.quick_settings_cast_title); 260 state.contentDescription = state.label; 261 state.stateDescription = ""; 262 state.value = false; 263 final List<CastDevice> devices = mController.getCastDevices(); 264 boolean connecting = false; 265 // We always choose the first device that's in the CONNECTED state in the case where 266 // multiple devices are CONNECTED at the same time. 267 for (CastDevice device : devices) { 268 if (device.state == CastDevice.STATE_CONNECTED) { 269 state.value = true; 270 state.secondaryLabel = getDeviceName(device); 271 state.stateDescription = state.stateDescription + "," 272 + mContext.getString( 273 R.string.accessibility_cast_name, state.label); 274 connecting = false; 275 break; 276 } else if (device.state == CastDevice.STATE_CONNECTING) { 277 connecting = true; 278 } 279 } 280 if (connecting && !state.value) { 281 state.secondaryLabel = mContext.getString(R.string.quick_settings_connecting); 282 } 283 state.icon = ResourceIcon.get(state.value ? R.drawable.ic_cast_connected 284 : R.drawable.ic_cast); 285 if (canCastToWifi() || state.value) { 286 state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; 287 if (!state.value) { 288 state.secondaryLabel = ""; 289 } 290 state.expandedAccessibilityClassName = Button.class.getName(); 291 state.forceExpandIcon = willPopDialog(); 292 } else { 293 state.state = Tile.STATE_UNAVAILABLE; 294 String noWifi = mContext.getString(R.string.quick_settings_cast_no_wifi); 295 state.secondaryLabel = noWifi; 296 state.forceExpandIcon = false; 297 } 298 state.stateDescription = state.stateDescription + ", " + state.secondaryLabel; 299 } 300 301 @Override getMetricsCategory()302 public int getMetricsCategory() { 303 return MetricsEvent.QS_CAST; 304 } 305 getDeviceName(CastDevice device)306 private String getDeviceName(CastDevice device) { 307 return device.name != null ? device.name 308 : mContext.getString(R.string.quick_settings_cast_device_default_name); 309 } 310 canCastToWifi()311 private boolean canCastToWifi() { 312 return mWifiConnected || mHotspotConnected; 313 } 314 setWifiConnected(boolean connected)315 private void setWifiConnected(boolean connected) { 316 if (connected != mWifiConnected) { 317 mWifiConnected = connected; 318 // Hotspot is not connected, so changes here should update 319 if (!mHotspotConnected) { 320 refreshState(); 321 } 322 } 323 } 324 setHotspotConnected(boolean connected)325 private void setHotspotConnected(boolean connected) { 326 if (connected != mHotspotConnected) { 327 mHotspotConnected = connected; 328 // Wifi is not connected, so changes here should update 329 if (!mWifiConnected) { 330 refreshState(); 331 } 332 } 333 } 334 335 private final Consumer<WifiNetworkModel> mNetworkModelConsumer = (model) -> { 336 setWifiConnected(model instanceof WifiNetworkModel.Active); 337 }; 338 339 private final SignalCallback mSignalCallback = new SignalCallback() { 340 @Override 341 public void setWifiIndicators(@NonNull WifiIndicators indicators) { 342 // statusIcon.visible has the connected status information 343 boolean enabledAndConnected = indicators.enabled 344 && (indicators.qsIcon != null && indicators.qsIcon.visible); 345 setWifiConnected(enabledAndConnected); 346 } 347 }; 348 349 private final HotspotController.Callback mHotspotCallback = 350 new HotspotController.Callback() { 351 @Override 352 public void onHotspotChanged(boolean enabled, int numDevices) { 353 boolean enabledAndConnected = enabled && numDevices > 0; 354 setHotspotConnected(enabledAndConnected); 355 } 356 }; 357 358 private final class Callback implements CastController.Callback, 359 KeyguardStateController.Callback { 360 @Override onCastDevicesChanged()361 public void onCastDevicesChanged() { 362 refreshState(); 363 } 364 365 @Override onKeyguardShowingChanged()366 public void onKeyguardShowingChanged() { 367 refreshState(); 368 } 369 } 370 } 371