1 /* 2 * Copyright (C) 2022 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.pipeline.wifi.shared.model 18 19 import android.net.wifi.WifiManager 20 import android.net.wifi.WifiManager.UNKNOWN_SSID 21 import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo 22 import android.telephony.SubscriptionManager 23 import androidx.annotation.VisibleForTesting 24 import com.android.systemui.log.table.Diffable 25 import com.android.systemui.log.table.TableRowLogger 26 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository 27 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL 28 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Active.Companion.isValid 29 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Active.Companion.of 30 import com.android.wifitrackerlib.HotspotNetworkEntry.DeviceType 31 import com.android.wifitrackerlib.WifiEntry 32 import com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_UNREACHABLE 33 34 /** Provides information about the current wifi network. */ 35 sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { 36 37 // TODO(b/238425913): Have a better, more unified strategy for diff-logging instead of 38 // copy-pasting the column names for each sub-object. 39 40 /** 41 * A model representing that we couldn't fetch any wifi information. 42 * 43 * This is only used with [DisabledWifiRepository], where [WifiManager] is null. 44 */ 45 object Unavailable : WifiNetworkModel() { toStringnull46 override fun toString() = "WifiNetwork.Unavailable" 47 48 override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { 49 if (prevVal is Unavailable) { 50 return 51 } 52 53 logFull(row) 54 } 55 logFullnull56 override fun logFull(row: TableRowLogger) { 57 row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE) 58 row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) 59 row.logChange(COL_VALIDATED, false) 60 row.logChange(COL_LEVEL, LEVEL_DEFAULT) 61 row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) 62 row.logChange(COL_SSID, null) 63 row.logChange(COL_HOTSPOT, null) 64 } 65 } 66 67 /** A model representing that the wifi information we received was invalid in some way. */ 68 data class Invalid( 69 /** A description of why the wifi information was invalid. */ 70 val invalidReason: String, 71 ) : WifiNetworkModel() { toStringnull72 override fun toString() = "WifiNetwork.Invalid[reason=$invalidReason]" 73 74 override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { 75 if (prevVal !is Invalid) { 76 logFull(row) 77 return 78 } 79 80 if (invalidReason != prevVal.invalidReason) { 81 row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE[reason=$invalidReason]") 82 } 83 } 84 logFullnull85 override fun logFull(row: TableRowLogger) { 86 row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE[reason=$invalidReason]") 87 row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) 88 row.logChange(COL_VALIDATED, false) 89 row.logChange(COL_LEVEL, LEVEL_DEFAULT) 90 row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) 91 row.logChange(COL_SSID, null) 92 row.logChange(COL_HOTSPOT, null) 93 } 94 } 95 96 /** A model representing that we have no active wifi network. */ 97 data class Inactive( 98 /** An optional description of why the wifi information was inactive. */ 99 val inactiveReason: String? = null, 100 ) : WifiNetworkModel() { toStringnull101 override fun toString() = "WifiNetwork.Inactive[reason=$inactiveReason]" 102 103 override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { 104 if (prevVal !is Inactive) { 105 logFull(row) 106 return 107 } 108 109 if (inactiveReason != prevVal.inactiveReason) { 110 row.logChange(COL_NETWORK_TYPE, "$TYPE_INACTIVE[reason=$inactiveReason]") 111 } 112 } 113 logFullnull114 override fun logFull(row: TableRowLogger) { 115 row.logChange(COL_NETWORK_TYPE, "$TYPE_INACTIVE[reason=$inactiveReason]") 116 row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) 117 row.logChange(COL_VALIDATED, false) 118 row.logChange(COL_LEVEL, LEVEL_DEFAULT) 119 row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) 120 row.logChange(COL_SSID, null) 121 row.logChange(COL_HOTSPOT, null) 122 } 123 } 124 125 /** 126 * A model representing that our wifi network is actually a carrier merged network, meaning it's 127 * treated as more of a mobile network. 128 * 129 * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information. 130 * 131 * IMPORTANT: Do *not* call [copy] on this class. Instead, use the factory [of] methods. [of] 132 * will verify preconditions correctly. 133 */ 134 data class CarrierMerged 135 private constructor( 136 /** 137 * The subscription ID that this connection represents. 138 * 139 * Comes from [android.net.wifi.WifiInfo.getSubscriptionId]. 140 * 141 * Per that method, this value must not be [SubscriptionManager.INVALID_SUBSCRIPTION_ID] (if 142 * it was invalid, then this is *not* a carrier merged network). 143 */ 144 val subscriptionId: Int, 145 146 /** The signal level, required to be 0 <= level <= numberOfLevels. */ 147 val level: Int, 148 149 /** The maximum possible level. */ 150 val numberOfLevels: Int, 151 ) : WifiNetworkModel() { 152 companion object { 153 /** 154 * Creates a [CarrierMerged] instance, or an [Invalid] instance if any of the arguments 155 * are invalid. 156 */ ofnull157 fun of( 158 subscriptionId: Int, 159 level: Int, 160 numberOfLevels: Int = MobileConnectionRepository.DEFAULT_NUM_LEVELS 161 ): WifiNetworkModel { 162 if (!subscriptionId.isSubscriptionIdValid()) { 163 return Invalid(INVALID_SUB_ID_ERROR_STRING) 164 } 165 if (!level.isLevelValid(numberOfLevels)) { 166 return Invalid(getInvalidLevelErrorString(level, numberOfLevels)) 167 } 168 return CarrierMerged(subscriptionId, level, numberOfLevels) 169 } 170 isLevelValidnull171 private fun Int.isLevelValid(maxLevel: Int): Boolean { 172 return this != WIFI_LEVEL_UNREACHABLE && this in MIN_VALID_LEVEL..maxLevel 173 } 174 getInvalidLevelErrorStringnull175 private fun getInvalidLevelErrorString(level: Int, maxLevel: Int): String { 176 return "Wifi network was carrier merged but had invalid level. " + 177 "$MIN_VALID_LEVEL <= wifi level <= $maxLevel required; " + 178 "level was $level" 179 } 180 isSubscriptionIdValidnull181 private fun Int.isSubscriptionIdValid(): Boolean { 182 return this != SubscriptionManager.INVALID_SUBSCRIPTION_ID 183 } 184 185 private const val INVALID_SUB_ID_ERROR_STRING = 186 "Wifi network was carrier merged but had invalid sub ID" 187 } 188 189 init { <lambda>null190 require(level.isLevelValid(numberOfLevels)) { 191 "${getInvalidLevelErrorString(level, numberOfLevels)}. $DO_NOT_USE_COPY_ERROR" 192 } <lambda>null193 require(subscriptionId.isSubscriptionIdValid()) { 194 "$INVALID_SUB_ID_ERROR_STRING. $DO_NOT_USE_COPY_ERROR" 195 } 196 } 197 logDiffsnull198 override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { 199 if (prevVal !is CarrierMerged) { 200 logFull(row) 201 return 202 } 203 204 if (prevVal.subscriptionId != subscriptionId) { 205 row.logChange(COL_SUB_ID, subscriptionId) 206 } 207 if (prevVal.level != level) { 208 row.logChange(COL_LEVEL, level) 209 } 210 if (prevVal.numberOfLevels != numberOfLevels) { 211 row.logChange(COL_NUM_LEVELS, numberOfLevels) 212 } 213 } 214 logFullnull215 override fun logFull(row: TableRowLogger) { 216 row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED) 217 row.logChange(COL_SUB_ID, subscriptionId) 218 row.logChange(COL_VALIDATED, true) 219 row.logChange(COL_LEVEL, level) 220 row.logChange(COL_NUM_LEVELS, numberOfLevels) 221 row.logChange(COL_SSID, null) 222 row.logChange(COL_HOTSPOT, null) 223 } 224 } 225 226 /** 227 * Provides information about an active wifi network. 228 * 229 * IMPORTANT: Do *not* call [copy] on this class. Instead, use the factory [of] method. [of] 230 * will verify preconditions correctly. 231 */ 232 data class Active 233 private constructor( 234 /** See [android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED]. */ 235 val isValidated: Boolean, 236 237 /** The wifi signal level, required to be 0 <= level <= 4. */ 238 val level: Int, 239 240 /** See [android.net.wifi.WifiInfo.ssid]. */ 241 val ssid: String?, 242 243 /** 244 * The type of device providing a hotspot connection, or [HotspotDeviceType.NONE] if this 245 * isn't a hotspot connection. 246 */ 247 val hotspotDeviceType: HotspotDeviceType, 248 ) : WifiNetworkModel() { 249 companion object { 250 /** 251 * Creates an [Active] instance, or an [Inactive] instance if any of the arguments are 252 * invalid. 253 */ 254 @JvmStatic ofnull255 fun of( 256 isValidated: Boolean = false, 257 level: Int, 258 ssid: String? = null, 259 hotspotDeviceType: HotspotDeviceType = HotspotDeviceType.NONE, 260 ): WifiNetworkModel { 261 if (!level.isValid()) { 262 return Inactive(getInvalidLevelErrorString(level)) 263 } 264 return Active(isValidated, level, ssid, hotspotDeviceType) 265 } 266 isValidnull267 private fun Int.isValid(): Boolean { 268 return this != WIFI_LEVEL_UNREACHABLE && this in MIN_VALID_LEVEL..MAX_VALID_LEVEL 269 } 270 getInvalidLevelErrorStringnull271 private fun getInvalidLevelErrorString(level: Int): String { 272 return "Wifi network was active but had invalid level. " + 273 "$MIN_VALID_LEVEL <= wifi level <= $MAX_VALID_LEVEL required; " + 274 "level was $level" 275 } 276 277 @VisibleForTesting internal const val MAX_VALID_LEVEL = WifiEntry.WIFI_LEVEL_MAX 278 } 279 280 init { <lambda>null281 require(level.isValid()) { 282 "${getInvalidLevelErrorString(level)}. $DO_NOT_USE_COPY_ERROR" 283 } 284 } 285 286 /** Returns true if this network has a valid SSID and false otherwise. */ hasValidSsidnull287 fun hasValidSsid(): Boolean { 288 return ssid != null && ssid != UNKNOWN_SSID 289 } 290 logDiffsnull291 override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { 292 if (prevVal !is Active) { 293 logFull(row) 294 return 295 } 296 297 if (prevVal.isValidated != isValidated) { 298 row.logChange(COL_VALIDATED, isValidated) 299 } 300 if (prevVal.level != level) { 301 row.logChange(COL_LEVEL, level) 302 } 303 if (prevVal.ssid != ssid) { 304 row.logChange(COL_SSID, ssid) 305 } 306 if (prevVal.hotspotDeviceType != hotspotDeviceType) { 307 row.logChange(COL_HOTSPOT, hotspotDeviceType.name) 308 } 309 } 310 logFullnull311 override fun logFull(row: TableRowLogger) { 312 row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE) 313 row.logChange(COL_SUB_ID, null) 314 row.logChange(COL_VALIDATED, isValidated) 315 row.logChange(COL_LEVEL, level) 316 row.logChange(COL_NUM_LEVELS, null) 317 row.logChange(COL_SSID, ssid) 318 row.logChange(COL_HOTSPOT, hotspotDeviceType.name) 319 } 320 } 321 322 companion object { 323 @VisibleForTesting internal const val MIN_VALID_LEVEL = WifiEntry.WIFI_LEVEL_MIN 324 } 325 326 /** 327 * Enum for the type of device providing the hotspot connection, or [NONE] if this connection 328 * isn't a hotspot. 329 */ 330 enum class HotspotDeviceType { 331 /* This wifi connection isn't a hotspot. */ 332 NONE, 333 /** The device type for this hotspot is unknown. */ 334 UNKNOWN, 335 PHONE, 336 TABLET, 337 LAPTOP, 338 WATCH, 339 AUTO, 340 /** The device type sent for this hotspot is invalid to SysUI. */ 341 INVALID, 342 } 343 344 /** 345 * Converts a device type from [com.android.wifitrackerlib.HotspotNetworkEntry.deviceType] to 346 * our internal representation. 347 */ toHotspotDeviceTypenull348 fun @receiver:DeviceType Int.toHotspotDeviceType(): HotspotDeviceType { 349 return when (this) { 350 NetworkProviderInfo.DEVICE_TYPE_UNKNOWN -> HotspotDeviceType.UNKNOWN 351 NetworkProviderInfo.DEVICE_TYPE_PHONE -> HotspotDeviceType.PHONE 352 NetworkProviderInfo.DEVICE_TYPE_TABLET -> HotspotDeviceType.TABLET 353 NetworkProviderInfo.DEVICE_TYPE_LAPTOP -> HotspotDeviceType.LAPTOP 354 NetworkProviderInfo.DEVICE_TYPE_WATCH -> HotspotDeviceType.WATCH 355 NetworkProviderInfo.DEVICE_TYPE_AUTO -> HotspotDeviceType.AUTO 356 else -> HotspotDeviceType.INVALID 357 } 358 } 359 } 360 361 const val TYPE_CARRIER_MERGED = "CarrierMerged" 362 const val TYPE_UNAVAILABLE = "Unavailable" 363 const val TYPE_INACTIVE = "Inactive" 364 const val TYPE_ACTIVE = "Active" 365 366 const val COL_NETWORK_TYPE = "type" 367 const val COL_SUB_ID = "subscriptionId" 368 const val COL_VALIDATED = "isValidated" 369 const val COL_LEVEL = "level" 370 const val COL_NUM_LEVELS = "maxLevel" 371 const val COL_SSID = "ssid" 372 const val COL_HOTSPOT = "hotspot" 373 374 val LEVEL_DEFAULT: String? = null 375 val NUM_LEVELS_DEFAULT: String? = null 376 val SUB_ID_DEFAULT: String? = null 377 378 private const val DO_NOT_USE_COPY_ERROR = 379 "This should only be an issue if the caller incorrectly used `copy` to get a new instance. " + 380 "Please use the `of` method instead." 381