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