/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.qs.tiles; import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY; import android.annotation.NonNull; import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.media.MediaRouter.RouteInfo; import android.os.Handler; import android.os.Looper; import android.os.SystemProperties; import android.provider.Settings; import android.service.quicksettings.Tile; import android.util.Log; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import android.widget.Button; import androidx.annotation.Nullable; import com.android.internal.app.MediaRouteDialogPresenter; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.R; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSTile.BooleanState; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSDetailItems; import com.android.systemui.qs.QSDetailItems.Item; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.connectivity.NetworkController; import com.android.systemui.statusbar.connectivity.SignalCallback; import com.android.systemui.statusbar.connectivity.WifiIndicators; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.CastController.CastDevice; import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.KeyguardStateController; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import javax.inject.Inject; /** Quick settings tile: Cast **/ public class CastTile extends QSTileImpl { private static final Intent CAST_SETTINGS = new Intent(Settings.ACTION_CAST_SETTINGS); private final CastController mController; private final CastDetailAdapter mDetailAdapter; private final KeyguardStateController mKeyguard; private final NetworkController mNetworkController; private final DialogLaunchAnimator mDialogLaunchAnimator; private final Callback mCallback = new Callback(); private Dialog mDialog; private boolean mWifiConnected; private boolean mHotspotConnected; private static final String WFD_ENABLE = "persist.debug.wfd.enable"; @Inject public CastTile( QSHost host, @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 ) { super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); mController = castController; mDetailAdapter = new CastDetailAdapter(); mKeyguard = keyguardStateController; mNetworkController = networkController; mDialogLaunchAnimator = dialogLaunchAnimator; mController.observe(this, mCallback); mKeyguard.observe(this, mCallback); mNetworkController.observe(this, mSignalCallback); hotspotController.observe(this, mHotspotCallback); } @Override public DetailAdapter getDetailAdapter() { return mDetailAdapter; } @Override public BooleanState newTileState() { BooleanState state = new BooleanState(); state.handlesLongClick = false; return state; } @Override public void handleSetListening(boolean listening) { super.handleSetListening(listening); if (DEBUG) Log.d(TAG, "handleSetListening " + listening); if (!listening) { mController.setDiscovering(false); } } @Override protected void handleUserSwitch(int newUserId) { super.handleUserSwitch(newUserId); mController.setCurrentUserId(newUserId); } @Override public Intent getLongClickIntent() { return new Intent(Settings.ACTION_CAST_SETTINGS); } @Override protected void handleLongClick(@Nullable View view) { handleClick(view); } @Override protected void handleClick(@Nullable View view) { if (getState().state == Tile.STATE_UNAVAILABLE) { return; } List activeDevices = getActiveDevices(); if (willPopDetail()) { if (!mKeyguard.isShowing()) { showDetail(view); } else { mActivityStarter.postQSRunnableDismissingKeyguard(() -> { // Dismissing the keyguard will collapse the shade, so we don't animate from the // view here as it would not look good. showDetail(null /* view */); }); } } else { mController.stopCasting(activeDevices.get(0)); } } // We want to pop up the media route selection dialog if we either have no active devices // (neither routes nor projection), or if we have an active route. In other cases, we assume // that a projection is active. This is messy, but this tile never correctly handled the // case where multiple devices were active :-/. private boolean willPopDetail() { List activeDevices = getActiveDevices(); return activeDevices.isEmpty() || (activeDevices.get(0).tag instanceof RouteInfo); } private List getActiveDevices() { ArrayList activeDevices = new ArrayList<>(); for (CastDevice device : mController.getCastDevices()) { if (device.state == CastDevice.STATE_CONNECTED || device.state == CastDevice.STATE_CONNECTING) { activeDevices.add(device); } } return activeDevices; } @Override public void showDetail(boolean show) { showDetail(null /* view */); } private void showDetail(@Nullable View view) { mUiHandler.post(() -> { mDialog = MediaRouteDialogPresenter.createDialog(mContext, ROUTE_TYPE_REMOTE_DISPLAY, v -> { mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations(); mDialog.dismiss(); mActivityStarter .postStartActivityDismissingKeyguard(getLongClickIntent(), 0); }); SystemUIDialog.setShowForAllUsers(mDialog, true); SystemUIDialog.registerDismissListener(mDialog); SystemUIDialog.setWindowOnTop(mDialog); mUiHandler.post(() -> { if (view != null) { mDialogLaunchAnimator.showFromView(mDialog, view); } else { mDialog.show(); } }); }); } @Override public CharSequence getTileLabel() { return mContext.getString(R.string.quick_settings_cast_title); } @Override protected void handleUpdateState(BooleanState state, Object arg) { state.label = mContext.getString(R.string.quick_settings_cast_title); state.contentDescription = state.label; state.stateDescription = ""; state.value = false; final List devices = mController.getCastDevices(); boolean connecting = false; // We always choose the first device that's in the CONNECTED state in the case where // multiple devices are CONNECTED at the same time. for (CastDevice device : devices) { if (device.state == CastDevice.STATE_CONNECTED) { state.value = true; state.secondaryLabel = getDeviceName(device); state.stateDescription = state.stateDescription + "," + mContext.getString( R.string.accessibility_cast_name, state.label); connecting = false; break; } else if (device.state == CastDevice.STATE_CONNECTING) { connecting = true; } } if (connecting && !state.value) { state.secondaryLabel = mContext.getString(R.string.quick_settings_connecting); } state.icon = ResourceIcon.get(state.value ? R.drawable.ic_cast_connected : R.drawable.ic_cast); if (canCastToWifi() || state.value) { state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; if (!state.value) { state.secondaryLabel = ""; } state.contentDescription = state.contentDescription + "," + mContext.getString(R.string.accessibility_quick_settings_open_details); state.expandedAccessibilityClassName = Button.class.getName(); state.forceExpandIcon = willPopDetail(); } else { state.state = Tile.STATE_UNAVAILABLE; String noWifi = mContext.getString(R.string.quick_settings_cast_no_wifi); state.secondaryLabel = noWifi; state.forceExpandIcon = false; } state.stateDescription = state.stateDescription + ", " + state.secondaryLabel; mDetailAdapter.updateItems(devices); } @Override public int getMetricsCategory() { return MetricsEvent.QS_CAST; } @Override protected String composeChangeAnnouncement() { if (!mState.value) { // We only announce when it's turned off to avoid vocal overflow. return mContext.getString(R.string.accessibility_casting_turned_off); } return null; } private String getDeviceName(CastDevice device) { return device.name != null ? device.name : mContext.getString(R.string.quick_settings_cast_device_default_name); } private boolean canCastToWifi() { return mWifiConnected || mHotspotConnected; } private final SignalCallback mSignalCallback = new SignalCallback() { @Override public void setWifiIndicators(@NonNull WifiIndicators indicators) { // statusIcon.visible has the connected status information if(SystemProperties.getBoolean(WFD_ENABLE, false)) { if(indicators.enabled != mWifiConnected) { mWifiConnected = indicators.enabled; refreshState(); } } else { boolean enabledAndConnected = indicators.enabled && indicators.qsIcon.visible; if (enabledAndConnected != mWifiConnected) { mWifiConnected = enabledAndConnected; // Hotspot is not connected, so changes here should update if (!mHotspotConnected) { refreshState(); } } } } }; private final HotspotController.Callback mHotspotCallback = new HotspotController.Callback() { @Override public void onHotspotChanged(boolean enabled, int numDevices) { boolean enabledAndConnected = enabled && numDevices > 0; if (enabledAndConnected != mHotspotConnected) { mHotspotConnected = enabledAndConnected; // Wifi is not connected, so changes here should update if (!mWifiConnected) { refreshState(); } } } }; private final class Callback implements CastController.Callback, KeyguardStateController.Callback { @Override public void onCastDevicesChanged() { refreshState(); } @Override public void onKeyguardShowingChanged() { refreshState(); } }; private final class CastDetailAdapter implements DetailAdapter, QSDetailItems.Callback { private final LinkedHashMap mVisibleOrder = new LinkedHashMap<>(); private QSDetailItems mItems; @Override public CharSequence getTitle() { return mContext.getString(R.string.quick_settings_cast_title); } @Override public Boolean getToggleState() { return null; } @Override public Intent getSettingsIntent() { return CAST_SETTINGS; } @Override public void setToggleState(boolean state) { // noop } @Override public int getMetricsCategory() { return MetricsEvent.QS_CAST_DETAILS; } @Override public View createDetailView(Context context, View convertView, ViewGroup parent) { mItems = QSDetailItems.convertOrInflate(context, convertView, parent); mItems.setTagSuffix("Cast"); if (convertView == null) { if (DEBUG) Log.d(TAG, "addOnAttachStateChangeListener"); mItems.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { if (DEBUG) Log.d(TAG, "onViewAttachedToWindow"); } @Override public void onViewDetachedFromWindow(View v) { if (DEBUG) Log.d(TAG, "onViewDetachedFromWindow"); mVisibleOrder.clear(); } }); } mItems.setEmptyState(R.drawable.ic_qs_cast_detail_empty, R.string.quick_settings_cast_detail_empty_text); mItems.setCallback(this); updateItems(mController.getCastDevices()); mController.setDiscovering(true); return mItems; } private void updateItems(List devices) { if (mItems == null) return; Item[] items = null; if (devices != null && !devices.isEmpty()) { // if we are connected, simply show that device for (CastDevice device : devices) { if (device.state == CastDevice.STATE_CONNECTED) { final Item item = new Item(); item.iconResId = R.drawable.ic_cast_connected; item.line1 = getDeviceName(device); item.line2 = mContext.getString(R.string.quick_settings_connected); item.tag = device; item.canDisconnect = true; items = new Item[] { item }; break; } } // otherwise list all available devices, and don't move them around if (items == null) { for (CastDevice device : devices) { mVisibleOrder.put(device.id, device); } items = new Item[devices.size()]; int i = 0; for (String id : mVisibleOrder.keySet()) { final CastDevice device = mVisibleOrder.get(id); if (!devices.contains(device)) continue; final Item item = new Item(); item.iconResId = R.drawable.ic_cast; item.line1 = getDeviceName(device); if (device.state == CastDevice.STATE_CONNECTING) { item.line2 = mContext.getString(R.string.quick_settings_connecting); } item.tag = device; items[i++] = item; } } } mItems.setItems(items); } @Override public void onDetailItemClick(Item item) { if (item == null || item.tag == null) return; MetricsLogger.action(mContext, MetricsEvent.QS_CAST_SELECT); final CastDevice device = (CastDevice) item.tag; mController.startCasting(device); } @Override public void onDetailItemDisconnect(Item item) { if (item == null || item.tag == null) return; MetricsLogger.action(mContext, MetricsEvent.QS_CAST_DISCONNECT); final CastDevice device = (CastDevice) item.tag; mController.stopCasting(device); } } }