1 /*
2  * Copyright (C) 2015 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 package com.android.systemui.statusbar.connectivity;
17 
18 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
19 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
20 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN;
21 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT;
22 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT;
23 
24 import android.content.Context;
25 import android.content.Intent;
26 import android.net.wifi.WifiManager;
27 import android.os.Handler;
28 import android.text.Html;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.internal.util.Preconditions;
32 import com.android.settingslib.SignalIcon.IconGroup;
33 import com.android.settingslib.SignalIcon.MobileIconGroup;
34 import com.android.settingslib.graph.SignalDrawable;
35 import com.android.settingslib.mobile.TelephonyIcons;
36 import com.android.settingslib.wifi.WifiStatusTracker;
37 import com.android.systemui.R;
38 import com.android.systemui.dagger.qualifiers.Background;
39 
40 import java.io.PrintWriter;
41 import java.util.BitSet;
42 
43 /** */
44 public class WifiSignalController extends SignalController<WifiState, IconGroup> {
45     private final boolean mHasMobileDataFeature;
46     private final WifiStatusTracker mWifiTracker;
47     private final IconGroup mUnmergedWifiIconGroup = WifiIcons.UNMERGED_WIFI;
48     private final MobileIconGroup mCarrierMergedWifiIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI;
49     private final WifiManager mWifiManager;
50 
51     private final Handler mBgHandler;
52 
WifiSignalController( Context context, boolean hasMobileDataFeature, CallbackHandler callbackHandler, NetworkControllerImpl networkController, WifiManager wifiManager, WifiStatusTrackerFactory trackerFactory, @Background Handler bgHandler)53     public WifiSignalController(
54             Context context,
55             boolean hasMobileDataFeature,
56             CallbackHandler callbackHandler,
57             NetworkControllerImpl networkController,
58             WifiManager wifiManager,
59             WifiStatusTrackerFactory trackerFactory,
60             @Background Handler bgHandler) {
61         super(
62                 "WifiSignalController",
63                 context,
64                 TRANSPORT_WIFI,
65                 callbackHandler,
66                 networkController);
67         mBgHandler = bgHandler;
68         mWifiManager = wifiManager;
69         mWifiTracker = trackerFactory.createTracker(this::handleStatusUpdated, bgHandler);
70         mWifiTracker.setListening(true);
71         mHasMobileDataFeature = hasMobileDataFeature;
72         if (wifiManager != null) {
73             wifiManager.registerTrafficStateCallback(context.getMainExecutor(),
74                     new WifiTrafficStateCallback());
75         }
76         mCurrentState.iconGroup = mLastState.iconGroup = mUnmergedWifiIconGroup;
77     }
78 
79     @Override
cleanState()80     protected WifiState cleanState() {
81         return new WifiState();
82     }
83 
refreshLocale()84     void refreshLocale() {
85         mWifiTracker.refreshLocale();
86     }
87 
88     @Override
notifyListeners(SignalCallback callback)89     public void notifyListeners(SignalCallback callback) {
90         if (mCurrentState.isCarrierMerged) {
91             if (mCurrentState.isDefault || !mNetworkController.isRadioOn()) {
92                 notifyListenersForCarrierWifi(callback);
93             }
94         } else {
95             notifyListenersForNonCarrierWifi(callback);
96         }
97     }
98 
notifyListenersForNonCarrierWifi(SignalCallback callback)99     private void notifyListenersForNonCarrierWifi(SignalCallback callback) {
100         // only show wifi in the cluster if connected or if wifi-only
101         boolean visibleWhenEnabled = mContext.getResources().getBoolean(
102                 R.bool.config_showWifiIndicatorWhenEnabled);
103         boolean wifiVisible = mCurrentState.enabled && (
104                 (mCurrentState.connected && mCurrentState.inetCondition == 1)
105                         || !mHasMobileDataFeature || mCurrentState.isDefault
106                         || visibleWhenEnabled);
107         String wifiDesc = mCurrentState.connected ? mCurrentState.ssid : null;
108         boolean ssidPresent = wifiVisible && mCurrentState.ssid != null;
109         String contentDescription = getTextIfExists(getContentDescription()).toString();
110         if (mCurrentState.inetCondition == 0) {
111             contentDescription += ("," + mContext.getString(R.string.data_connection_no_internet));
112         }
113         IconState statusIcon = new IconState(
114                 wifiVisible, getCurrentIconId(), contentDescription);
115         IconState qsIcon = null;
116         if (mCurrentState.isDefault || (!mNetworkController.isRadioOn()
117                 && !mNetworkController.isEthernetDefault())) {
118             qsIcon = new IconState(mCurrentState.connected,
119                     mWifiTracker.isCaptivePortal ? R.drawable.ic_qs_wifi_disconnected
120                             : getQsCurrentIconId(), contentDescription);
121         }
122         WifiIndicators wifiIndicators = new WifiIndicators(
123                 mCurrentState.enabled, statusIcon, qsIcon,
124                 ssidPresent && mCurrentState.activityIn,
125                 ssidPresent && mCurrentState.activityOut,
126                 wifiDesc, mCurrentState.isTransient, mCurrentState.statusLabel
127         );
128         callback.setWifiIndicators(wifiIndicators);
129     }
130 
notifyListenersForCarrierWifi(SignalCallback callback)131     private void notifyListenersForCarrierWifi(SignalCallback callback) {
132         MobileIconGroup icons = mCarrierMergedWifiIconGroup;
133         String contentDescription = getTextIfExists(getContentDescription()).toString();
134         CharSequence dataContentDescriptionHtml = getTextIfExists(icons.dataContentDescription);
135 
136         CharSequence dataContentDescription = Html.fromHtml(
137                 dataContentDescriptionHtml.toString(), 0).toString();
138         if (mCurrentState.inetCondition == 0) {
139             dataContentDescription = mContext.getString(R.string.data_connection_no_internet);
140         }
141         boolean sbVisible = mCurrentState.enabled && mCurrentState.connected
142                 && mCurrentState.isDefault;
143         IconState statusIcon =
144                 new IconState(sbVisible, getCurrentIconIdForCarrierWifi(), contentDescription);
145         int typeIcon = sbVisible ? icons.dataType : 0;
146         int qsTypeIcon = 0;
147         IconState qsIcon = null;
148         if (sbVisible) {
149             qsTypeIcon = icons.dataType;
150             qsIcon = new IconState(mCurrentState.connected, getQsCurrentIconIdForCarrierWifi(),
151                     contentDescription);
152         }
153         CharSequence description =
154                 mNetworkController.getNetworkNameForCarrierWiFi(mCurrentState.subId);
155         MobileDataIndicators mobileDataIndicators = new MobileDataIndicators(
156                 statusIcon, qsIcon, typeIcon, qsTypeIcon,
157                 mCurrentState.activityIn, mCurrentState.activityOut, dataContentDescription,
158                 dataContentDescriptionHtml, description,
159                 mCurrentState.subId, /* roaming= */ false, /* showTriangle= */ true
160         );
161         callback.setMobileDataIndicators(mobileDataIndicators);
162     }
163 
getCurrentIconIdForCarrierWifi()164     private int getCurrentIconIdForCarrierWifi() {
165         int level = mCurrentState.level;
166         // The WiFi signal level returned by WifiManager#calculateSignalLevel start from 0, so
167         // WifiManager#getMaxSignalLevel + 1 represents the total level buckets count.
168         int totalLevel = mWifiManager.getMaxSignalLevel() + 1;
169         // A carrier merged connection could come from a WIFI *or* CELLULAR transport, so we can't
170         // use [mCurrentState.inetCondition], which only checks the WIFI status. Instead, check if
171         // the default connection is validated at all.
172         boolean noInternet = !mCurrentState.isDefaultConnectionValidated;
173         if (mCurrentState.connected) {
174             return SignalDrawable.getState(level, totalLevel, noInternet);
175         } else if (mCurrentState.enabled) {
176             return SignalDrawable.getEmptyState(totalLevel);
177         } else {
178             return 0;
179         }
180     }
181 
getQsCurrentIconIdForCarrierWifi()182     private int getQsCurrentIconIdForCarrierWifi() {
183         return getCurrentIconIdForCarrierWifi();
184     }
185 
186     /**
187      * Fetches wifi initial state replacing the initial sticky broadcast.
188      */
fetchInitialState()189     public void fetchInitialState() {
190         doInBackground(() -> {
191             mWifiTracker.fetchInitialState();
192             copyWifiStates();
193             notifyListenersIfNecessary();
194         });
195     }
196 
197     /**
198      * Extract wifi state directly from broadcasts about changes in wifi state.
199      */
handleBroadcast(Intent intent)200     void handleBroadcast(Intent intent) {
201         doInBackground(() -> {
202             mWifiTracker.handleBroadcast(intent);
203             copyWifiStates();
204             notifyListenersIfNecessary();
205         });
206     }
207 
handleStatusUpdated()208     private void handleStatusUpdated() {
209         // The WifiStatusTracker callback comes in on the main thread, but the rest of our data
210         // access happens on the bgHandler
211         doInBackground(() -> {
212             copyWifiStates();
213             notifyListenersIfNecessary();
214         });
215     }
216 
doInBackground(Runnable action)217     private void doInBackground(Runnable action) {
218         if (Thread.currentThread() != mBgHandler.getLooper().getThread()) {
219             mBgHandler.post(action);
220         } else {
221             action.run();
222         }
223     }
224 
copyWifiStates()225     private void copyWifiStates() {
226         // Data access should only happen on our bg thread
227         Preconditions.checkState(mBgHandler.getLooper().isCurrentThread());
228 
229         mCurrentState.enabled = mWifiTracker.enabled;
230         mCurrentState.isDefault = mWifiTracker.isDefaultNetwork;
231         mCurrentState.connected = mWifiTracker.connected;
232         mCurrentState.ssid = mWifiTracker.ssid;
233         mCurrentState.rssi = mWifiTracker.rssi;
234         mCurrentState.level = mWifiTracker.level;
235         mCurrentState.statusLabel = mWifiTracker.statusLabel;
236         mCurrentState.isCarrierMerged = mWifiTracker.isCarrierMerged;
237         mCurrentState.subId = mWifiTracker.subId;
238         mCurrentState.iconGroup =
239                 mCurrentState.isCarrierMerged ? mCarrierMergedWifiIconGroup
240                         : mUnmergedWifiIconGroup;
241     }
242 
isCarrierMergedWifi(int subId)243     boolean isCarrierMergedWifi(int subId) {
244         return mCurrentState.isDefault
245                 && mCurrentState.isCarrierMerged && (mCurrentState.subId == subId);
246     }
247 
248     @Override
updateConnectivity(BitSet connectedTransports, BitSet validatedTransports)249     void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) {
250         mCurrentState.inetCondition = validatedTransports.get(mTransportType) ? 1 : 0;
251         // Because a carrier merged connection can come from either a CELLULAR *or* WIFI transport,
252         // we need to also store if either transport is validated to correctly display the carrier
253         // merged case.
254         mCurrentState.isDefaultConnectionValidated =
255                 validatedTransports.get(TRANSPORT_CELLULAR)
256                         || validatedTransports.get(TRANSPORT_WIFI);
257         notifyListenersIfNecessary();
258     }
259 
260     @VisibleForTesting
setActivity(int wifiActivity)261     void setActivity(int wifiActivity) {
262         mCurrentState.activityIn = wifiActivity == DATA_ACTIVITY_INOUT
263                 || wifiActivity == DATA_ACTIVITY_IN;
264         mCurrentState.activityOut = wifiActivity == DATA_ACTIVITY_INOUT
265                 || wifiActivity == DATA_ACTIVITY_OUT;
266         notifyListenersIfNecessary();
267     }
268 
269     @Override
dump(PrintWriter pw)270     public void dump(PrintWriter pw) {
271         super.dump(pw);
272         mWifiTracker.dump(pw);
273         dumpTableData(pw);
274     }
275 
276     /**
277      * Handler to receive the data activity on wifi.
278      */
279     private class WifiTrafficStateCallback implements WifiManager.TrafficStateCallback {
280         @Override
onStateChanged(int state)281         public void onStateChanged(int state) {
282             setActivity(state);
283         }
284     }
285 }
286