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 android.app.Dialog; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.provider.Settings; 25 import android.service.quicksettings.Tile; 26 import android.util.Log; 27 import android.view.View; 28 import android.view.View.OnAttachStateChangeListener; 29 import android.view.ViewGroup; 30 import android.view.WindowManager.LayoutParams; 31 import android.widget.Button; 32 33 import com.android.internal.app.MediaRouteDialogPresenter; 34 import com.android.internal.logging.MetricsLogger; 35 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 36 import com.android.systemui.Dependency; 37 import com.android.systemui.R; 38 import com.android.systemui.plugins.ActivityStarter; 39 import com.android.systemui.plugins.qs.DetailAdapter; 40 import com.android.systemui.plugins.qs.QSTile.BooleanState; 41 import com.android.systemui.qs.QSDetailItems; 42 import com.android.systemui.qs.QSDetailItems.Item; 43 import com.android.systemui.qs.QSHost; 44 import com.android.systemui.qs.tileimpl.QSTileImpl; 45 import com.android.systemui.statusbar.phone.SystemUIDialog; 46 import com.android.systemui.statusbar.policy.CastController; 47 import com.android.systemui.statusbar.policy.CastController.CastDevice; 48 import com.android.systemui.statusbar.policy.KeyguardMonitor; 49 50 import java.util.LinkedHashMap; 51 import java.util.Set; 52 53 /** Quick settings tile: Cast **/ 54 public class CastTile extends QSTileImpl<BooleanState> { 55 private static final Intent CAST_SETTINGS = 56 new Intent(Settings.ACTION_CAST_SETTINGS); 57 58 private final CastController mController; 59 private final CastDetailAdapter mDetailAdapter; 60 private final KeyguardMonitor mKeyguard; 61 private final Callback mCallback = new Callback(); 62 private final ActivityStarter mActivityStarter; 63 private Dialog mDialog; 64 private boolean mRegistered; 65 66 public CastTile(QSHost host) { 67 super(host); 68 mController = Dependency.get(CastController.class); 69 mDetailAdapter = new CastDetailAdapter(); 70 mKeyguard = Dependency.get(KeyguardMonitor.class); 71 mActivityStarter = Dependency.get(ActivityStarter.class); 72 } 73 74 @Override 75 public DetailAdapter getDetailAdapter() { 76 return mDetailAdapter; 77 } 78 79 @Override 80 public BooleanState newTileState() { 81 return new BooleanState(); 82 } 83 84 @Override 85 public void handleSetListening(boolean listening) { 86 if (DEBUG) Log.d(TAG, "handleSetListening " + listening); 87 if (listening) { 88 mController.addCallback(mCallback); 89 mKeyguard.addCallback(mCallback); 90 } else { 91 mController.setDiscovering(false); 92 mController.removeCallback(mCallback); 93 mKeyguard.removeCallback(mCallback); 94 } 95 } 96 97 @Override 98 protected void handleUserSwitch(int newUserId) { 99 super.handleUserSwitch(newUserId); 100 mController.setCurrentUserId(newUserId); 101 } 102 103 @Override 104 public Intent getLongClickIntent() { 105 return new Intent(Settings.ACTION_CAST_SETTINGS); 106 } 107 108 @Override 109 protected void handleSecondaryClick() { 110 handleClick(); 111 } 112 113 @Override 114 protected void handleClick() { 115 if (mKeyguard.isSecure() && !mKeyguard.canSkipBouncer()) { 116 mActivityStarter.postQSRunnableDismissingKeyguard(() -> { 117 showDetail(true); 118 }); 119 return; 120 } 121 showDetail(true); 122 } 123 124 @Override 125 public void showDetail(boolean show) { 126 mUiHandler.post(() -> { 127 mDialog = MediaRouteDialogPresenter.createDialog(mContext, ROUTE_TYPE_REMOTE_DISPLAY, 128 v -> { 129 mDialog.dismiss(); 130 Dependency.get(ActivityStarter.class) 131 .postStartActivityDismissingKeyguard(getLongClickIntent(), 0); 132 }); 133 mDialog.getWindow().setType(LayoutParams.TYPE_KEYGUARD_DIALOG); 134 SystemUIDialog.setShowForAllUsers(mDialog, true); 135 SystemUIDialog.registerDismissListener(mDialog); 136 SystemUIDialog.setWindowOnTop(mDialog); 137 mUiHandler.post(() -> mDialog.show()); 138 mHost.collapsePanels(); 139 }); 140 } 141 142 @Override 143 public CharSequence getTileLabel() { 144 return mContext.getString(R.string.quick_settings_cast_title); 145 } 146 147 @Override 148 protected void handleUpdateState(BooleanState state, Object arg) { 149 state.label = mContext.getString(R.string.quick_settings_cast_title); 150 state.contentDescription = state.label; 151 state.value = false; 152 final Set<CastDevice> devices = mController.getCastDevices(); 153 boolean connecting = false; 154 for (CastDevice device : devices) { 155 if (device.state == CastDevice.STATE_CONNECTED) { 156 state.value = true; 157 state.label = getDeviceName(device); 158 state.contentDescription = state.contentDescription + "," + 159 mContext.getString(R.string.accessibility_cast_name, state.label); 160 } else if (device.state == CastDevice.STATE_CONNECTING) { 161 connecting = true; 162 } 163 } 164 if (!state.value && connecting) { 165 state.label = mContext.getString(R.string.quick_settings_connecting); 166 } 167 state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; 168 state.icon = ResourceIcon.get(state.value ? R.drawable.ic_qs_cast_on 169 : R.drawable.ic_qs_cast_off); 170 mDetailAdapter.updateItems(devices); 171 state.expandedAccessibilityClassName = Button.class.getName(); 172 state.contentDescription = state.contentDescription + "," 173 + mContext.getString(R.string.accessibility_quick_settings_open_details); 174 } 175 176 @Override 177 public int getMetricsCategory() { 178 return MetricsEvent.QS_CAST; 179 } 180 181 @Override 182 protected String composeChangeAnnouncement() { 183 if (!mState.value) { 184 // We only announce when it's turned off to avoid vocal overflow. 185 return mContext.getString(R.string.accessibility_casting_turned_off); 186 } 187 return null; 188 } 189 190 private String getDeviceName(CastDevice device) { 191 return device.name != null ? device.name 192 : mContext.getString(R.string.quick_settings_cast_device_default_name); 193 } 194 195 private final class Callback implements CastController.Callback, KeyguardMonitor.Callback { 196 @Override 197 public void onCastDevicesChanged() { 198 refreshState(); 199 } 200 201 @Override 202 public void onKeyguardShowingChanged() { 203 refreshState(); 204 } 205 }; 206 207 private final class CastDetailAdapter implements DetailAdapter, QSDetailItems.Callback { 208 private final LinkedHashMap<String, CastDevice> mVisibleOrder = new LinkedHashMap<>(); 209 210 private QSDetailItems mItems; 211 212 @Override 213 public CharSequence getTitle() { 214 return mContext.getString(R.string.quick_settings_cast_title); 215 } 216 217 @Override 218 public Boolean getToggleState() { 219 return null; 220 } 221 222 @Override 223 public Intent getSettingsIntent() { 224 return CAST_SETTINGS; 225 } 226 227 @Override 228 public void setToggleState(boolean state) { 229 // noop 230 } 231 232 @Override 233 public int getMetricsCategory() { 234 return MetricsEvent.QS_CAST_DETAILS; 235 } 236 237 @Override 238 public View createDetailView(Context context, View convertView, ViewGroup parent) { 239 mItems = QSDetailItems.convertOrInflate(context, convertView, parent); 240 mItems.setTagSuffix("Cast"); 241 if (convertView == null) { 242 if (DEBUG) Log.d(TAG, "addOnAttachStateChangeListener"); 243 mItems.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { 244 @Override 245 public void onViewAttachedToWindow(View v) { 246 if (DEBUG) Log.d(TAG, "onViewAttachedToWindow"); 247 } 248 249 @Override 250 public void onViewDetachedFromWindow(View v) { 251 if (DEBUG) Log.d(TAG, "onViewDetachedFromWindow"); 252 mVisibleOrder.clear(); 253 } 254 }); 255 } 256 mItems.setEmptyState(R.drawable.ic_qs_cast_detail_empty, 257 R.string.quick_settings_cast_detail_empty_text); 258 mItems.setCallback(this); 259 updateItems(mController.getCastDevices()); 260 mController.setDiscovering(true); 261 return mItems; 262 } 263 264 private void updateItems(Set<CastDevice> devices) { 265 if (mItems == null) return; 266 Item[] items = null; 267 if (devices != null && !devices.isEmpty()) { 268 // if we are connected, simply show that device 269 for (CastDevice device : devices) { 270 if (device.state == CastDevice.STATE_CONNECTED) { 271 final Item item = new Item(); 272 item.iconResId = R.drawable.ic_qs_cast_on; 273 item.line1 = getDeviceName(device); 274 item.line2 = mContext.getString(R.string.quick_settings_connected); 275 item.tag = device; 276 item.canDisconnect = true; 277 items = new Item[] { item }; 278 break; 279 } 280 } 281 // otherwise list all available devices, and don't move them around 282 if (items == null) { 283 for (CastDevice device : devices) { 284 mVisibleOrder.put(device.id, device); 285 } 286 items = new Item[devices.size()]; 287 int i = 0; 288 for (String id : mVisibleOrder.keySet()) { 289 final CastDevice device = mVisibleOrder.get(id); 290 if (!devices.contains(device)) continue; 291 final Item item = new Item(); 292 item.iconResId = R.drawable.ic_qs_cast_off; 293 item.line1 = getDeviceName(device); 294 if (device.state == CastDevice.STATE_CONNECTING) { 295 item.line2 = mContext.getString(R.string.quick_settings_connecting); 296 } 297 item.tag = device; 298 items[i++] = item; 299 } 300 } 301 } 302 mItems.setItems(items); 303 } 304 305 @Override 306 public void onDetailItemClick(Item item) { 307 if (item == null || item.tag == null) return; 308 MetricsLogger.action(mContext, MetricsEvent.QS_CAST_SELECT); 309 final CastDevice device = (CastDevice) item.tag; 310 mController.startCasting(device); 311 } 312 313 @Override 314 public void onDetailItemDisconnect(Item item) { 315 if (item == null || item.tag == null) return; 316 MetricsLogger.action(mContext, MetricsEvent.QS_CAST_DISCONNECT); 317 final CastDevice device = (CastDevice) item.tag; 318 mController.stopCasting(device); 319 } 320 } 321 } 322