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.UNKNOWN_SSID
20 import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo
21 import android.telephony.SubscriptionManager
22 import androidx.annotation.VisibleForTesting
23 import com.android.systemui.log.table.Diffable
24 import com.android.systemui.log.table.TableRowLogger
25 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
26 import com.android.wifitrackerlib.HotspotNetworkEntry.DeviceType
27 
28 /** Provides information about the current wifi network. */
29 sealed class WifiNetworkModel : Diffable<WifiNetworkModel> {
30 
31     // TODO(b/238425913): Have a better, more unified strategy for diff-logging instead of
32     //   copy-pasting the column names for each sub-object.
33 
34     /**
35      * A model representing that we couldn't fetch any wifi information.
36      *
37      * This is only used with [DisabledWifiRepository], where [WifiManager] is null.
38      */
39     object Unavailable : WifiNetworkModel() {
toStringnull40         override fun toString() = "WifiNetwork.Unavailable"
41         override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
42             if (prevVal is Unavailable) {
43                 return
44             }
45 
46             logFull(row)
47         }
48 
logFullnull49         override fun logFull(row: TableRowLogger) {
50             row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE)
51             row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
52             row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
53             row.logChange(COL_VALIDATED, false)
54             row.logChange(COL_LEVEL, LEVEL_DEFAULT)
55             row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
56             row.logChange(COL_SSID, null)
57             row.logChange(COL_HOTSPOT, null)
58             row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
59             row.logChange(COL_ONLINE_SIGN_UP, false)
60             row.logChange(COL_PASSPOINT_NAME, null)
61         }
62     }
63 
64     /** A model representing that the wifi information we received was invalid in some way. */
65     data class Invalid(
66         /** A description of why the wifi information was invalid. */
67         val invalidReason: String,
68     ) : WifiNetworkModel() {
toStringnull69         override fun toString() = "WifiNetwork.Invalid[$invalidReason]"
70         override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
71             if (prevVal !is Invalid) {
72                 logFull(row)
73                 return
74             }
75 
76             if (invalidReason != prevVal.invalidReason) {
77                 row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
78             }
79         }
80 
logFullnull81         override fun logFull(row: TableRowLogger) {
82             row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
83             row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
84             row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
85             row.logChange(COL_VALIDATED, false)
86             row.logChange(COL_LEVEL, LEVEL_DEFAULT)
87             row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
88             row.logChange(COL_SSID, null)
89             row.logChange(COL_HOTSPOT, null)
90             row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
91             row.logChange(COL_ONLINE_SIGN_UP, false)
92             row.logChange(COL_PASSPOINT_NAME, null)
93         }
94     }
95 
96     /** A model representing that we have no active wifi network. */
97     object Inactive : WifiNetworkModel() {
toStringnull98         override fun toString() = "WifiNetwork.Inactive"
99 
100         override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
101             if (prevVal is Inactive) {
102                 return
103             }
104 
105             // When changing to Inactive, we need to log diffs to all the fields.
106             logFull(row)
107         }
108 
logFullnull109         override fun logFull(row: TableRowLogger) {
110             row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
111             row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
112             row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
113             row.logChange(COL_VALIDATED, false)
114             row.logChange(COL_LEVEL, LEVEL_DEFAULT)
115             row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
116             row.logChange(COL_SSID, null)
117             row.logChange(COL_HOTSPOT, null)
118             row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
119             row.logChange(COL_ONLINE_SIGN_UP, false)
120             row.logChange(COL_PASSPOINT_NAME, null)
121         }
122     }
123 
124     /**
125      * A model representing that our wifi network is actually a carrier merged network, meaning it's
126      * treated as more of a mobile network.
127      *
128      * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
129      */
130     data class CarrierMerged(
131         /**
132          * The [android.net.Network.netId] we received from
133          * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
134          *
135          * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId].
136          */
137         val networkId: Int,
138 
139         /**
140          * The subscription ID that this connection represents.
141          *
142          * Comes from [android.net.wifi.WifiInfo.getSubscriptionId].
143          *
144          * Per that method, this value must not be [INVALID_SUBSCRIPTION_ID] (if it was invalid,
145          * then this is *not* a carrier merged network).
146          */
147         val subscriptionId: Int,
148 
149         /** The signal level, guaranteed to be 0 <= level <= numberOfLevels. */
150         val level: Int,
151 
152         /** The maximum possible level. */
153         val numberOfLevels: Int = MobileConnectionRepository.DEFAULT_NUM_LEVELS,
154     ) : WifiNetworkModel() {
155         init {
<lambda>null156             require(level in MIN_VALID_LEVEL..numberOfLevels) {
157                 "0 <= wifi level <= $numberOfLevels required; level was $level"
158             }
<lambda>null159             require(subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
160                 "subscription ID cannot be invalid"
161             }
162         }
163 
logDiffsnull164         override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
165             if (prevVal !is CarrierMerged) {
166                 logFull(row)
167                 return
168             }
169 
170             if (prevVal.networkId != networkId) {
171                 row.logChange(COL_NETWORK_ID, networkId)
172             }
173             if (prevVal.subscriptionId != subscriptionId) {
174                 row.logChange(COL_SUB_ID, subscriptionId)
175             }
176             if (prevVal.level != level) {
177                 row.logChange(COL_LEVEL, level)
178             }
179             if (prevVal.numberOfLevels != numberOfLevels) {
180                 row.logChange(COL_NUM_LEVELS, numberOfLevels)
181             }
182         }
183 
logFullnull184         override fun logFull(row: TableRowLogger) {
185             row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
186             row.logChange(COL_NETWORK_ID, networkId)
187             row.logChange(COL_SUB_ID, subscriptionId)
188             row.logChange(COL_VALIDATED, true)
189             row.logChange(COL_LEVEL, level)
190             row.logChange(COL_NUM_LEVELS, numberOfLevels)
191             row.logChange(COL_SSID, null)
192             row.logChange(COL_HOTSPOT, null)
193             row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
194             row.logChange(COL_ONLINE_SIGN_UP, false)
195             row.logChange(COL_PASSPOINT_NAME, null)
196         }
197     }
198 
199     /** Provides information about an active wifi network. */
200     data class Active(
201         /**
202          * The [android.net.Network.netId] we received from
203          * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
204          *
205          * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId].
206          */
207         val networkId: Int,
208 
209         /** See [android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED]. */
210         val isValidated: Boolean = false,
211 
212         /** The wifi signal level, guaranteed to be 0 <= level <= 4. */
213         val level: Int,
214 
215         /** See [android.net.wifi.WifiInfo.ssid]. */
216         val ssid: String? = null,
217 
218         /**
219          * The type of device providing a hotspot connection, or [HotspotDeviceType.NONE] if this
220          * isn't a hotspot connection.
221          */
222         val hotspotDeviceType: HotspotDeviceType = WifiNetworkModel.HotspotDeviceType.NONE,
223 
224         /** See [android.net.wifi.WifiInfo.isPasspointAp]. */
225         val isPasspointAccessPoint: Boolean = false,
226 
227         /** See [android.net.wifi.WifiInfo.isOsuAp]. */
228         val isOnlineSignUpForPasspointAccessPoint: Boolean = false,
229 
230         /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */
231         val passpointProviderFriendlyName: String? = null,
232     ) : WifiNetworkModel() {
233         init {
<lambda>null234             require(level in MIN_VALID_LEVEL..MAX_VALID_LEVEL) {
235                 "0 <= wifi level <= 4 required; level was $level"
236             }
237         }
238 
239         /** Returns true if this network has a valid SSID and false otherwise. */
hasValidSsidnull240         fun hasValidSsid(): Boolean {
241             return ssid != null && ssid != UNKNOWN_SSID
242         }
243 
logDiffsnull244         override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
245             if (prevVal !is Active) {
246                 logFull(row)
247                 return
248             }
249 
250             if (prevVal.networkId != networkId) {
251                 row.logChange(COL_NETWORK_ID, networkId)
252             }
253             if (prevVal.isValidated != isValidated) {
254                 row.logChange(COL_VALIDATED, isValidated)
255             }
256             if (prevVal.level != level) {
257                 row.logChange(COL_LEVEL, level)
258             }
259             if (prevVal.ssid != ssid) {
260                 row.logChange(COL_SSID, ssid)
261             }
262             if (prevVal.hotspotDeviceType != hotspotDeviceType) {
263                 row.logChange(COL_HOTSPOT, hotspotDeviceType.name)
264             }
265 
266             // TODO(b/238425913): The passpoint-related values are frequently never used, so it
267             //   would be great to not log them when they're not used.
268             if (prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
269                 row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
270             }
271             if (
272                 prevVal.isOnlineSignUpForPasspointAccessPoint !=
273                     isOnlineSignUpForPasspointAccessPoint
274             ) {
275                 row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
276             }
277             if (prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
278                 row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
279             }
280         }
281 
logFullnull282         override fun logFull(row: TableRowLogger) {
283             row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
284             row.logChange(COL_NETWORK_ID, networkId)
285             row.logChange(COL_SUB_ID, null)
286             row.logChange(COL_VALIDATED, isValidated)
287             row.logChange(COL_LEVEL, level)
288             row.logChange(COL_NUM_LEVELS, null)
289             row.logChange(COL_SSID, ssid)
290             row.logChange(COL_HOTSPOT, hotspotDeviceType.name)
291             row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
292             row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
293             row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
294         }
295 
toStringnull296         override fun toString(): String {
297             // Only include the passpoint-related values in the string if we have them. (Most
298             // networks won't have them so they'll be mostly clutter.)
299             val passpointString =
300                 if (
301                     isPasspointAccessPoint ||
302                         isOnlineSignUpForPasspointAccessPoint ||
303                         passpointProviderFriendlyName != null
304                 ) {
305                     ", isPasspointAp=$isPasspointAccessPoint, " +
306                         "isOnlineSignUpForPasspointAp=$isOnlineSignUpForPasspointAccessPoint, " +
307                         "passpointName=$passpointProviderFriendlyName"
308                 } else {
309                     ""
310                 }
311 
312             return "WifiNetworkModel.Active(networkId=$networkId, isValidated=$isValidated, " +
313                 "level=$level, ssid=$ssid$passpointString)"
314         }
315 
316         companion object {
317             // TODO(b/292534484): Use [com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_MAX] instead
318             // once the migration to WifiTrackerLib is complete.
319             @VisibleForTesting internal const val MAX_VALID_LEVEL = 4
320         }
321     }
322 
323     companion object {
324         // TODO(b/292534484): Use [com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_MIN] instead
325         // once the migration to WifiTrackerLib is complete.
326         @VisibleForTesting internal const val MIN_VALID_LEVEL = 0
327     }
328 
329     /**
330      * Enum for the type of device providing the hotspot connection, or [NONE] if this connection
331      * isn't a hotspot.
332      */
333     enum class HotspotDeviceType {
334         /* This wifi connection isn't a hotspot. */
335         NONE,
336         /** The device type for this hotspot is unknown. */
337         UNKNOWN,
338         PHONE,
339         TABLET,
340         LAPTOP,
341         WATCH,
342         AUTO,
343         /** The device type sent for this hotspot is invalid to SysUI. */
344         INVALID,
345     }
346 
347     /**
348      * Converts a device type from [com.android.wifitrackerlib.HotspotNetworkEntry.deviceType] to
349      * our internal representation.
350      */
toHotspotDeviceTypenull351     fun @receiver:DeviceType Int.toHotspotDeviceType(): HotspotDeviceType {
352         return when (this) {
353             NetworkProviderInfo.DEVICE_TYPE_UNKNOWN -> HotspotDeviceType.UNKNOWN
354             NetworkProviderInfo.DEVICE_TYPE_PHONE -> HotspotDeviceType.PHONE
355             NetworkProviderInfo.DEVICE_TYPE_TABLET -> HotspotDeviceType.TABLET
356             NetworkProviderInfo.DEVICE_TYPE_LAPTOP -> HotspotDeviceType.LAPTOP
357             NetworkProviderInfo.DEVICE_TYPE_WATCH -> HotspotDeviceType.WATCH
358             NetworkProviderInfo.DEVICE_TYPE_AUTO -> HotspotDeviceType.AUTO
359             else -> HotspotDeviceType.INVALID
360         }
361     }
362 }
363 
364 const val TYPE_CARRIER_MERGED = "CarrierMerged"
365 const val TYPE_UNAVAILABLE = "Unavailable"
366 const val TYPE_INACTIVE = "Inactive"
367 const val TYPE_ACTIVE = "Active"
368 
369 const val COL_NETWORK_TYPE = "type"
370 const val COL_NETWORK_ID = "networkId"
371 const val COL_SUB_ID = "subscriptionId"
372 const val COL_VALIDATED = "isValidated"
373 const val COL_LEVEL = "level"
374 const val COL_NUM_LEVELS = "maxLevel"
375 const val COL_SSID = "ssid"
376 const val COL_HOTSPOT = "hotspot"
377 const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint"
378 const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint"
379 const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName"
380 
381 val LEVEL_DEFAULT: String? = null
382 val NUM_LEVELS_DEFAULT: String? = null
383 val NETWORK_ID_DEFAULT: String? = null
384 val SUB_ID_DEFAULT: String? = null
385