chore(android): update icons and platform config

main
Peter Steinberger 2025-12-20 23:30:35 +01:00
parent 873daf079c
commit c1050da852
13 changed files with 58 additions and 47 deletions

View File

@ -47,6 +47,10 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
} }
lint {
disable += setOf("IconLauncherShape")
}
} }
dependencies { dependencies {
@ -56,7 +60,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.17.0") implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation("androidx.activity:activity-compose:1.12.1") implementation("androidx.activity:activity-compose:1.12.2")
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")

View File

@ -12,12 +12,20 @@
<uses-permission <uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION" android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<application <application
android:name=".NodeApp" android:name=".NodeApp"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name" android:label="@string/app_name"

View File

@ -10,7 +10,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -85,7 +84,6 @@ class NodeForegroundService : Service() {
override fun onBind(intent: Intent?) = null override fun onBind(intent: Intent?) = null
private fun ensureChannel() { private fun ensureChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val mgr = getSystemService(NotificationManager::class.java) val mgr = getSystemService(NotificationManager::class.java)
val channel = val channel =
NotificationChannel( NotificationChannel(
@ -101,12 +99,7 @@ class NodeForegroundService : Service() {
private fun buildNotification(title: String, text: String): Notification { private fun buildNotification(title: String, text: String): Notification {
val stopIntent = Intent(this, NodeForegroundService::class.java).setAction(ACTION_STOP) val stopIntent = Intent(this, NodeForegroundService::class.java).setAction(ACTION_STOP)
val flags = val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val stopPending = PendingIntent.getService(this, 2, stopIntent, flags) val stopPending = PendingIntent.getService(this, 2, stopIntent, flags)
return NotificationCompat.Builder(this, CHANNEL_ID) return NotificationCompat.Builder(this, CHANNEL_ID)
@ -126,11 +119,6 @@ class NodeForegroundService : Service() {
} }
private fun startForegroundWithTypes(notification: Notification, requiresMic: Boolean) { private fun startForegroundWithTypes(notification: Notification, requiresMic: Boolean) {
if (Build.VERSION.SDK_INT < 29) {
startForeground(NOTIFICATION_ID, notification)
return
}
if (didStartForeground && requiresMic == lastRequiresMic) { if (didStartForeground && requiresMic == lastRequiresMic) {
updateNotification(notification) updateNotification(notification)
return return
@ -162,11 +150,7 @@ class NodeForegroundService : Service() {
fun start(context: Context) { fun start(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java) val intent = Intent(context, NodeForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent)
context.startForegroundService(intent)
} else {
context.startService(intent)
}
} }
fun stop(context: Context) { fun stop(context: Context) {

View File

@ -3,6 +3,7 @@
package com.steipete.clawdis.node package com.steipete.clawdis.node
import android.content.Context import android.content.Context
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey import androidx.security.crypto.MasterKey
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -70,39 +71,39 @@ class SecurePrefs(context: Context) {
fun setLastDiscoveredStableId(value: String) { fun setLastDiscoveredStableId(value: String) {
val trimmed = value.trim() val trimmed = value.trim()
prefs.edit().putString("bridge.lastDiscoveredStableId", trimmed).apply() prefs.edit { putString("bridge.lastDiscoveredStableId", trimmed) }
_lastDiscoveredStableId.value = trimmed _lastDiscoveredStableId.value = trimmed
} }
fun setDisplayName(value: String) { fun setDisplayName(value: String) {
val trimmed = value.trim() val trimmed = value.trim()
prefs.edit().putString(displayNameKey, trimmed).apply() prefs.edit { putString(displayNameKey, trimmed) }
_displayName.value = trimmed _displayName.value = trimmed
} }
fun setCameraEnabled(value: Boolean) { fun setCameraEnabled(value: Boolean) {
prefs.edit().putBoolean("camera.enabled", value).apply() prefs.edit { putBoolean("camera.enabled", value) }
_cameraEnabled.value = value _cameraEnabled.value = value
} }
fun setPreventSleep(value: Boolean) { fun setPreventSleep(value: Boolean) {
prefs.edit().putBoolean("screen.preventSleep", value).apply() prefs.edit { putBoolean("screen.preventSleep", value) }
_preventSleep.value = value _preventSleep.value = value
} }
fun setManualEnabled(value: Boolean) { fun setManualEnabled(value: Boolean) {
prefs.edit().putBoolean("bridge.manual.enabled", value).apply() prefs.edit { putBoolean("bridge.manual.enabled", value) }
_manualEnabled.value = value _manualEnabled.value = value
} }
fun setManualHost(value: String) { fun setManualHost(value: String) {
val trimmed = value.trim() val trimmed = value.trim()
prefs.edit().putString("bridge.manual.host", trimmed).apply() prefs.edit { putString("bridge.manual.host", trimmed) }
_manualHost.value = trimmed _manualHost.value = trimmed
} }
fun setManualPort(value: Int) { fun setManualPort(value: Int) {
prefs.edit().putInt("bridge.manual.port", value).apply() prefs.edit { putInt("bridge.manual.port", value) }
_manualPort.value = value _manualPort.value = value
} }
@ -113,14 +114,14 @@ class SecurePrefs(context: Context) {
fun saveBridgeToken(token: String) { fun saveBridgeToken(token: String) {
val key = "bridge.token.${_instanceId.value}" val key = "bridge.token.${_instanceId.value}"
prefs.edit().putString(key, token.trim()).apply() prefs.edit { putString(key, token.trim()) }
} }
private fun loadOrCreateInstanceId(): String { private fun loadOrCreateInstanceId(): String {
val existing = prefs.getString("node.instanceId", null)?.trim() val existing = prefs.getString("node.instanceId", null)?.trim()
if (!existing.isNullOrBlank()) return existing if (!existing.isNullOrBlank()) return existing
val fresh = UUID.randomUUID().toString() val fresh = UUID.randomUUID().toString()
prefs.edit().putString("node.instanceId", fresh).apply() prefs.edit { putString("node.instanceId", fresh) }
return fresh return fresh
} }
@ -131,7 +132,7 @@ class SecurePrefs(context: Context) {
val candidate = DeviceNames.bestDefaultNodeName(context).trim() val candidate = DeviceNames.bestDefaultNodeName(context).trim()
val resolved = candidate.ifEmpty { "Android Node" } val resolved = candidate.ifEmpty { "Android Node" }
prefs.edit().putString(displayNameKey, resolved).apply() prefs.edit { putString(displayNameKey, resolved) }
return resolved return resolved
} }
@ -139,12 +140,12 @@ class SecurePrefs(context: Context) {
val sanitized = WakeWords.sanitize(words, defaultWakeWords) val sanitized = WakeWords.sanitize(words, defaultWakeWords)
val encoded = val encoded =
JsonArray(sanitized.map { JsonPrimitive(it) }).toString() JsonArray(sanitized.map { JsonPrimitive(it) }).toString()
prefs.edit().putString("voiceWake.triggerWords", encoded).apply() prefs.edit { putString("voiceWake.triggerWords", encoded) }
_wakeWords.value = sanitized _wakeWords.value = sanitized
} }
fun setVoiceWakeMode(mode: VoiceWakeMode) { fun setVoiceWakeMode(mode: VoiceWakeMode) {
prefs.edit().putString(voiceWakeModeKey, mode.rawValue).apply() prefs.edit { putString(voiceWakeModeKey, mode.rawValue) }
_voiceWakeMode.value = mode _voiceWakeMode.value = mode
} }
@ -154,7 +155,7 @@ class SecurePrefs(context: Context) {
// Default ON (foreground) when unset. // Default ON (foreground) when unset.
if (raw.isNullOrBlank()) { if (raw.isNullOrBlank()) {
prefs.edit().putString(voiceWakeModeKey, resolved.rawValue).apply() prefs.edit { putString(voiceWakeModeKey, resolved.rawValue) }
} }
return resolved return resolved

View File

@ -6,7 +6,6 @@ import android.net.DnsResolver
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.nsd.NsdManager import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.os.CancellationSignal import android.os.CancellationSignal
import android.util.Log import android.util.Log
import java.io.IOException import java.io.IOException
@ -181,7 +180,6 @@ class BridgeDiscovery(
} }
private fun txt(info: NsdServiceInfo, key: String): String? { private fun txt(info: NsdServiceInfo, key: String): String? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null
val bytes = info.attributes[key] ?: return null val bytes = info.attributes[key] ?: return null
return try { return try {
String(bytes, Charsets.UTF_8).trim().ifEmpty { null } String(bytes, Charsets.UTF_8).trim().ifEmpty { null }
@ -401,7 +399,7 @@ class BridgeDiscovery(
dns.rawQuery( dns.rawQuery(
network, network,
wireQuery, wireQuery,
0, DnsResolver.FLAG_EMPTY,
dnsExecutor, dnsExecutor,
signal, signal,
object : DnsResolver.Callback<ByteArray> { object : DnsResolver.Callback<ByteArray> {

View File

@ -2,6 +2,7 @@ package com.steipete.clawdis.node.node
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.annotation.SuppressLint
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.util.Base64 import android.util.Base64
@ -18,6 +19,7 @@ import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent import androidx.camera.video.VideoRecordEvent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.checkSelfPermission import androidx.core.content.ContextCompat.checkSelfPermission
import androidx.core.graphics.scale
import com.steipete.clawdis.node.PermissionRequester import com.steipete.clawdis.node.PermissionRequester
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@ -92,7 +94,7 @@ class CameraCaptureManager(private val context: Context) {
(decoded.height.toDouble() * (maxWidth.toDouble() / decoded.width.toDouble())) (decoded.height.toDouble() * (maxWidth.toDouble() / decoded.width.toDouble()))
.toInt() .toInt()
.coerceAtLeast(1) .coerceAtLeast(1)
Bitmap.createScaledBitmap(decoded, maxWidth, h, true) decoded.scale(maxWidth, h)
} else { } else {
decoded decoded
} }
@ -108,6 +110,7 @@ class CameraCaptureManager(private val context: Context) {
) )
} }
@SuppressLint("MissingPermission")
suspend fun clip(paramsJson: String?): Payload = suspend fun clip(paramsJson: String?): Payload =
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
ensureCameraPermission() ensureCameraPermission()

View File

@ -1,10 +1,11 @@
package com.steipete.clawdis.node.node package com.steipete.clawdis.node.node
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build
import android.graphics.Canvas import android.graphics.Canvas
import android.os.Looper import android.os.Looper
import android.webkit.WebView import android.webkit.WebView
import androidx.core.graphics.createBitmap
import androidx.core.graphics.scale
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -80,7 +81,7 @@ class CanvasController {
val scaled = val scaled =
if (maxWidth != null && maxWidth > 0 && bmp.width > maxWidth) { if (maxWidth != null && maxWidth > 0 && bmp.width > maxWidth) {
val h = (bmp.height.toDouble() * (maxWidth.toDouble() / bmp.width.toDouble())).toInt().coerceAtLeast(1) val h = (bmp.height.toDouble() * (maxWidth.toDouble() / bmp.width.toDouble())).toInt().coerceAtLeast(1)
Bitmap.createScaledBitmap(bmp, maxWidth, h, true) bmp.scale(maxWidth, h)
} else { } else {
bmp bmp
} }
@ -97,7 +98,7 @@ class CanvasController {
val scaled = val scaled =
if (maxWidth != null && maxWidth > 0 && bmp.width > maxWidth) { if (maxWidth != null && maxWidth > 0 && bmp.width > maxWidth) {
val h = (bmp.height.toDouble() * (maxWidth.toDouble() / bmp.width.toDouble())).toInt().coerceAtLeast(1) val h = (bmp.height.toDouble() * (maxWidth.toDouble() / bmp.width.toDouble())).toInt().coerceAtLeast(1)
Bitmap.createScaledBitmap(bmp, maxWidth, h, true) bmp.scale(maxWidth, h)
} else { } else {
bmp bmp
} }
@ -116,7 +117,7 @@ class CanvasController {
suspendCancellableCoroutine { cont -> suspendCancellableCoroutine { cont ->
val width = width.coerceAtLeast(1) val width = width.coerceAtLeast(1)
val height = height.coerceAtLeast(1) val height = height.coerceAtLeast(1)
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val bitmap = createBitmap(width, height, Bitmap.Config.ARGB_8888)
// WebView isn't supported by PixelCopy.request(...) directly; draw() is the most reliable // WebView isn't supported by PixelCopy.request(...) directly; draw() is the most reliable
// cross-version snapshot for this lightweight "canvas" use-case. // cross-version snapshot for this lightweight "canvas" use-case.

View File

@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View File

@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View File

@ -1,8 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<style name="Theme.ClawdisNode" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Theme.ClawdisNode" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item> <item name="android:windowLightStatusBar">false</item>
</style> </style>
</resources> </resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include domain="file" path="." />
</full-backup-content>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<include domain="file" path="." />
</cloud-backup>
<device-transfer>
<include domain="file" path="." />
</device-transfer>
</data-extraction-rules>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<network-security-config> <network-security-config xmlns:tools="http://schemas.android.com/tools">
<!-- This app is primarily used on a trusted tailnet; allow cleartext for IP-based endpoints too. --> <!-- This app is primarily used on a trusted tailnet; allow cleartext for IP-based endpoints too. -->
<base-config cleartextTrafficPermitted="true" /> <base-config cleartextTrafficPermitted="true" tools:ignore="InsecureBaseConfiguration" />
<!-- Allow HTTP for tailnet/local dev endpoints (e.g. canvas/background web). --> <!-- Allow HTTP for tailnet/local dev endpoints (e.g. canvas/background web). -->
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">clawdis.internal</domain> <domain includeSubdomains="true">clawdis.internal</domain>