feat: onboarding refresh (#2551)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich 2025-07-29 09:42:36 -05:00 committed by GitHub
parent c1408816a4
commit 2c6751a574
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 2795 additions and 2513 deletions

View file

@ -18,161 +18,58 @@
package com.geeksville.mesh.android
import android.Manifest
import android.app.Activity
import android.app.NotificationManager
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.pm.PackageManager
import android.location.LocationManager
import androidx.core.app.ActivityCompat
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.geeksville.mesh.R
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
* @return null on platforms without a BlueTooth driver (i.e. the emulator)
*/
val Context.bluetoothManager: BluetoothManager?
get() = getSystemService(Context.BLUETOOTH_SERVICE).takeIf { hasBluetoothPermission() } as? BluetoothManager?
/** Checks if the device has a GPS receiver. */
fun Context.hasGps(): Boolean {
val lm = getSystemService(Context.LOCATION_SERVICE) as? LocationManager
return lm?.allProviders?.contains(LocationManager.GPS_PROVIDER) == true
}
val Context.notificationManager: NotificationManager get() = requireNotNull(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager?)
val Context.locationManager: LocationManager get() = requireNotNull(getSystemService(Context.LOCATION_SERVICE) as? LocationManager?)
/**
* @return true if the device has a GPS receiver
*/
fun Context.hasGps(): Boolean = locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)
/**
* @return true if the device has a GPS receiver and it is disabled (location turned off)
*/
fun Context.gpsDisabled(): Boolean =
if (hasGps()) !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) else false
/**
* @return the text string of the permissions missing
*/
val Context.permissionMissing: String
get() = if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S) {
getString(R.string.permission_missing)
/** Checks if the device has a GPS receiver and it is currently disabled. */
fun Context.gpsDisabled(): Boolean {
val lm = getSystemService(Context.LOCATION_SERVICE) as? LocationManager ?: return false
return if (lm.allProviders.contains(LocationManager.GPS_PROVIDER)) {
!lm.isProviderEnabled(LocationManager.GPS_PROVIDER)
} else {
getString(R.string.permission_missing_31)
false
}
}
/**
* Checks if any given permissions need to show rationale.
* Determines the list of Bluetooth permissions that are currently missing. Internal helper for
* [hasBluetoothPermission].
*
* @return true if should show UI with rationale before requesting a permission.
*/
fun Activity.shouldShowRequestPermissionRationale(permissions: Array<String>): Boolean {
for (permission in permissions) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
return true
}
}
return false
}
/**
* Checks if any given permissions need to show rationale.
* For Android S (API 31) and above, this includes [Manifest.permission.BLUETOOTH_SCAN] and
* [Manifest.permission.BLUETOOTH_CONNECT]. For older versions, it includes [Manifest.permission.ACCESS_FINE_LOCATION]
* as it is required for Bluetooth scanning.
*
* @return true if should show UI with rationale before requesting a permission.
* @return Array of missing Bluetooth permission strings. Empty if all are granted.
*/
fun Fragment.shouldShowRequestPermissionRationale(permissions: Array<String>): Boolean {
for (permission in permissions) {
if (shouldShowRequestPermissionRationale(permission)) {
return true
}
}
return false
}
private fun Context.getBluetoothPermissions(): Array<String> {
val requiredPermissions = mutableListOf<String>()
/**
* Handles whether a rationale dialog should be shown before performing an action.
*/
fun Context.rationaleDialog(
shouldShowRequestPermissionRationale: Boolean = true,
title: Int = R.string.required_permissions,
rationale: CharSequence = permissionMissing,
invokeFun: () -> Unit,
) {
if (!shouldShowRequestPermissionRationale) invokeFun()
else MaterialAlertDialogBuilder(this)
.setTitle(title)
.setMessage(rationale)
.setNeutralButton(R.string.cancel) { _, _ ->
}
.setPositiveButton(R.string.accept) { _, _ ->
invokeFun()
}
.show()
}
/**
* return a list of the permissions we don't have
*/
fun Context.getMissingPermissions(perms: List<String>): Array<String> = perms.filter {
ContextCompat.checkSelfPermission(
this,
it
) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()
/**
* Bluetooth permissions (or empty if we already have what we need)
*/
fun Context.getBluetoothPermissions(): Array<String> {
val perms = mutableListOf<String>()
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
perms.add(Manifest.permission.BLUETOOTH_SCAN)
perms.add(Manifest.permission.BLUETOOTH_CONNECT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requiredPermissions.add(Manifest.permission.BLUETOOTH_SCAN)
requiredPermissions.add(Manifest.permission.BLUETOOTH_CONNECT)
} else {
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
// ACCESS_FINE_LOCATION is required for Bluetooth scanning on pre-S devices.
requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
}
return getMissingPermissions(perms)
return requiredPermissions
.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
.toTypedArray()
}
/** @return true if the user already has Bluetooth connect permission */
fun Context.hasBluetoothPermission() = getBluetoothPermissions().isEmpty()
/** Checks if all necessary Bluetooth permissions have been granted. */
fun Context.hasBluetoothPermission(): Boolean = getBluetoothPermissions().isEmpty()
/**
* Camera permission (or empty if we already have what we need)
*/
fun Context.getCameraPermissions(): Array<String> {
val perms = mutableListOf(Manifest.permission.CAMERA)
return getMissingPermissions(perms)
/** @return true if the user already has location permission (ACCESS_FINE_LOCATION). */
fun Context.hasLocationPermission(): Boolean {
val perms = listOf(Manifest.permission.ACCESS_FINE_LOCATION)
return perms.all { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED }
}
/** @return true if the user already has camera permission */
fun Context.hasCameraPermission() = getCameraPermissions().isEmpty()
/**
* Location permission (or empty if we already have what we need)
*/
fun Context.getLocationPermissions(): Array<String> {
val perms = mutableListOf(Manifest.permission.ACCESS_FINE_LOCATION)
return getMissingPermissions(perms)
}
/** @return true if the user already has location permission */
fun Context.hasLocationPermission() = getLocationPermissions().isEmpty()
/**
* Notification permission (or empty if we already have what we need)
*/
fun Context.getNotificationPermissions(): Array<String> {
val perms = mutableListOf<String>()
if (android.os.Build.VERSION.SDK_INT >= 33) {
perms.add(Manifest.permission.POST_NOTIFICATIONS)
}
return getMissingPermissions(perms)
}
/** @return true if the user already has notification permission */
fun Context.hasNotificationPermission() = getNotificationPermissions().isEmpty()