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.statusbar.policy;
18 
19 import static android.net.TetheringManager.TETHERING_WIFI;
20 
21 import android.content.Context;
22 import android.net.ConnectivityManager;
23 import android.net.TetheringManager;
24 import android.net.TetheringManager.TetheringRequest;
25 import android.net.wifi.WifiClient;
26 import android.net.wifi.WifiManager;
27 import android.os.Handler;
28 import android.os.HandlerExecutor;
29 import android.os.UserManager;
30 import android.util.Log;
31 
32 import androidx.annotation.NonNull;
33 
34 import com.android.internal.util.ConcurrentUtils;
35 import com.android.systemui.R;
36 import com.android.systemui.dagger.SysUISingleton;
37 import com.android.systemui.dagger.qualifiers.Background;
38 import com.android.systemui.dagger.qualifiers.Main;
39 import com.android.systemui.dump.DumpManager;
40 import com.android.systemui.settings.UserTracker;
41 
42 import java.io.PrintWriter;
43 import java.util.ArrayList;
44 import java.util.List;
45 
46 import javax.inject.Inject;
47 
48 /**
49  * Controller used to retrieve information related to a hotspot.
50  */
51 @SysUISingleton
52 public class HotspotControllerImpl implements HotspotController, WifiManager.SoftApCallback {
53 
54     private static final String TAG = "HotspotController";
55     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
56 
57     private final ArrayList<Callback> mCallbacks = new ArrayList<>();
58     private final TetheringManager mTetheringManager;
59     private final WifiManager mWifiManager;
60     private final Handler mMainHandler;
61     private final Context mContext;
62     private final UserTracker mUserTracker;
63 
64     private int mHotspotState;
65     private volatile int mNumConnectedDevices;
66     // Assume tethering is available until told otherwise
67     private volatile boolean mIsTetheringSupported = true;
68     private final boolean mIsTetheringSupportedConfig;
69     private volatile boolean mHasTetherableWifiRegexs = true;
70     private boolean mWaitingForTerminalState;
71 
72     private TetheringManager.TetheringEventCallback mTetheringCallback =
73             new TetheringManager.TetheringEventCallback() {
74                 @Override
75                 public void onTetheringSupported(boolean supported) {
76                     if (mIsTetheringSupported != supported) {
77                         mIsTetheringSupported = supported;
78                         fireHotspotAvailabilityChanged();
79                     }
80                 }
81 
82                 @Override
83                 public void onTetherableInterfaceRegexpsChanged(
84                         TetheringManager.TetheringInterfaceRegexps reg) {
85                     final boolean newValue = reg.getTetherableWifiRegexs().size() != 0;
86                     if (mHasTetherableWifiRegexs != newValue) {
87                         mHasTetherableWifiRegexs = newValue;
88                         fireHotspotAvailabilityChanged();
89                     }
90                 }
91             };
92 
93     /**
94      * Controller used to retrieve information related to a hotspot.
95      */
96     @Inject
HotspotControllerImpl( Context context, UserTracker userTracker, @Main Handler mainHandler, @Background Handler backgroundHandler, DumpManager dumpManager)97     public HotspotControllerImpl(
98             Context context,
99             UserTracker userTracker,
100             @Main Handler mainHandler,
101             @Background Handler backgroundHandler,
102             DumpManager dumpManager) {
103         mContext = context;
104         mUserTracker = userTracker;
105         mTetheringManager = context.getSystemService(TetheringManager.class);
106         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
107         mMainHandler = mainHandler;
108         mIsTetheringSupportedConfig = context.getResources()
109                 .getBoolean(R.bool.config_show_wifi_tethering);
110         if (mIsTetheringSupportedConfig) {
111             mTetheringManager.registerTetheringEventCallback(
112                     new HandlerExecutor(backgroundHandler), mTetheringCallback);
113         }
114         dumpManager.registerDumpable(getClass().getSimpleName(), this);
115     }
116 
117     /**
118      * Whether hotspot is currently supported.
119      *
120      * This may return {@code true} immediately on creation of the controller, but may be updated
121      * later as capabilities are collected from System Server.
122      *
123      * Callbacks from this controllers will notify if the state changes.
124      *
125      * @return {@code true} if hotspot is supported (or we haven't been told it's not)
126      * @see #addCallback
127      */
128     @Override
isHotspotSupported()129     public boolean isHotspotSupported() {
130         return mIsTetheringSupportedConfig && mIsTetheringSupported && mHasTetherableWifiRegexs
131                 && UserManager.get(mContext).isUserAdmin(mUserTracker.getUserId());
132     }
133 
dump(PrintWriter pw, String[] args)134     public void dump(PrintWriter pw, String[] args) {
135         pw.println("HotspotController state:");
136         pw.print("  available="); pw.println(isHotspotSupported());
137         pw.print("  mHotspotState="); pw.println(stateToString(mHotspotState));
138         pw.print("  mNumConnectedDevices="); pw.println(mNumConnectedDevices);
139         pw.print("  mWaitingForTerminalState="); pw.println(mWaitingForTerminalState);
140     }
141 
stateToString(int hotspotState)142     private static String stateToString(int hotspotState) {
143         switch (hotspotState) {
144             case WifiManager.WIFI_AP_STATE_DISABLED:
145                 return "DISABLED";
146             case WifiManager.WIFI_AP_STATE_DISABLING:
147                 return "DISABLING";
148             case WifiManager.WIFI_AP_STATE_ENABLED:
149                 return "ENABLED";
150             case WifiManager.WIFI_AP_STATE_ENABLING:
151                 return "ENABLING";
152             case WifiManager.WIFI_AP_STATE_FAILED:
153                 return "FAILED";
154         }
155         return null;
156     }
157 
158     /**
159      * Adds {@code callback} to the controller. The controller will update the callback on state
160      * changes. It will immediately trigger the callback added to notify current state.
161      */
162     @Override
addCallback(@onNull Callback callback)163     public void addCallback(@NonNull Callback callback) {
164         synchronized (mCallbacks) {
165             if (callback == null || mCallbacks.contains(callback)) return;
166             if (DEBUG) Log.d(TAG, "addCallback " + callback);
167             mCallbacks.add(callback);
168             if (mWifiManager != null) {
169                 if (mCallbacks.size() == 1) {
170                     mWifiManager.registerSoftApCallback(new HandlerExecutor(mMainHandler), this);
171                 } else {
172                     // mWifiManager#registerSoftApCallback triggers a call to onNumClientsChanged
173                     // on the Main Handler. In order to always update the callback on added, we
174                     // make this call when adding callbacks after the first.
175                     mMainHandler.post(() ->
176                             callback.onHotspotChanged(isHotspotEnabled(), mNumConnectedDevices));
177                 }
178             }
179         }
180     }
181 
182     @Override
removeCallback(@onNull Callback callback)183     public void removeCallback(@NonNull Callback callback) {
184         if (callback == null) return;
185         if (DEBUG) Log.d(TAG, "removeCallback " + callback);
186         synchronized (mCallbacks) {
187             mCallbacks.remove(callback);
188             if (mCallbacks.isEmpty() && mWifiManager != null) {
189                 mWifiManager.unregisterSoftApCallback(this);
190             }
191         }
192     }
193 
194     @Override
isHotspotEnabled()195     public boolean isHotspotEnabled() {
196         return mHotspotState == WifiManager.WIFI_AP_STATE_ENABLED;
197     }
198 
199     @Override
isHotspotTransient()200     public boolean isHotspotTransient() {
201         return mWaitingForTerminalState || (mHotspotState == WifiManager.WIFI_AP_STATE_ENABLING);
202     }
203 
204     @Override
setHotspotEnabled(boolean enabled)205     public void setHotspotEnabled(boolean enabled) {
206         if (mWaitingForTerminalState) {
207             if (DEBUG) Log.d(TAG, "Ignoring setHotspotEnabled; waiting for terminal state.");
208             return;
209         }
210         if (enabled) {
211             mWaitingForTerminalState = true;
212             if (DEBUG) Log.d(TAG, "Starting tethering");
213             mTetheringManager.startTethering(new TetheringRequest.Builder(
214                     TETHERING_WIFI).setShouldShowEntitlementUi(false).build(),
215                     ConcurrentUtils.DIRECT_EXECUTOR,
216                     new TetheringManager.StartTetheringCallback() {
217                         @Override
218                         public void onTetheringFailed(final int result) {
219                             if (DEBUG) Log.d(TAG, "onTetheringFailed");
220                             maybeResetSoftApState();
221                             fireHotspotChangedCallback();
222                         }
223                     });
224         } else {
225             mTetheringManager.stopTethering(ConnectivityManager.TETHERING_WIFI);
226         }
227     }
228 
229     @Override
getNumConnectedDevices()230     public int getNumConnectedDevices() {
231         return mNumConnectedDevices;
232     }
233 
234     /**
235      * Sends a hotspot changed callback.
236      * Be careful when calling over multiple threads, especially if one of them is the main thread
237      * (as it can be blocked).
238      */
fireHotspotChangedCallback()239     private void fireHotspotChangedCallback() {
240         List<Callback> list;
241         synchronized (mCallbacks) {
242             list = new ArrayList<>(mCallbacks);
243         }
244         for (Callback callback : list) {
245             callback.onHotspotChanged(isHotspotEnabled(), mNumConnectedDevices);
246         }
247     }
248 
249     /**
250      * Sends a hotspot available changed callback.
251      */
fireHotspotAvailabilityChanged()252     private void fireHotspotAvailabilityChanged() {
253         List<Callback> list;
254         synchronized (mCallbacks) {
255             list = new ArrayList<>(mCallbacks);
256         }
257         for (Callback callback : list) {
258             callback.onHotspotAvailabilityChanged(isHotspotSupported());
259         }
260     }
261 
262     @Override
onStateChanged(int state, int failureReason)263     public void onStateChanged(int state, int failureReason) {
264         // Update internal hotspot state for tracking before using any enabled/callback methods.
265         mHotspotState = state;
266 
267         maybeResetSoftApState();
268         if (!isHotspotEnabled()) {
269             // Reset num devices if the hotspot is no longer enabled so we don't get ghost
270             // counters.
271             mNumConnectedDevices = 0;
272         }
273 
274         fireHotspotChangedCallback();
275     }
276 
maybeResetSoftApState()277     private void maybeResetSoftApState() {
278         if (!mWaitingForTerminalState) {
279             return; // Only reset soft AP state if enabled from this controller.
280         }
281         switch (mHotspotState) {
282             case WifiManager.WIFI_AP_STATE_FAILED:
283                 // TODO(b/110697252): must be called to reset soft ap state after failure
284                 mTetheringManager.stopTethering(ConnectivityManager.TETHERING_WIFI);
285                 // Fall through
286             case WifiManager.WIFI_AP_STATE_ENABLED:
287             case WifiManager.WIFI_AP_STATE_DISABLED:
288                 mWaitingForTerminalState = false;
289                 break;
290             case WifiManager.WIFI_AP_STATE_ENABLING:
291             case WifiManager.WIFI_AP_STATE_DISABLING:
292             default:
293                 break;
294         }
295     }
296 
297     @Override
onConnectedClientsChanged(List<WifiClient> clients)298     public void onConnectedClientsChanged(List<WifiClient> clients) {
299         mNumConnectedDevices = clients.size();
300         fireHotspotChangedCallback();
301     }
302 }
303