Add WiZ provisioning wizard

This commit is contained in:
Artem Kokos
2026-05-16 17:24:28 +07:00
parent 0a635115d4
commit 866a074c03
19 changed files with 2668 additions and 0 deletions

View File

@@ -1,7 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

View File

@@ -1,11 +1,19 @@
package ru.akokos.ignis_app
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import android.location.LocationManager
import android.provider.Settings
import androidx.core.app.ActivityCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.engine.FlutterEngine
@@ -14,9 +22,13 @@ import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private var pendingNotificationPermissionResult: MethodChannel.Result? = null
private var pendingProvisioningPermissionResult: MethodChannel.Result? = null
private val notificationPrefs by lazy {
getSharedPreferences(notificationPrefsName, MODE_PRIVATE)
}
private val provisioningPrefs by lazy {
getSharedPreferences(provisioningPrefsName, MODE_PRIVATE)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
@@ -94,6 +106,34 @@ class MainActivity : FlutterActivity() {
else -> result.notImplemented()
}
}
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
wizProvisioningChannelName,
).setMethodCallHandler { call, result ->
when (call.method) {
"getProvisioningEnvironment" -> {
result.success(buildProvisioningEnvironment())
}
"requestProvisioningPermissions" -> {
requestProvisioningPermissions(result)
}
"openWifiSettings" -> {
startActivity(Intent(Settings.ACTION_WIFI_SETTINGS))
result.success(null)
}
"openAppSettings" -> {
val intent =
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", packageName, null),
)
startActivity(intent)
result.success(null)
}
else -> result.notImplemented()
}
}
}
override fun onRequestPermissionsResult(
@@ -104,6 +144,10 @@ class MainActivity : FlutterActivity() {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode != notificationPermissionRequestCode) {
if (requestCode == provisioningPermissionRequestCode) {
pendingProvisioningPermissionResult?.success(buildProvisioningEnvironment())
pendingProvisioningPermissionResult = null
}
return
}
@@ -111,6 +155,30 @@ class MainActivity : FlutterActivity() {
pendingNotificationPermissionResult = null
}
private fun requestProvisioningPermissions(result: MethodChannel.Result) {
if (pendingProvisioningPermissionResult != null) {
result.error(
"request_in_progress",
"Provisioning permission request is already in progress",
null,
)
return
}
if (hasProvisioningPermissions()) {
result.success(buildProvisioningEnvironment())
return
}
pendingProvisioningPermissionResult = result
provisioningPrefs.edit().putBoolean(provisioningPermissionRequestedKey, true).apply()
ActivityCompat.requestPermissions(
this,
requiredProvisioningPermissions(),
provisioningPermissionRequestCode,
)
}
private fun requestNotificationPermission(result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
result.success(getNotificationPermissionStatusValue())
@@ -182,10 +250,137 @@ class MainActivity : FlutterActivity() {
NotificationManagerCompat.from(this).areNotificationsEnabled()
}
private fun buildProvisioningEnvironment(): Map<String, Any?> {
val wifiSnapshot = currentWifiSnapshot()
return mapOf(
"platform" to "android",
"androidApiLevel" to Build.VERSION.SDK_INT,
"smartPairingSupported" to true,
"wifiSettingsSupported" to true,
"appSettingsSupported" to true,
"permissionStatus" to provisioningPermissionStatusValue(),
"locationServicesEnabled" to isLocationServicesEnabled(),
"connectedToWifi" to wifiSnapshot.connectedToWifi,
"ssid" to wifiSnapshot.ssid,
"bssid" to wifiSnapshot.bssid,
"frequencyMhz" to wifiSnapshot.frequencyMhz,
)
}
private fun provisioningPermissionStatusValue(): String {
if (hasProvisioningPermissions()) {
return "granted"
}
val wasRequestedBefore =
provisioningPrefs.getBoolean(provisioningPermissionRequestedKey, false)
val canShowPromptAgain =
requiredProvisioningPermissions().any {
ActivityCompat.shouldShowRequestPermissionRationale(this, it)
}
return if (!wasRequestedBefore || canShowPromptAgain) {
"requestable"
} else {
"settings_required"
}
}
private fun hasProvisioningPermissions(): Boolean {
return requiredProvisioningPermissions().all { permission ->
ContextCompat.checkSelfPermission(this, permission) ==
PackageManager.PERMISSION_GRANTED
}
}
private fun requiredProvisioningPermissions(): Array<String> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.NEARBY_WIFI_DEVICES,
)
} else {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
private fun isLocationServicesEnabled(): Boolean {
val manager = getSystemService(Context.LOCATION_SERVICE) as? LocationManager
return manager?.let(LocationManagerCompat::isLocationEnabled) ?: false
}
private fun currentWifiSnapshot(): WifiSnapshot {
if (!hasProvisioningPermissions()) {
return WifiSnapshot()
}
val connectivityManager =
getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
val isWifiTransport =
networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
val wifiInfo =
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
networkCapabilities?.transportInfo is WifiInfo -> {
networkCapabilities.transportInfo as WifiInfo
}
else -> {
@Suppress("DEPRECATION")
val wifiManager =
applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
@Suppress("DEPRECATION")
wifiManager.connectionInfo
}
}
val sanitizedSsid = sanitizeSsid(wifiInfo?.ssid)
val sanitizedBssid = sanitizeBssid(wifiInfo?.bssid)
val frequency = wifiInfo?.frequency?.takeIf { it > 0 }
return WifiSnapshot(
connectedToWifi = isWifiTransport && sanitizedSsid != null,
ssid = sanitizedSsid,
bssid = sanitizedBssid,
frequencyMhz = frequency,
)
}
private fun sanitizeSsid(raw: String?): String? {
val trimmed = raw?.trim()?.removePrefix("\"")?.removeSuffix("\"")
return when {
trimmed.isNullOrEmpty() -> null
trimmed.equals(WifiManager.UNKNOWN_SSID, ignoreCase = true) -> null
else -> trimmed
}
}
private fun sanitizeBssid(raw: String?): String? {
val trimmed = raw?.trim()
return when {
trimmed.isNullOrEmpty() -> null
trimmed == "02:00:00:00:00:00" -> null
else -> trimmed.lowercase()
}
}
data class WifiSnapshot(
val connectedToWifi: Boolean = false,
val ssid: String? = null,
val bssid: String? = null,
val frequencyMhz: Int? = null,
)
companion object {
private const val notificationPermissionRequestCode = 4102
private const val notificationPrefsName = "ignis_notification_permissions"
private const val notificationPermissionRequestedKey =
"post_notifications_requested"
private const val provisioningPermissionRequestCode = 4103
private const val provisioningPrefsName = "ignis_wiz_provisioning"
private const val provisioningPermissionRequestedKey =
"wiz_provisioning_permissions_requested"
private const val wizProvisioningChannelName = "ignis/wiz_provisioning"
}
}