mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(android): align lint gates and photo permission handling
This commit is contained in:
@@ -66,6 +66,7 @@ android {
|
||||
lint {
|
||||
disable +=
|
||||
setOf(
|
||||
"AndroidGradlePluginVersion",
|
||||
"GradleDependency",
|
||||
"IconLauncherShape",
|
||||
"NewerVersionAvailable",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
@@ -359,6 +359,7 @@ class CameraCaptureManager(private val context: Context) {
|
||||
.build()
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun cameraDeviceInfoOrNull(info: CameraInfo): CameraDeviceInfo? {
|
||||
val cameraId = cameraIdOrNull(info) ?: return null
|
||||
val lensFacing =
|
||||
@@ -389,6 +390,7 @@ class CameraCaptureManager(private val context: Context) {
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun cameraIdOrNull(info: CameraInfo): String? =
|
||||
runCatching { Camera2CameraInfo.from(info).cameraId }.getOrNull()
|
||||
}
|
||||
|
||||
@@ -136,12 +136,7 @@ class DeviceHandler(
|
||||
} else {
|
||||
hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
}
|
||||
val motionGranted =
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
hasPermission(Manifest.permission.ACTIVITY_RECOGNITION)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
val motionGranted = hasPermission(Manifest.permission.ACTIVITY_RECOGNITION)
|
||||
val notificationsGranted =
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
hasPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
@@ -228,7 +223,7 @@ class DeviceHandler(
|
||||
"motion",
|
||||
permissionStateJson(
|
||||
granted = motionGranted,
|
||||
promptableWhenDenied = Build.VERSION.SDK_INT >= 29,
|
||||
promptableWhenDenied = true,
|
||||
),
|
||||
)
|
||||
// Screen capture on Android is interactive per-capture consent, not a sticky app permission.
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.app.RemoteInput
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.notification.NotificationListenerService
|
||||
import android.service.notification.StatusBarNotification
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
@@ -234,9 +233,6 @@ class DeviceNotificationListenerService : NotificationListenerService() {
|
||||
}
|
||||
|
||||
fun requestServiceRebind(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
NotificationListenerService.requestRebind(serviceComponent(context))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import androidx.core.content.ContextCompat
|
||||
import ai.openclaw.android.gateway.GatewaySession
|
||||
@@ -85,7 +84,6 @@ private object SystemMotionDataSource : MotionDataSource {
|
||||
}
|
||||
|
||||
override fun hasPermission(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT < 29) return true
|
||||
return ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) ==
|
||||
android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.scale
|
||||
import ai.openclaw.android.gateway.GatewaySession
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.time.Instant
|
||||
@@ -158,7 +159,7 @@ private object SystemPhotosDataSource : PhotosDataSource {
|
||||
|
||||
if (decoded.width <= maxWidth) return decoded
|
||||
val targetHeight = max(1, ((decoded.height.toDouble() * maxWidth) / decoded.width).roundToInt())
|
||||
return Bitmap.createScaledBitmap(decoded, maxWidth, targetHeight, true)
|
||||
return decoded.scale(maxWidth, targetHeight, true)
|
||||
}
|
||||
|
||||
private fun computeInSampleSize(width: Int, maxWidth: Int): Int {
|
||||
@@ -198,7 +199,7 @@ private object SystemPhotosDataSource : PhotosDataSource {
|
||||
val nextWidth = max(240, (working.width * 0.75f).roundToInt())
|
||||
if (nextWidth >= working.width) return null
|
||||
val nextHeight = max(1, ((working.height.toDouble() * nextWidth) / working.width).roundToInt())
|
||||
working = Bitmap.createScaledBitmap(working, nextWidth, nextHeight, true)
|
||||
working = working.scale(nextWidth, nextHeight, true)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -67,9 +67,6 @@ private class AndroidSystemNotificationPoster(
|
||||
}
|
||||
|
||||
private fun ensureChannel(priority: String?): String {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return NOTIFICATION_CHANNEL_BASE_ID
|
||||
}
|
||||
val normalizedPriority = priority.orEmpty().trim().lowercase()
|
||||
val (suffix, importance, name) =
|
||||
when (normalizedPriority) {
|
||||
|
||||
@@ -80,6 +80,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
@@ -242,7 +243,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
remember(context) {
|
||||
hasMotionCapabilities(context)
|
||||
}
|
||||
val motionPermissionRequired = Build.VERSION.SDK_INT >= 29
|
||||
val motionPermissionRequired = true
|
||||
val notificationsPermissionRequired = Build.VERSION.SDK_INT >= 33
|
||||
val discoveryPermission =
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
@@ -1635,7 +1636,6 @@ private fun isNotificationListenerEnabled(context: Context): Boolean {
|
||||
}
|
||||
|
||||
private fun canInstallUnknownApps(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT < 26) return true
|
||||
return context.packageManager.canRequestPackageInstalls()
|
||||
}
|
||||
|
||||
@@ -1649,11 +1649,10 @@ private fun openNotificationListenerSettings(context: Context) {
|
||||
}
|
||||
|
||||
private fun openUnknownAppSourcesSettings(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < 26) return
|
||||
val intent =
|
||||
Intent(
|
||||
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||
Uri.parse("package:${context.packageName}"),
|
||||
"package:${context.packageName}".toUri(),
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
runCatching {
|
||||
context.startActivity(intent)
|
||||
|
||||
@@ -62,6 +62,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
@@ -171,7 +172,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
} else {
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
}
|
||||
val motionPermissionRequired = Build.VERSION.SDK_INT >= 29
|
||||
val motionPermissionRequired = true
|
||||
val motionAvailable = remember(context) { hasMotionCapabilities(context) }
|
||||
|
||||
var notificationsPermissionGranted by
|
||||
@@ -424,7 +425,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Microphone permission", style = mobileHeadline) },
|
||||
supportingContent = {
|
||||
@@ -477,7 +478,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Allow Camera", style = mobileHeadline) },
|
||||
supportingContent = { Text("Allows the gateway to request photos or short video clips (foreground only).", style = mobileCallout) },
|
||||
@@ -510,7 +511,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
else -> "Grant"
|
||||
}
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("SMS Permission", style = mobileHeadline) },
|
||||
supportingContent = {
|
||||
@@ -561,7 +562,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
"Grant"
|
||||
}
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("System Notifications", style = mobileHeadline) },
|
||||
supportingContent = {
|
||||
@@ -589,7 +590,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Notification Listener Access", style = mobileHeadline) },
|
||||
supportingContent = {
|
||||
@@ -624,7 +625,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Photos Permission", style = mobileHeadline) },
|
||||
supportingContent = {
|
||||
@@ -655,7 +656,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Contacts Permission", style = mobileHeadline) },
|
||||
supportingContent = {
|
||||
@@ -686,7 +687,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Calendar Permission", style = mobileHeadline) },
|
||||
supportingContent = {
|
||||
@@ -724,7 +725,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
else -> "Grant"
|
||||
}
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Motion Permission", style = mobileHeadline) },
|
||||
supportingContent = {
|
||||
@@ -768,7 +769,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Install App Updates", style = mobileHeadline) },
|
||||
supportingContent = {
|
||||
@@ -802,7 +803,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
)
|
||||
}
|
||||
item {
|
||||
Column(modifier = settingsRowModifier(), verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||
Column(modifier = Modifier.settingsRowModifier(), verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||
ListItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = listItemColors,
|
||||
@@ -877,7 +878,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Prevent Sleep", style = mobileHeadline) },
|
||||
supportingContent = { Text("Keeps the screen awake while OpenClaw is open.", style = mobileCallout) },
|
||||
@@ -897,7 +898,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
modifier = settingsRowModifier(),
|
||||
modifier = Modifier.settingsRowModifier(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Debug Canvas Status", style = mobileHeadline) },
|
||||
supportingContent = { Text("Show status text in the canvas when debug is enabled.", style = mobileCallout) },
|
||||
@@ -927,8 +928,8 @@ private fun settingsTextFieldColors() =
|
||||
cursorColor = mobileAccent,
|
||||
)
|
||||
|
||||
private fun settingsRowModifier() =
|
||||
Modifier
|
||||
private fun Modifier.settingsRowModifier() =
|
||||
this
|
||||
.fillMaxWidth()
|
||||
.border(width = 1.dp, color = mobileBorder, shape = RoundedCornerShape(14.dp))
|
||||
.background(Color.White, RoundedCornerShape(14.dp))
|
||||
@@ -970,14 +971,10 @@ private fun openNotificationListenerSettings(context: Context) {
|
||||
}
|
||||
|
||||
private fun openUnknownAppSourcesSettings(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
openAppSettings(context)
|
||||
return
|
||||
}
|
||||
val intent =
|
||||
Intent(
|
||||
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||
Uri.parse("package:${context.packageName}"),
|
||||
"package:${context.packageName}".toUri(),
|
||||
)
|
||||
runCatching {
|
||||
context.startActivity(intent)
|
||||
@@ -997,7 +994,6 @@ private fun isNotificationListenerEnabled(context: Context): Boolean {
|
||||
}
|
||||
|
||||
private fun canInstallUnknownApps(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT < 26) return true
|
||||
return context.packageManager.canRequestPackageInstalls()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.core.content.ContextCompat
|
||||
import ai.openclaw.android.gateway.GatewaySession
|
||||
import ai.openclaw.android.isCanonicalMainSessionKey
|
||||
import ai.openclaw.android.normalizeMainKey
|
||||
import android.os.Build
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
@@ -1316,43 +1315,28 @@ private const val defaultTalkProvider = "elevenlabs"
|
||||
|
||||
private fun requestAudioFocusForTts(): Boolean {
|
||||
val am = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: return true
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val req = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
|
||||
.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build()
|
||||
)
|
||||
.setOnAudioFocusChangeListener(audioFocusListener)
|
||||
.build()
|
||||
audioFocusRequest = req
|
||||
val result = am.requestAudioFocus(req)
|
||||
Log.d(tag, "audio focus request result=$result")
|
||||
result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED || result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
val result = am.requestAudioFocus(
|
||||
audioFocusListener,
|
||||
AudioManager.STREAM_MUSIC,
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
|
||||
val req = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
|
||||
.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build()
|
||||
)
|
||||
result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
||||
}
|
||||
.setOnAudioFocusChangeListener(audioFocusListener)
|
||||
.build()
|
||||
audioFocusRequest = req
|
||||
val result = am.requestAudioFocus(req)
|
||||
Log.d(tag, "audio focus request result=$result")
|
||||
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED || result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED
|
||||
}
|
||||
|
||||
private fun abandonAudioFocus() {
|
||||
val am = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
audioFocusRequest?.let {
|
||||
am.abandonAudioFocusRequest(it)
|
||||
Log.d(tag, "audio focus abandoned")
|
||||
}
|
||||
audioFocusRequest = null
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
am.abandonAudioFocus(audioFocusListener)
|
||||
audioFocusRequest?.let {
|
||||
am.abandonAudioFocusRequest(it)
|
||||
Log.d(tag, "audio focus abandoned")
|
||||
}
|
||||
audioFocusRequest = null
|
||||
}
|
||||
|
||||
private fun cleanupPlayer() {
|
||||
|
||||
Reference in New Issue
Block a user