1 /* 2 * Copyright (C) 2024 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.google.snippet.wifi; 18 19 import static android.net.wifi.DeauthenticationReasonCode.REASON_UNKNOWN; 20 21 import android.content.Context; 22 import android.net.wifi.SoftApConfiguration; 23 import android.net.wifi.SoftApInfo; 24 import android.net.wifi.WifiClient; 25 import android.net.wifi.WifiConfiguration; 26 import android.net.wifi.WifiManager; 27 import android.os.Handler; 28 import android.os.HandlerThread; 29 import android.util.Log; 30 31 import androidx.annotation.NonNull; 32 import androidx.test.platform.app.InstrumentationRegistry; 33 34 import com.android.compatibility.common.util.PollingCheck; 35 import com.android.wifi.flags.Flags; 36 37 import com.google.android.mobly.snippet.Snippet; 38 import com.google.android.mobly.snippet.event.EventCache; 39 import com.google.android.mobly.snippet.event.SnippetEvent; 40 import com.google.android.mobly.snippet.rpc.AsyncRpc; 41 import com.google.android.mobly.snippet.rpc.Rpc; 42 43 import org.json.JSONException; 44 import org.json.JSONObject; 45 46 import java.util.List; 47 import java.util.concurrent.TimeUnit; 48 49 /** Snippet class for WifiManager. */ 50 public class WifiManagerSnippet implements Snippet { 51 52 private static final String TAG = "WifiManagerSnippet"; 53 private static final long POLLING_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10); 54 55 private final WifiManager mWifiManager; 56 private final Handler mHandler; 57 private final Object mLock = new Object(); 58 59 private WifiManagerSnippet.SnippetSoftApCallback mSoftApCallback; 60 private WifiManager.LocalOnlyHotspotReservation mLocalOnlyHotspotReservation; 61 62 /** Callback to listen in and verify events to SoftAp. */ 63 private static class SnippetSoftApCallback implements WifiManager.SoftApCallback { 64 private final String mCallbackId; 65 SnippetSoftApCallback(String callbackId)66 SnippetSoftApCallback(String callbackId) { 67 mCallbackId = callbackId; 68 } 69 70 @Override onConnectedClientsChanged(@onNull SoftApInfo info, @NonNull List<WifiClient> clients)71 public void onConnectedClientsChanged(@NonNull SoftApInfo info, 72 @NonNull List<WifiClient> clients) { 73 Log.d(TAG, "onConnectedClientsChanged, info=" + info + ", clients=" + clients); 74 SnippetEvent event = new SnippetEvent(mCallbackId, "onConnectedClientsChanged"); 75 event.getData().putInt("connectedClientsCount", clients.size()); 76 String macAddress = null; 77 if (!clients.isEmpty()) { 78 // In our Mobly test cases, there is only ever one other device. 79 WifiClient client = clients.getFirst(); 80 macAddress = client.getMacAddress().toString(); 81 } 82 event.getData().putString("clientMacAddress", macAddress); 83 EventCache.getInstance().postEvent(event); 84 } 85 86 @Override onClientsDisconnected(@onNull SoftApInfo info, @NonNull List<WifiClient> clients)87 public void onClientsDisconnected(@NonNull SoftApInfo info, 88 @NonNull List<WifiClient> clients) { 89 Log.d(TAG, "onClientsDisconnected, info=" + info + ", clients=" + clients); 90 SnippetEvent event = new SnippetEvent(mCallbackId, "onClientsDisconnected"); 91 event.getData().putInt("disconnectedClientsCount", clients.size()); 92 String macAddress = null; 93 int disconnectReason = REASON_UNKNOWN; 94 if (!clients.isEmpty()) { 95 // In our Mobly test cases, there is only ever one other device. 96 WifiClient client = clients.getFirst(); 97 macAddress = client.getMacAddress().toString(); 98 disconnectReason = client.getDisconnectReason(); 99 } 100 event.getData().putString("clientMacAddress", macAddress); 101 event.getData().putInt("clientDisconnectReason", disconnectReason); 102 EventCache.getInstance().postEvent(event); 103 } 104 } 105 106 /** Callback class to get the results of local hotspot start. */ 107 private class SnippetLocalOnlyHotspotCallback extends WifiManager.LocalOnlyHotspotCallback { 108 private final String mCallbackId; 109 SnippetLocalOnlyHotspotCallback(String callbackId)110 SnippetLocalOnlyHotspotCallback(String callbackId) { 111 mCallbackId = callbackId; 112 } 113 114 @Override onStarted(WifiManager.LocalOnlyHotspotReservation reservation)115 public void onStarted(WifiManager.LocalOnlyHotspotReservation reservation) { 116 Log.d(TAG, "Local-only hotspot onStarted"); 117 synchronized (mLock) { 118 mLocalOnlyHotspotReservation = reservation; 119 } 120 SoftApConfiguration currentConfiguration = reservation.getSoftApConfiguration(); 121 SnippetEvent event = new SnippetEvent(mCallbackId, "onStarted"); 122 event.getData().putString("ssid", 123 WifiJsonConverter.trimQuotationMarks( 124 currentConfiguration.getWifiSsid().toString())); 125 event.getData() 126 .putString( 127 "passphrase", 128 currentConfiguration.getPassphrase()); 129 EventCache.getInstance().postEvent(event); 130 } 131 } 132 WifiManagerSnippet()133 public WifiManagerSnippet() { 134 Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 135 mWifiManager = context.getSystemService(WifiManager.class); 136 HandlerThread handlerThread = new HandlerThread(getClass().getSimpleName()); 137 handlerThread.start(); 138 mHandler = new Handler(handlerThread.getLooper()); 139 } 140 141 /** 142 * Starts local-only hotspot. 143 * 144 * @param callbackId A unique identifier assigned automatically by Mobly. 145 */ 146 @AsyncRpc(description = "Call to start local-only hotspot.") wifiStartLocalOnlyHotspot(String callbackId)147 public void wifiStartLocalOnlyHotspot(String callbackId) { 148 mWifiManager.startLocalOnlyHotspot(new SnippetLocalOnlyHotspotCallback(callbackId), 149 mHandler); 150 } 151 152 /** 153 * Stop local-only hotspot. 154 */ 155 @Rpc(description = "Call to stop local-only hotspot.") wifiStopLocalOnlyHotspot()156 public void wifiStopLocalOnlyHotspot() { 157 synchronized (mLock) { 158 if (mLocalOnlyHotspotReservation == null) { 159 Log.w(TAG, "Requested to stop local-only hotspot which was already stopped."); 160 return; 161 } 162 163 mLocalOnlyHotspotReservation.close(); 164 mLocalOnlyHotspotReservation = null; 165 } 166 } 167 168 /** 169 * Registers a callback for Soft AP. 170 * 171 * @param callbackId A unique identifier assigned automatically by Mobly. 172 */ 173 @AsyncRpc(description = "Call to register SoftApCallback.") wifiRegisterSoftApCallback(String callbackId)174 public void wifiRegisterSoftApCallback(String callbackId) { 175 if (mSoftApCallback == null) { 176 mSoftApCallback = new SnippetSoftApCallback(callbackId); 177 mWifiManager.registerSoftApCallback(mHandler::post, mSoftApCallback); 178 } 179 } 180 181 182 /** 183 * Registers a callback for local-only hotspot. 184 * 185 * @param callbackId A unique identifier assigned automatically by Mobly. 186 */ 187 @AsyncRpc(description = "Call to register SoftApCallback for local-only hotspot.") wifiRegisterLocalOnlyHotspotSoftApCallback(String callbackId)188 public void wifiRegisterLocalOnlyHotspotSoftApCallback(String callbackId) { 189 if (mSoftApCallback == null) { 190 mSoftApCallback = new SnippetSoftApCallback(callbackId); 191 mWifiManager.registerLocalOnlyHotspotSoftApCallback(mHandler::post, 192 mSoftApCallback); 193 } 194 } 195 196 /** 197 * Checks if the device supports portable hotspot. 198 * 199 * @return {@code true} if the device supports portable hotspot, {@code false} otherwise. 200 */ 201 @Rpc(description = "Check if the device supports portable hotspot.") wifiIsPortableHotspotSupported()202 public boolean wifiIsPortableHotspotSupported() { 203 return mWifiManager.isPortableHotspotSupported(); 204 } 205 206 /** 207 * Unregisters soft AP callback function. 208 */ 209 @Rpc(description = "Unregister soft AP callback function.") wifiUnregisterSoftApCallback()210 public void wifiUnregisterSoftApCallback() { 211 if (mSoftApCallback == null) { 212 return; 213 } 214 215 mWifiManager.unregisterSoftApCallback(mSoftApCallback); 216 mSoftApCallback = null; 217 } 218 219 /** 220 * Unregisters soft AP callback function. 221 */ 222 @Rpc(description = "Unregister soft AP callback function.") wifiUnregisterLocalOnlyHotspotSoftApCallback()223 public void wifiUnregisterLocalOnlyHotspotSoftApCallback() { 224 if (mSoftApCallback == null) { 225 return; 226 } 227 228 mWifiManager.unregisterLocalOnlyHotspotSoftApCallback(mSoftApCallback); 229 mSoftApCallback = null; 230 } 231 232 /** 233 * Enables all saved networks. 234 */ 235 @Rpc(description = "Enable all saved networks.") wifiEnableAllSavedNetworks()236 public void wifiEnableAllSavedNetworks() { 237 for (WifiConfiguration savedNetwork : mWifiManager.getConfiguredNetworks()) { 238 mWifiManager.enableNetwork(savedNetwork.networkId, false); 239 } 240 } 241 242 /** 243 * Disables all saved networks. 244 */ 245 @Rpc(description = "Disable all saved networks.") wifiDisableAllSavedNetworks()246 public void wifiDisableAllSavedNetworks() { 247 for (WifiConfiguration savedNetwork : mWifiManager.getConfiguredNetworks()) { 248 mWifiManager.disableNetwork(savedNetwork.networkId); 249 } 250 } 251 252 /** 253 * Checks the softap_disconnect_reason flag. 254 * 255 * @return {@code true} if the softap_disconnect_reason flag is enabled, {@code false} 256 * otherwise. 257 */ 258 @Rpc(description = "Checks SoftApDisconnectReason flag.") wifiCheckSoftApDisconnectReasonFlag()259 public boolean wifiCheckSoftApDisconnectReasonFlag() { 260 return Flags.softapDisconnectReason(); 261 } 262 263 /** 264 * Gets the Wi-Fi tethered AP Configuration. 265 * 266 * @return AP details in {@link SoftApConfiguration} as JSON format. 267 */ 268 @Rpc(description = "Get current SoftApConfiguration.") wifiGetSoftApConfiguration()269 public JSONObject wifiGetSoftApConfiguration() throws JSONException { 270 return WifiJsonConverter.serialize(mWifiManager.getSoftApConfiguration()); 271 } 272 273 /** 274 * Waits for tethering to be disabled. 275 * 276 * @return {@code true} if tethering is disabled within the timeout, {@code false} otherwise. 277 */ 278 @Rpc(description = "Call to wait for tethering to be disabled.") wifiWaitForTetheringDisabled()279 public boolean wifiWaitForTetheringDisabled() { 280 try { 281 PollingCheck.check("Tethering NOT disabled", POLLING_TIMEOUT_MS, 282 () -> !mWifiManager.isWifiApEnabled()); 283 } catch (Exception e) { 284 return false; 285 } 286 return true; 287 } 288 } 289