Implement complete Android settings and recovery controls (#104)
Android ARM verification / rust (push) Failing after 46m46s
Android ARM verification / android (push) Failing after 7s
Dependency security audit / rustsec (push) Successful in 22s

This commit is contained in:
hugo committed 2026-10-10 12:07:28 +02:00
1 parent 06575363a2
commit 43f660ace3
34 files changed
+2720 -64

No files matched your search

@@ -0,0 +1,138 @@
package de.rfc1437.ironstorage.platform
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.credentials.CredentialManager
import android.os.Build
import android.provider.Settings
import android.view.autofill.AutofillManager
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import de.rfc1437.ironstorage.core.ProviderStatus
import de.rfc1437.ironstorage.core.SettingsPlatform
class AndroidSettingsPlatform(
private val context: Context,
private val keystore: AndroidKeystoreAdapter
) : SettingsPlatform {
private var picker: ActivityResultLauncher<String>? = null
private var host: java.lang.ref.WeakReference<FragmentActivity> = java.lang.ref.WeakReference(
null
)
fun attach(activity: FragmentActivity, selected: (List<String>) -> Unit) {
host = java.lang.ref.WeakReference(activity)
picker =
activity.activityResultRegistry.register(
"ironstorage.settings.qr-images",
activity,
ActivityResultContracts.GetMultipleContents()
) { uris ->
if (uris.isNotEmpty()) selected(uris.map { it.toString() })
}
}
fun detach(activity: FragmentActivity) {
if (host.get() === activity) {
picker = null
host.clear()
}
}
override fun chooseQrImages(): Boolean {
val picker = picker ?: return false
return try {
picker.launch("image/*")
true
} catch (
_: android.content.ActivityNotFoundException
) {
false
}
}
@Suppress("DEPRECATION")
private fun services(permission: String): List<ComponentName> =
context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SERVICES)
.services.orEmpty().filter { it.permission == permission }
.map { ComponentName(context.packageName, it.name) }
override fun autofill(): ProviderStatus {
val manager = context.getSystemService(AutofillManager::class.java)
return ProviderStatus(
manager?.isAutofillSupported == true,
services("android.permission.BIND_AUTOFILL_SERVICE").isNotEmpty(),
manager?.hasEnabledAutofillServices() == true
)
}
override fun credentials(): ProviderStatus {
val services = services("android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE")
val enabled = if (Build.VERSION.SDK_INT >= 34) {
val manager = context.getSystemService(CredentialManager::class.java)
services.any { manager?.isEnabledCredentialProviderService(it) == true }
} else {
false
}
return ProviderStatus(Build.VERSION.SDK_INT >= 34, services.isNotEmpty(), enabled)
}
override fun biometricsAvailable() = keystore.deviceState().strongBiometric
@Suppress("DEPRECATION")
override fun version(): String = context.packageManager.getPackageInfo(context.packageName, 0)
.versionName.orEmpty()
private fun open(intent: Intent): Boolean = try {
context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
true
} catch (
_: android.content.ActivityNotFoundException
) {
false
} catch (_: SecurityException) {
false
}
override fun openAutofill(): Boolean {
if (!autofill().supported) return false
return open(
Intent(
Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE,
"package:${context.packageName}".toUri()
)
)
}
override fun openCredentials(): Boolean {
if (Build.VERSION.SDK_INT < 34) return false
val intent = if (Build.VERSION.SDK_INT >= 35) {
Intent(Settings.ACTION_CREDENTIAL_PROVIDER, "package:${context.packageName}".toUri())
} else {
Intent(
Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE,
"package:${context.packageName}".toUri()
)
}
return open(intent)
}
override suspend fun readQrImage(uri: String): ByteArray {
val selected = uri.toUri()
if (selected.scheme != "content") throw java.io.IOException("Not a content URI")
return context.contentResolver.openInputStream(selected)?.use { input ->
val output = java.io.ByteArrayOutputStream()
val buffer = ByteArray(16384)
while (true) {
val count = input.read(buffer)
if (count < 0) break
if (output.size() + count >
32 * 1024 * 1024
) {
throw java.io.IOException("Image exceeds the supported size")
}
output.write(buffer, 0, count)
}
val bytes = output.toByteArray()
if (bytes.size > 32 * 1024 * 1024) {
bytes.fill(0)
throw java.io.IOException("Image exceeds the supported size")
}
bytes
} ?: throw java.io.IOException("Image unavailable")
}
}