Files
Gearslip/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/WirelessButton.kt
T

158 lines
7.3 KiB
Kotlin

package app.seb3thehacker.gearslip.ui
import androidx.compose.ui.res.stringResource
import app.seb3thehacker.gearslip.R
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.bluetooth.BluetoothDevice
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import app.seb3thehacker.gearslip.wireless.WirelessLink
/** What the wireless start-up needs: Bluetooth to reach the car, nearby Wi-Fi to join its network, local network to talk to it. */
private val WIRELESS_PERMISSIONS = buildList {
add(Manifest.permission.BLUETOOTH_CONNECT)
if (Build.VERSION.SDK_INT >= 33) add(Manifest.permission.NEARBY_WIFI_DEVICES)
// Android 17 silently drops traffic to local addresses, the car included, without it.
if (Build.VERSION.SDK_INT >= 37) add("android.permission.ACCESS_LOCAL_NETWORK")
add(Manifest.permission.ACCESS_FINE_LOCATION)
}.toTypedArray()
/**
* Wireless Android Auto, in Home's status card: pick the paired car, and Gearslip connects to
* it over Bluetooth and Wi-Fi.
*/
@Composable
internal fun WirelessButton() {
val context = LocalContext.current
var devices by remember { mutableStateOf<List<BluetoothDevice>?>(null) }
var denied by remember { mutableStateOf(false) }
fun granted() = WIRELESS_PERMISSIONS.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
val ask = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
if (granted()) devices = WirelessLink.pairedDevices(context) else denied = true
}
if (denied) {
Text(
stringResource(R.string.wireless_needs_permission),
style = MaterialTheme.typography.bodyMedium,
)
// Once Android stops asking, the only way back is the app's own permission settings.
TextButton(
onClick = {
runCatching {
context.startActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", context.packageName, null)),
)
}
},
modifier = Modifier.padding(bottom = 4.dp),
) { Text(stringResource(R.string.wireless_enable_permission)) }
}
Button(
onClick = {
denied = false
if (granted()) devices = WirelessLink.pairedDevices(context) else ask.launch(WIRELESS_PERMISSIONS)
},
modifier = Modifier.fillMaxWidth().height(64.dp),
) {
Box(Modifier.size(28.dp)) { WifiIcon(MaterialTheme.colorScheme.onPrimary) }
Spacer(Modifier.width(12.dp))
Text(stringResource(R.string.wireless_connect), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
}
devices?.let { list ->
AlertDialog(
onDismissRequest = { devices = null },
title = { Text(stringResource(R.string.wireless_which_car)) },
text = {
Column(Modifier.verticalScroll(rememberScrollState())) {
if (list.isEmpty()) {
Text(stringResource(R.string.wireless_none_paired))
}
list.forEach { device ->
Text(
WirelessLink.name(device),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.fillMaxWidth()
.clickable {
devices = null
WirelessLink.start(context, device)
// Next time this car connects, Gearslip does this by itself.
app.seb3thehacker.gearslip.wireless.WirelessAuto.connectedByHand(context, device)
}
.padding(vertical = 14.dp),
)
// A car the driver once said no to never asks again, so offer the way back here.
if (app.seb3thehacker.gearslip.wireless.WirelessAuto.declinedEarlier(context, device)) {
Text(
stringResource(R.string.wireless_auto_off),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
TextButton(
onClick = {
devices = null
app.seb3thehacker.gearslip.wireless.WirelessAuto.askAgain(context, device)
},
modifier = Modifier.padding(bottom = 4.dp),
) { Text(stringResource(R.string.wireless_auto_turn_on)) }
}
}
// The car has to be paired before it shows up here, and pairing happens in Android's settings.
if (list.isNotEmpty()) {
Text(
stringResource(R.string.wireless_not_listed),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp),
)
}
}
},
dismissButton = {
TextButton(onClick = {
devices = null
runCatching {
context.startActivity(Intent(Settings.ACTION_BLUETOOTH_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}) { Text(stringResource(R.string.bluetooth_settings)) }
},
confirmButton = { TextButton(onClick = { devices = null }) { Text(stringResource(R.string.cancel)) } },
)
}
}