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 android.content.Context;
20import android.content.Intent;
21import android.content.pm.PackageManager;
22import android.content.res.Resources;
23import android.provider.Settings;
24import android.service.quicksettings.Tile;
25import android.util.Log;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.Switch;
29
30import com.android.internal.logging.MetricsLogger;
31import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
32import com.android.settingslib.wifi.AccessPoint;
33import com.android.systemui.Dependency;
34import com.android.systemui.R;
35import com.android.systemui.plugins.ActivityStarter;
36import com.android.systemui.plugins.qs.DetailAdapter;
37import com.android.systemui.plugins.qs.QSIconView;
38import com.android.systemui.plugins.qs.QSTile;
39import com.android.systemui.plugins.qs.QSTile.SignalState;
40import com.android.systemui.qs.AlphaControlledSignalTileView;
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.QSIconViewImpl;
45import com.android.systemui.qs.tileimpl.QSTileImpl;
46import com.android.systemui.statusbar.policy.NetworkController;
47import com.android.systemui.statusbar.policy.NetworkController.AccessPointController;
48import com.android.systemui.statusbar.policy.NetworkController.IconState;
49import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
50
51import java.util.List;
52
53/** Quick settings tile: Wifi **/
54public class WifiTile extends QSTileImpl<SignalState> {
55    private static final Intent WIFI_SETTINGS = new Intent(Settings.ACTION_WIFI_SETTINGS);
56
57    protected final NetworkController mController;
58    private final AccessPointController mWifiController;
59    private final WifiDetailAdapter mDetailAdapter;
60    private final QSTile.SignalState mStateBeforeClick = newTileState();
61
62    protected final WifiSignalCallback mSignalCallback = new WifiSignalCallback();
63    private final ActivityStarter mActivityStarter;
64    private boolean mExpectDisabled;
65
66    public WifiTile(QSHost host) {
67        super(host);
68        mController = Dependency.get(NetworkController.class);
69        mWifiController = mController.getAccessPointController();
70        mDetailAdapter = (WifiDetailAdapter) createDetailAdapter();
71        mActivityStarter = Dependency.get(ActivityStarter.class);
72    }
73
74    @Override
75    public SignalState newTileState() {
76        return new SignalState();
77    }
78
79    @Override
80    public void handleSetListening(boolean listening) {
81        if (listening) {
82            mController.addCallback(mSignalCallback);
83        } else {
84            mController.removeCallback(mSignalCallback);
85        }
86    }
87
88    @Override
89    public void setDetailListening(boolean listening) {
90        if (listening) {
91            mWifiController.addAccessPointCallback(mDetailAdapter);
92        } else {
93            mWifiController.removeAccessPointCallback(mDetailAdapter);
94        }
95    }
96
97    @Override
98    public DetailAdapter getDetailAdapter() {
99        return mDetailAdapter;
100    }
101
102    @Override
103    protected DetailAdapter createDetailAdapter() {
104        return new WifiDetailAdapter();
105    }
106
107    @Override
108    public QSIconView createTileView(Context context) {
109        return new AlphaControlledSignalTileView(context);
110    }
111
112    @Override
113    public Intent getLongClickIntent() {
114        return WIFI_SETTINGS;
115    }
116
117    @Override
118    protected void handleClick() {
119        // Secondary clicks are header clicks, just toggle.
120        mState.copyTo(mStateBeforeClick);
121        boolean wifiEnabled = mState.value;
122        // Immediately enter transient state when turning on wifi.
123        refreshState(wifiEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
124        mController.setWifiEnabled(!wifiEnabled);
125        mExpectDisabled = wifiEnabled;
126        if (mExpectDisabled) {
127            mHandler.postDelayed(() -> {
128                if (mExpectDisabled) {
129                    mExpectDisabled = false;
130                    refreshState();
131                }
132            }, QSIconViewImpl.QS_ANIM_LENGTH);
133        }
134    }
135
136    @Override
137    protected void handleSecondaryClick() {
138        if (!mWifiController.canConfigWifi()) {
139            mActivityStarter.postStartActivityDismissingKeyguard(
140                    new Intent(Settings.ACTION_WIFI_SETTINGS), 0);
141            return;
142        }
143        showDetail(true);
144        if (!mState.value) {
145            mController.setWifiEnabled(true);
146        }
147    }
148
149    @Override
150    public CharSequence getTileLabel() {
151        return mContext.getString(R.string.quick_settings_wifi_label);
152    }
153
154    @Override
155    protected void handleUpdateState(SignalState state, Object arg) {
156        if (DEBUG) Log.d(TAG, "handleUpdateState arg=" + arg);
157        final CallbackInfo cb = mSignalCallback.mInfo;
158        if (mExpectDisabled) {
159            if (cb.enabled) {
160                return; // Ignore updates until disabled event occurs.
161            } else {
162                mExpectDisabled = false;
163            }
164        }
165        boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING;
166        boolean wifiConnected = cb.enabled && (cb.wifiSignalIconId > 0) && (cb.ssid != null);
167        boolean wifiNotConnected = (cb.wifiSignalIconId > 0) && (cb.ssid == null);
168        boolean enabledChanging = state.value != cb.enabled;
169        if (enabledChanging) {
170            mDetailAdapter.setItemsVisible(cb.enabled);
171            fireToggleStateChanged(cb.enabled);
172        }
173        if (state.slash == null) {
174            state.slash = new SlashState();
175            state.slash.rotation = 6;
176        }
177        state.slash.isSlashed = false;
178        boolean isTransient = transientEnabling || cb.isTransient;
179        state.secondaryLabel = getSecondaryLabel(isTransient, cb.statusLabel);
180        state.state = Tile.STATE_ACTIVE;
181        state.dualTarget = true;
182        state.value = transientEnabling || cb.enabled;
183        state.activityIn = cb.enabled && cb.activityIn;
184        state.activityOut = cb.enabled && cb.activityOut;
185        final StringBuffer minimalContentDescription = new StringBuffer();
186        final Resources r = mContext.getResources();
187        if (isTransient) {
188            state.icon = ResourceIcon.get(R.drawable.ic_signal_wifi_transient_animation);
189            state.label = r.getString(R.string.quick_settings_wifi_label);
190        } else if (!state.value) {
191            state.slash.isSlashed = true;
192            state.state = Tile.STATE_INACTIVE;
193            state.icon = ResourceIcon.get(R.drawable.ic_qs_wifi_disabled);
194            state.label = r.getString(R.string.quick_settings_wifi_label);
195        } else if (wifiConnected) {
196            state.icon = ResourceIcon.get(cb.wifiSignalIconId);
197            state.label = removeDoubleQuotes(cb.ssid);
198        } else if (wifiNotConnected) {
199            state.icon = ResourceIcon.get(R.drawable.ic_qs_wifi_disconnected);
200            state.label = r.getString(R.string.quick_settings_wifi_label);
201        } else {
202            state.icon = ResourceIcon.get(R.drawable.ic_qs_wifi_no_network);
203            state.label = r.getString(R.string.quick_settings_wifi_label);
204        }
205        minimalContentDescription.append(
206                mContext.getString(R.string.quick_settings_wifi_label)).append(",");
207        if (state.value) {
208            if (wifiConnected) {
209                minimalContentDescription.append(cb.wifiSignalContentDescription).append(",");
210                minimalContentDescription.append(removeDoubleQuotes(cb.ssid));
211            }
212        }
213        state.contentDescription = minimalContentDescription.toString();
214        state.dualLabelContentDescription = r.getString(
215                R.string.accessibility_quick_settings_open_settings, getTileLabel());
216        state.expandedAccessibilityClassName = Switch.class.getName();
217    }
218
219    private CharSequence getSecondaryLabel(boolean isTransient, String statusLabel) {
220        return isTransient
221                ? mContext.getString(R.string.quick_settings_wifi_secondary_label_transient)
222                : statusLabel;
223    }
224
225    @Override
226    public int getMetricsCategory() {
227        return MetricsEvent.QS_WIFI;
228    }
229
230    @Override
231    protected boolean shouldAnnouncementBeDelayed() {
232        return mStateBeforeClick.value == mState.value;
233    }
234
235    @Override
236    protected String composeChangeAnnouncement() {
237        if (mState.value) {
238            return mContext.getString(R.string.accessibility_quick_settings_wifi_changed_on);
239        } else {
240            return mContext.getString(R.string.accessibility_quick_settings_wifi_changed_off);
241        }
242    }
243
244    @Override
245    public boolean isAvailable() {
246        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI);
247    }
248
249    private static String removeDoubleQuotes(String string) {
250        if (string == null) return null;
251        final int length = string.length();
252        if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) {
253            return string.substring(1, length - 1);
254        }
255        return string;
256    }
257
258    protected static final class CallbackInfo {
259        boolean enabled;
260        boolean connected;
261        int wifiSignalIconId;
262        String ssid;
263        boolean activityIn;
264        boolean activityOut;
265        String wifiSignalContentDescription;
266        boolean isTransient;
267        public String statusLabel;
268
269        @Override
270        public String toString() {
271            return new StringBuilder("CallbackInfo[")
272                    .append("enabled=").append(enabled)
273                    .append(",connected=").append(connected)
274                    .append(",wifiSignalIconId=").append(wifiSignalIconId)
275                    .append(",ssid=").append(ssid)
276                    .append(",activityIn=").append(activityIn)
277                    .append(",activityOut=").append(activityOut)
278                    .append(",wifiSignalContentDescription=").append(wifiSignalContentDescription)
279                    .append(",isTransient=").append(isTransient)
280                    .append(']').toString();
281        }
282    }
283
284    protected final class WifiSignalCallback implements SignalCallback {
285        final CallbackInfo mInfo = new CallbackInfo();
286
287        @Override
288        public void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon,
289                boolean activityIn, boolean activityOut, String description, boolean isTransient,
290                String statusLabel) {
291            if (DEBUG) Log.d(TAG, "onWifiSignalChanged enabled=" + enabled);
292            mInfo.enabled = enabled;
293            mInfo.connected = qsIcon.visible;
294            mInfo.wifiSignalIconId = qsIcon.icon;
295            mInfo.ssid = description;
296            mInfo.activityIn = activityIn;
297            mInfo.activityOut = activityOut;
298            mInfo.wifiSignalContentDescription = qsIcon.contentDescription;
299            mInfo.isTransient = isTransient;
300            mInfo.statusLabel = statusLabel;
301            if (isShowingDetail()) {
302                mDetailAdapter.updateItems();
303            }
304            refreshState();
305        }
306    }
307
308    protected class WifiDetailAdapter implements DetailAdapter,
309            NetworkController.AccessPointController.AccessPointCallback, QSDetailItems.Callback {
310
311        private QSDetailItems mItems;
312        private AccessPoint[] mAccessPoints;
313
314        @Override
315        public CharSequence getTitle() {
316            return mContext.getString(R.string.quick_settings_wifi_label);
317        }
318
319        public Intent getSettingsIntent() {
320            return WIFI_SETTINGS;
321        }
322
323        @Override
324        public Boolean getToggleState() {
325            return mState.value;
326        }
327
328        @Override
329        public void setToggleState(boolean state) {
330            if (DEBUG) Log.d(TAG, "setToggleState " + state);
331            MetricsLogger.action(mContext, MetricsEvent.QS_WIFI_TOGGLE, state);
332            mController.setWifiEnabled(state);
333        }
334
335        @Override
336        public int getMetricsCategory() {
337            return MetricsEvent.QS_WIFI_DETAILS;
338        }
339
340        @Override
341        public View createDetailView(Context context, View convertView, ViewGroup parent) {
342            if (DEBUG) Log.d(TAG, "createDetailView convertView=" + (convertView != null));
343            mAccessPoints = null;
344            mItems = QSDetailItems.convertOrInflate(context, convertView, parent);
345            mItems.setTagSuffix("Wifi");
346            mItems.setCallback(this);
347            mWifiController.scanForAccessPoints(); // updates APs and items
348            setItemsVisible(mState.value);
349            return mItems;
350        }
351
352        @Override
353        public void onAccessPointsChanged(final List<AccessPoint> accessPoints) {
354            mAccessPoints = accessPoints.toArray(new AccessPoint[accessPoints.size()]);
355            filterUnreachableAPs();
356
357            updateItems();
358        }
359
360        /** Filter unreachable APs from mAccessPoints */
361        private void filterUnreachableAPs() {
362            int numReachable = 0;
363            for (AccessPoint ap : mAccessPoints) {
364                if (ap.isReachable()) numReachable++;
365            }
366            if (numReachable != mAccessPoints.length) {
367                AccessPoint[] unfiltered = mAccessPoints;
368                mAccessPoints = new AccessPoint[numReachable];
369                int i = 0;
370                for (AccessPoint ap : unfiltered) {
371                    if (ap.isReachable()) mAccessPoints[i++] = ap;
372                }
373            }
374        }
375
376        @Override
377        public void onSettingsActivityTriggered(Intent settingsIntent) {
378            mActivityStarter.postStartActivityDismissingKeyguard(settingsIntent, 0);
379        }
380
381        @Override
382        public void onDetailItemClick(Item item) {
383            if (item == null || item.tag == null) return;
384            final AccessPoint ap = (AccessPoint) item.tag;
385            if (!ap.isActive()) {
386                if (mWifiController.connect(ap)) {
387                    mHost.collapsePanels();
388                }
389            }
390            showDetail(false);
391        }
392
393        @Override
394        public void onDetailItemDisconnect(Item item) {
395            // noop
396        }
397
398        public void setItemsVisible(boolean visible) {
399            if (mItems == null) return;
400            mItems.setItemsVisible(visible);
401        }
402
403        private void updateItems() {
404            if (mItems == null) return;
405            if ((mAccessPoints != null && mAccessPoints.length > 0)
406                    || !mSignalCallback.mInfo.enabled) {
407                fireScanStateChanged(false);
408            } else {
409                fireScanStateChanged(true);
410            }
411
412            // Wi-Fi is off
413            if (!mSignalCallback.mInfo.enabled) {
414                mItems.setEmptyState(R.drawable.ic_qs_wifi_detail_empty,
415                        R.string.wifi_is_off);
416                mItems.setItems(null);
417                return;
418            }
419
420            // No available access points
421            mItems.setEmptyState(R.drawable.ic_qs_wifi_detail_empty,
422                    R.string.quick_settings_wifi_detail_empty_text);
423
424            // Build the list
425            Item[] items = null;
426            if (mAccessPoints != null) {
427                items = new Item[mAccessPoints.length];
428                for (int i = 0; i < mAccessPoints.length; i++) {
429                    final AccessPoint ap = mAccessPoints[i];
430                    final Item item = new Item();
431                    item.tag = ap;
432                    item.iconResId = mWifiController.getIcon(ap);
433                    item.line1 = ap.getSsid();
434                    item.line2 = ap.isActive() ? ap.getSummary() : null;
435                    item.icon2 = ap.getSecurity() != AccessPoint.SECURITY_NONE
436                            ? R.drawable.qs_ic_wifi_lock
437                            : -1;
438                    items[i] = item;
439                }
440            }
441            mItems.setItems(items);
442        }
443    }
444}
445