1 /* 2 * Copyright (C) 2023 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.wifitrackerlib; 18 19 import static android.net.wifi.WifiInfo.DEFAULT_MAC_ADDRESS; 20 import static android.os.Build.VERSION_CODES; 21 22 import android.annotation.TargetApi; 23 import android.content.Context; 24 import android.net.wifi.WifiInfo; 25 import android.net.wifi.WifiManager; 26 import android.net.wifi.sharedconnectivity.app.HotspotNetwork; 27 import android.net.wifi.sharedconnectivity.app.HotspotNetworkConnectionStatus; 28 import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo; 29 import android.net.wifi.sharedconnectivity.app.SharedConnectivityManager; 30 import android.os.Handler; 31 import android.text.BidiFormatter; 32 import android.text.TextUtils; 33 import android.util.Log; 34 35 import androidx.annotation.IntDef; 36 import androidx.annotation.IntRange; 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 import androidx.annotation.WorkerThread; 40 41 import org.json.JSONException; 42 import org.json.JSONObject; 43 44 import java.lang.annotation.Retention; 45 import java.lang.annotation.RetentionPolicy; 46 import java.util.ArrayList; 47 import java.util.Collections; 48 import java.util.Objects; 49 50 /** 51 * WifiEntry representation of a Hotspot Network provided via {@link SharedConnectivityManager}. 52 */ 53 @TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE) 54 public class HotspotNetworkEntry extends WifiEntry { 55 static final String TAG = "HotspotNetworkEntry"; 56 public static final String KEY_PREFIX = "HotspotNetworkEntry:"; 57 58 @NonNull private final WifiTrackerInjector mInjector; 59 @NonNull private final Context mContext; 60 @Nullable private final SharedConnectivityManager mSharedConnectivityManager; 61 62 @Nullable private HotspotNetwork mHotspotNetworkData; 63 @NonNull private HotspotNetworkEntryKey mKey; 64 65 /** 66 * If editing this IntDef also edit the definition in: 67 * {@link android.net.wifi.sharedconnectivity.app.HotspotNetwork} 68 * 69 * @hide 70 */ 71 @Retention(RetentionPolicy.SOURCE) 72 @IntDef({ 73 HotspotNetwork.NETWORK_TYPE_UNKNOWN, 74 HotspotNetwork.NETWORK_TYPE_CELLULAR, 75 HotspotNetwork.NETWORK_TYPE_WIFI, 76 HotspotNetwork.NETWORK_TYPE_ETHERNET 77 }) 78 public @interface NetworkType {} // TODO(b/271868642): Add IfThisThanThat lint 79 80 /** 81 * If editing this IntDef also edit the definition in: 82 * {@link android.net.wifi.sharedconnectivity.app.NetworkProviderInfo} 83 * 84 * @hide 85 */ 86 @Retention(RetentionPolicy.SOURCE) 87 @IntDef({ 88 NetworkProviderInfo.DEVICE_TYPE_UNKNOWN, 89 NetworkProviderInfo.DEVICE_TYPE_PHONE, 90 NetworkProviderInfo.DEVICE_TYPE_TABLET, 91 NetworkProviderInfo.DEVICE_TYPE_LAPTOP, 92 NetworkProviderInfo.DEVICE_TYPE_WATCH, 93 NetworkProviderInfo.DEVICE_TYPE_AUTO 94 }) 95 public @interface DeviceType {} // TODO(b/271868642): Add IfThisThanThat lint 96 97 /** 98 * If editing this IntDef also edit the definition in: 99 * {@link android.net.wifi.sharedconnectivity.app.HotspotNetworkConnectionStatus} 100 * 101 * @hide 102 */ 103 @Retention(RetentionPolicy.SOURCE) 104 @IntDef({ 105 HotspotNetworkConnectionStatus.CONNECTION_STATUS_UNKNOWN, 106 HotspotNetworkConnectionStatus.CONNECTION_STATUS_ENABLING_HOTSPOT, 107 HotspotNetworkConnectionStatus.CONNECTION_STATUS_UNKNOWN_ERROR, 108 HotspotNetworkConnectionStatus.CONNECTION_STATUS_PROVISIONING_FAILED, 109 HotspotNetworkConnectionStatus.CONNECTION_STATUS_TETHERING_TIMEOUT, 110 HotspotNetworkConnectionStatus.CONNECTION_STATUS_TETHERING_UNSUPPORTED, 111 HotspotNetworkConnectionStatus.CONNECTION_STATUS_NO_CELL_DATA, 112 HotspotNetworkConnectionStatus.CONNECTION_STATUS_ENABLING_HOTSPOT_FAILED, 113 HotspotNetworkConnectionStatus.CONNECTION_STATUS_ENABLING_HOTSPOT_TIMEOUT, 114 HotspotNetworkConnectionStatus.CONNECTION_STATUS_CONNECT_TO_HOTSPOT_FAILED, 115 }) 116 public @interface ConnectionStatus {} // TODO(b/271868642): Add IfThisThanThat lint 117 118 /** 119 * Create a HotspotNetworkEntry from HotspotNetwork data. 120 */ HotspotNetworkEntry( @onNull WifiTrackerInjector injector, @NonNull Context context, @NonNull Handler callbackHandler, @NonNull WifiManager wifiManager, @Nullable SharedConnectivityManager sharedConnectivityManager, @NonNull HotspotNetwork hotspotNetworkData)121 HotspotNetworkEntry( 122 @NonNull WifiTrackerInjector injector, 123 @NonNull Context context, @NonNull Handler callbackHandler, 124 @NonNull WifiManager wifiManager, 125 @Nullable SharedConnectivityManager sharedConnectivityManager, 126 @NonNull HotspotNetwork hotspotNetworkData) { 127 super(injector, callbackHandler, wifiManager, false /*forSavedNetworksPage*/); 128 mInjector = injector; 129 mContext = context; 130 mSharedConnectivityManager = sharedConnectivityManager; 131 mHotspotNetworkData = hotspotNetworkData; 132 mKey = new HotspotNetworkEntryKey(hotspotNetworkData); 133 } 134 135 /** 136 * Create a HotspotNetworkEntry from HotspotNetworkEntryKey. 137 */ HotspotNetworkEntry( @onNull WifiTrackerInjector injector, @NonNull Context context, @NonNull Handler callbackHandler, @NonNull WifiManager wifiManager, @Nullable SharedConnectivityManager sharedConnectivityManager, @NonNull HotspotNetworkEntryKey key)138 HotspotNetworkEntry( 139 @NonNull WifiTrackerInjector injector, 140 @NonNull Context context, @NonNull Handler callbackHandler, 141 @NonNull WifiManager wifiManager, 142 @Nullable SharedConnectivityManager sharedConnectivityManager, 143 @NonNull HotspotNetworkEntryKey key) { 144 super(injector, callbackHandler, wifiManager, false /*forSavedNetworksPage*/); 145 mInjector = injector; 146 mContext = context; 147 mSharedConnectivityManager = sharedConnectivityManager; 148 mHotspotNetworkData = null; 149 mKey = key; 150 } 151 152 @Override getKey()153 public String getKey() { 154 return mKey.toString(); 155 } 156 getHotspotNetworkEntryKey()157 public HotspotNetworkEntryKey getHotspotNetworkEntryKey() { 158 return mKey; 159 } 160 161 /** 162 * Updates the hotspot data for this entry. Creates a new key when called. 163 * 164 * @param hotspotNetworkData An updated data set from SharedConnectivityService. 165 */ 166 @WorkerThread updateHotspotNetworkData( @onNull HotspotNetwork hotspotNetworkData)167 protected synchronized void updateHotspotNetworkData( 168 @NonNull HotspotNetwork hotspotNetworkData) { 169 mHotspotNetworkData = hotspotNetworkData; 170 mKey = new HotspotNetworkEntryKey(hotspotNetworkData); 171 notifyOnUpdated(); 172 } 173 174 @WorkerThread connectionInfoMatches(@onNull WifiInfo wifiInfo)175 protected synchronized boolean connectionInfoMatches(@NonNull WifiInfo wifiInfo) { 176 if (mKey.isVirtualEntry()) { 177 return false; 178 } 179 return Objects.equals(mKey.getScanResultKey(), 180 new StandardWifiEntry.ScanResultKey(WifiInfo.sanitizeSsid(wifiInfo.getSSID()), 181 Collections.singletonList(wifiInfo.getCurrentSecurityType()))); 182 } 183 184 @Override getLevel()185 public int getLevel() { 186 if (getConnectedState() == CONNECTED_STATE_DISCONNECTED) { 187 return WIFI_LEVEL_MAX; 188 } 189 return super.getLevel(); 190 } 191 192 @Override getTitle()193 public synchronized String getTitle() { 194 if (mHotspotNetworkData == null) { 195 return ""; 196 } 197 return mHotspotNetworkData.getNetworkProviderInfo().getDeviceName(); 198 } 199 200 @Override getSummary(boolean concise)201 public synchronized String getSummary(boolean concise) { 202 if (mHotspotNetworkData == null) { 203 return ""; 204 } 205 if (mCalledConnect) { 206 return mContext.getString(R.string.wifitrackerlib_hotspot_network_connecting); 207 } 208 return mContext.getString(R.string.wifitrackerlib_hotspot_network_summary, 209 BidiFormatter.getInstance().unicodeWrap(mHotspotNetworkData.getNetworkName()), 210 BidiFormatter.getInstance().unicodeWrap( 211 mHotspotNetworkData.getNetworkProviderInfo().getModelName())); 212 } 213 214 /** 215 * Alternate summary string to be used on Network & internet page. 216 * 217 * @return Display string. 218 */ getAlternateSummary()219 public synchronized String getAlternateSummary() { 220 if (mHotspotNetworkData == null) { 221 return ""; 222 } 223 return mContext.getString(R.string.wifitrackerlib_hotspot_network_alternate, 224 BidiFormatter.getInstance().unicodeWrap(mHotspotNetworkData.getNetworkName()), 225 BidiFormatter.getInstance().unicodeWrap( 226 mHotspotNetworkData.getNetworkProviderInfo().getDeviceName())); 227 } 228 229 @Override getSsid()230 public synchronized String getSsid() { 231 StandardWifiEntry.ScanResultKey scanResultKey = mKey.getScanResultKey(); 232 if (scanResultKey == null) { 233 return null; 234 } 235 return scanResultKey.getSsid(); 236 } 237 238 @Override 239 @Nullable getMacAddress()240 public synchronized String getMacAddress() { 241 if (mWifiInfo == null) { 242 return null; 243 } 244 final String wifiInfoMac = mWifiInfo.getMacAddress(); 245 if (!TextUtils.isEmpty(wifiInfoMac) 246 && !TextUtils.equals(wifiInfoMac, DEFAULT_MAC_ADDRESS)) { 247 return wifiInfoMac; 248 } 249 if (getPrivacy() != PRIVACY_RANDOMIZED_MAC) { 250 final String[] factoryMacs = mWifiManager.getFactoryMacAddresses(); 251 if (factoryMacs.length > 0) { 252 return factoryMacs[0]; 253 } 254 } 255 return null; 256 } 257 258 @Override 259 @Privacy getPrivacy()260 public int getPrivacy() { 261 return PRIVACY_RANDOMIZED_MAC; 262 } 263 264 @Override getSecurityString(boolean concise)265 public synchronized String getSecurityString(boolean concise) { 266 if (mHotspotNetworkData == null) { 267 return ""; 268 } 269 return Utils.getSecurityString(mContext, 270 new ArrayList<>(mHotspotNetworkData.getHotspotSecurityTypes()), concise); 271 } 272 273 @Override getStandardString()274 public synchronized String getStandardString() { 275 if (mWifiInfo == null) { 276 return ""; 277 } 278 return Utils.getStandardString(mContext, mWifiInfo.getWifiStandard()); 279 } 280 281 @Override getBandString()282 public synchronized String getBandString() { 283 if (mWifiInfo == null) { 284 return ""; 285 } 286 return Utils.wifiInfoToBandString(mContext, mWifiInfo); 287 } 288 289 /** 290 * Connection strength between the host device and the internet. 291 * 292 * @return Displayed connection strength in the range 0 to 4. 293 */ 294 @IntRange(from = 0, to = 4) getUpstreamConnectionStrength()295 public synchronized int getUpstreamConnectionStrength() { 296 if (mHotspotNetworkData == null) { 297 return 0; 298 } 299 return mHotspotNetworkData.getNetworkProviderInfo().getConnectionStrength(); 300 } 301 302 /** 303 * Network type used by the host device to connect to the internet. 304 * 305 * @return NetworkType enum. 306 */ 307 @NetworkType getNetworkType()308 public synchronized int getNetworkType() { 309 if (mHotspotNetworkData == null) { 310 return HotspotNetwork.NETWORK_TYPE_UNKNOWN; 311 } 312 return mHotspotNetworkData.getHostNetworkType(); 313 } 314 315 /** 316 * Device type of the host device. 317 * 318 * @return DeviceType enum. 319 */ 320 @DeviceType getDeviceType()321 public synchronized int getDeviceType() { 322 if (mHotspotNetworkData == null) { 323 return NetworkProviderInfo.DEVICE_TYPE_UNKNOWN; 324 } 325 return mHotspotNetworkData.getNetworkProviderInfo().getDeviceType(); 326 } 327 328 /** 329 * The battery percentage of the host device. 330 */ 331 @IntRange(from = 0, to = 100) getBatteryPercentage()332 public synchronized int getBatteryPercentage() { 333 if (mHotspotNetworkData == null) { 334 return 0; 335 } 336 return mHotspotNetworkData.getNetworkProviderInfo().getBatteryPercentage(); 337 } 338 339 /** 340 * If the host device is currently charging its battery. 341 */ isBatteryCharging()342 public synchronized boolean isBatteryCharging() { 343 if (mHotspotNetworkData == null) { 344 return false; 345 } 346 return mHotspotNetworkData.getExtras().getBoolean("is_battery_charging", false); 347 } 348 349 @Override canConnect()350 public synchronized boolean canConnect() { 351 return getConnectedState() == CONNECTED_STATE_DISCONNECTED; 352 } 353 354 @Override connect(@ullable ConnectCallback callback)355 public synchronized void connect(@Nullable ConnectCallback callback) { 356 mConnectCallback = callback; 357 if (mSharedConnectivityManager == null) { 358 if (callback != null) { 359 mCallbackHandler.post(() -> callback.onConnectResult( 360 ConnectCallback.CONNECT_STATUS_FAILURE_UNKNOWN)); 361 } 362 return; 363 } 364 mSharedConnectivityManager.connectHotspotNetwork(mHotspotNetworkData); 365 } 366 367 @Override canDisconnect()368 public synchronized boolean canDisconnect() { 369 return getConnectedState() != CONNECTED_STATE_DISCONNECTED; 370 } 371 372 @Override disconnect(@ullable DisconnectCallback callback)373 public synchronized void disconnect(@Nullable DisconnectCallback callback) { 374 mDisconnectCallback = callback; 375 if (mSharedConnectivityManager == null) { 376 if (callback != null) { 377 mCallbackHandler.post(() -> callback.onDisconnectResult( 378 DisconnectCallback.DISCONNECT_STATUS_FAILURE_UNKNOWN)); 379 } 380 return; 381 } 382 mSharedConnectivityManager.disconnectHotspotNetwork(mHotspotNetworkData); 383 } 384 385 @Nullable getHotspotNetworkData()386 public HotspotNetwork getHotspotNetworkData() { 387 return mHotspotNetworkData; 388 } 389 390 /** 391 * Trigger ConnectCallback with data from SharedConnectivityService. 392 * @param status HotspotNetworkConnectionStatus#ConnectionStatus enum. 393 */ onConnectionStatusChanged(@onnectionStatus int status)394 public void onConnectionStatusChanged(@ConnectionStatus int status) { 395 if (mConnectCallback == null) return; 396 switch (status) { 397 case HotspotNetworkConnectionStatus.CONNECTION_STATUS_ENABLING_HOTSPOT: 398 mCalledConnect = true; 399 notifyOnUpdated(); 400 break; 401 case HotspotNetworkConnectionStatus.CONNECTION_STATUS_UNKNOWN_ERROR: 402 case HotspotNetworkConnectionStatus.CONNECTION_STATUS_PROVISIONING_FAILED: 403 case HotspotNetworkConnectionStatus.CONNECTION_STATUS_TETHERING_TIMEOUT: 404 case HotspotNetworkConnectionStatus.CONNECTION_STATUS_TETHERING_UNSUPPORTED: 405 case HotspotNetworkConnectionStatus.CONNECTION_STATUS_NO_CELL_DATA: 406 case HotspotNetworkConnectionStatus.CONNECTION_STATUS_ENABLING_HOTSPOT_FAILED: 407 case HotspotNetworkConnectionStatus.CONNECTION_STATUS_ENABLING_HOTSPOT_TIMEOUT: 408 case HotspotNetworkConnectionStatus.CONNECTION_STATUS_CONNECT_TO_HOTSPOT_FAILED: 409 mCallbackHandler.post(() -> mConnectCallback.onConnectResult( 410 ConnectCallback.CONNECT_STATUS_FAILURE_UNKNOWN)); 411 mCalledConnect = false; 412 notifyOnUpdated(); 413 break; 414 default: 415 // Do nothing 416 } 417 } 418 419 static class HotspotNetworkEntryKey { 420 private static final String KEY_IS_VIRTUAL_ENTRY_KEY = "IS_VIRTUAL_ENTRY_KEY"; 421 private static final String KEY_DEVICE_ID_KEY = "DEVICE_ID_KEY"; 422 private static final String KEY_SCAN_RESULT_KEY = "SCAN_RESULT_KEY"; 423 424 private boolean mIsVirtualEntry; 425 private long mDeviceId; 426 @Nullable 427 private StandardWifiEntry.ScanResultKey mScanResultKey; 428 429 /** 430 * Creates a HotspotNetworkEntryKey based on a {@link HotspotNetwork} parcelable object. 431 * 432 * @param hotspotNetworkData A {@link HotspotNetwork} object from SharedConnectivityService. 433 */ HotspotNetworkEntryKey(@onNull HotspotNetwork hotspotNetworkData)434 HotspotNetworkEntryKey(@NonNull HotspotNetwork hotspotNetworkData) { 435 mDeviceId = hotspotNetworkData.getDeviceId(); 436 if (hotspotNetworkData.getHotspotSsid() == null || ( 437 hotspotNetworkData.getHotspotSecurityTypes() == null)) { 438 mIsVirtualEntry = true; 439 mScanResultKey = null; 440 } else { 441 mIsVirtualEntry = false; 442 mScanResultKey = new StandardWifiEntry.ScanResultKey( 443 hotspotNetworkData.getHotspotSsid(), 444 new ArrayList<>(hotspotNetworkData.getHotspotSecurityTypes())); 445 } 446 } 447 448 /** 449 * Creates a HotspotNetworkEntryKey from its String representation. 450 */ HotspotNetworkEntryKey(@onNull String string)451 HotspotNetworkEntryKey(@NonNull String string) { 452 mScanResultKey = null; 453 if (!string.startsWith(KEY_PREFIX)) { 454 Log.e(TAG, "String key does not start with key prefix!"); 455 return; 456 } 457 try { 458 final JSONObject keyJson = new JSONObject( 459 string.substring(KEY_PREFIX.length())); 460 if (keyJson.has(KEY_IS_VIRTUAL_ENTRY_KEY)) { 461 mIsVirtualEntry = keyJson.getBoolean(KEY_IS_VIRTUAL_ENTRY_KEY); 462 } 463 if (keyJson.has(KEY_DEVICE_ID_KEY)) { 464 mDeviceId = keyJson.getLong(KEY_DEVICE_ID_KEY); 465 } 466 if (keyJson.has(KEY_SCAN_RESULT_KEY)) { 467 mScanResultKey = new StandardWifiEntry.ScanResultKey(keyJson.getString( 468 KEY_SCAN_RESULT_KEY)); 469 } 470 } catch (JSONException e) { 471 Log.e(TAG, "JSONException while converting HotspotNetworkEntryKey to string: " + e); 472 } 473 } 474 475 /** 476 * Returns the JSON String representation of this HotspotNetworkEntryKey. 477 */ 478 @Override toString()479 public String toString() { 480 final JSONObject keyJson = new JSONObject(); 481 try { 482 keyJson.put(KEY_IS_VIRTUAL_ENTRY_KEY, mIsVirtualEntry); 483 keyJson.put(KEY_DEVICE_ID_KEY, mDeviceId); 484 if (mScanResultKey != null) { 485 keyJson.put(KEY_SCAN_RESULT_KEY, mScanResultKey.toString()); 486 } 487 } catch (JSONException e) { 488 Log.wtf(TAG, 489 "JSONException while converting HotspotNetworkEntryKey to string: " + e); 490 } 491 return KEY_PREFIX + keyJson.toString(); 492 } 493 isVirtualEntry()494 public boolean isVirtualEntry() { 495 return mIsVirtualEntry; 496 } 497 getDeviceId()498 public long getDeviceId() { 499 return mDeviceId; 500 } 501 502 /** 503 * Returns the ScanResultKey of this HotspotNetworkEntryKey to match against ScanResults 504 */ 505 @Nullable getScanResultKey()506 StandardWifiEntry.ScanResultKey getScanResultKey() { 507 return mScanResultKey; 508 } 509 } 510 } 511