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
17package com.android.systemui.qs.tiles;
18
19import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
20
21import android.app.Dialog;
22import android.content.Context;
23import android.content.Intent;
24import android.provider.Settings;
25import android.service.quicksettings.Tile;
26import android.util.Log;
27import android.view.View;
28import android.view.View.OnAttachStateChangeListener;
29import android.view.ViewGroup;
30import android.view.WindowManager.LayoutParams;
31import android.widget.Button;
32
33import com.android.internal.app.MediaRouteDialogPresenter;
34import com.android.internal.logging.MetricsLogger;
35import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
36import com.android.systemui.Dependency;
37import com.android.systemui.R;
38import com.android.systemui.plugins.ActivityStarter;
39import com.android.systemui.plugins.qs.DetailAdapter;
40import com.android.systemui.plugins.qs.QSTile.BooleanState;
41import com.android.systemui.qs.QSDetailItems;
42import com.android.systemui.qs.QSDetailItems.Item;
43import com.android.systemui.qs.QSHost;
44import com.android.systemui.qs.tileimpl.QSTileImpl;
45import com.android.systemui.statusbar.phone.SystemUIDialog;
46import com.android.systemui.statusbar.policy.CastController;
47import com.android.systemui.statusbar.policy.CastController.CastDevice;
48import com.android.systemui.statusbar.policy.KeyguardMonitor;
49
50import java.util.LinkedHashMap;
51import java.util.Set;
52
53/** Quick settings tile: Cast **/
54public 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