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