feat(android): add HomeScreen, SettingsScreen, LogsScreen with bottom nav
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
100
android/app/src/main/java/com/thisux/droidclaw/MainActivity.kt
Normal file
100
android/app/src/main/java/com/thisux/droidclaw/MainActivity.kt
Normal file
@@ -0,0 +1,100 @@
|
||||
package com.thisux.droidclaw
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.thisux.droidclaw.ui.screens.HomeScreen
|
||||
import com.thisux.droidclaw.ui.screens.LogsScreen
|
||||
import com.thisux.droidclaw.ui.screens.SettingsScreen
|
||||
import com.thisux.droidclaw.ui.theme.DroidClawTheme
|
||||
|
||||
sealed class Screen(val route: String, val label: String) {
|
||||
data object Home : Screen("home", "Home")
|
||||
data object Settings : Screen("settings", "Settings")
|
||||
data object Logs : Screen("logs", "Logs")
|
||||
}
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
DroidClawTheme {
|
||||
MainNavigation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainNavigation() {
|
||||
val navController = rememberNavController()
|
||||
val screens = listOf(Screen.Home, Screen.Settings, Screen.Logs)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
|
||||
screens.forEach { screen ->
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
when (screen) {
|
||||
is Screen.Home -> Icons.Filled.Home
|
||||
is Screen.Settings -> Icons.Filled.Settings
|
||||
is Screen.Logs -> Icons.Filled.History
|
||||
},
|
||||
contentDescription = screen.label
|
||||
)
|
||||
},
|
||||
label = { Text(screen.label) },
|
||||
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
|
||||
onClick = {
|
||||
navController.navigate(screen.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Home.route,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
composable(Screen.Home.route) { HomeScreen() }
|
||||
composable(Screen.Settings.route) { SettingsScreen() }
|
||||
composable(Screen.Logs.route) { LogsScreen() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.thisux.droidclaw.ui.screens
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.thisux.droidclaw.connection.ConnectionService
|
||||
import com.thisux.droidclaw.model.ConnectionState
|
||||
import com.thisux.droidclaw.model.GoalStatus
|
||||
|
||||
@Composable
|
||||
fun HomeScreen() {
|
||||
val context = LocalContext.current
|
||||
val connectionState by ConnectionService.connectionState.collectAsState()
|
||||
val goalStatus by ConnectionService.currentGoalStatus.collectAsState()
|
||||
val steps by ConnectionService.currentSteps.collectAsState()
|
||||
val currentGoal by ConnectionService.currentGoal.collectAsState()
|
||||
val errorMessage by ConnectionService.errorMessage.collectAsState()
|
||||
|
||||
var goalInput by remember { mutableStateOf("") }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Status Badge
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
when (connectionState) {
|
||||
ConnectionState.Connected -> Color(0xFF4CAF50)
|
||||
ConnectionState.Connecting -> Color(0xFFFFC107)
|
||||
ConnectionState.Error -> Color(0xFFF44336)
|
||||
ConnectionState.Disconnected -> Color.Gray
|
||||
}
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = when (connectionState) {
|
||||
ConnectionState.Connected -> "Connected to server"
|
||||
ConnectionState.Connecting -> "Connecting..."
|
||||
ConnectionState.Error -> errorMessage ?: "Connection error"
|
||||
ConnectionState.Disconnected -> "Disconnected"
|
||||
},
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Connect/Disconnect button
|
||||
Button(
|
||||
onClick = {
|
||||
val intent = Intent(context, ConnectionService::class.java).apply {
|
||||
action = if (connectionState == ConnectionState.Disconnected || connectionState == ConnectionState.Error) {
|
||||
ConnectionService.ACTION_CONNECT
|
||||
} else {
|
||||
ConnectionService.ACTION_DISCONNECT
|
||||
}
|
||||
}
|
||||
context.startForegroundService(intent)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
when (connectionState) {
|
||||
ConnectionState.Disconnected, ConnectionState.Error -> "Connect"
|
||||
else -> "Disconnect"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Goal Input
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = goalInput,
|
||||
onValueChange = { goalInput = it },
|
||||
label = { Text("Enter a goal...") },
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = connectionState == ConnectionState.Connected && goalStatus != GoalStatus.Running,
|
||||
singleLine = true
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
if (goalInput.isNotBlank()) {
|
||||
val intent = Intent(context, ConnectionService::class.java).apply {
|
||||
action = ConnectionService.ACTION_SEND_GOAL
|
||||
putExtra(ConnectionService.EXTRA_GOAL, goalInput)
|
||||
}
|
||||
context.startService(intent)
|
||||
goalInput = ""
|
||||
}
|
||||
},
|
||||
enabled = connectionState == ConnectionState.Connected
|
||||
&& goalStatus != GoalStatus.Running
|
||||
&& goalInput.isNotBlank()
|
||||
) {
|
||||
Text("Run")
|
||||
}
|
||||
}
|
||||
|
||||
if (currentGoal.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Goal: $currentGoal",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Step Log
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(steps) { step ->
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = "Step ${step.step}: ${step.action}",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
if (step.reasoning.isNotEmpty()) {
|
||||
Text(
|
||||
text = step.reasoning,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Goal Status
|
||||
if (goalStatus == GoalStatus.Completed || goalStatus == GoalStatus.Failed) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = if (goalStatus == GoalStatus.Completed) {
|
||||
"Goal completed (${steps.size} steps)"
|
||||
} else {
|
||||
"Goal failed"
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (goalStatus == GoalStatus.Completed) {
|
||||
Color(0xFF4CAF50)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.error
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.thisux.droidclaw.ui.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.thisux.droidclaw.connection.ConnectionService
|
||||
import com.thisux.droidclaw.model.GoalStatus
|
||||
|
||||
@Composable
|
||||
fun LogsScreen() {
|
||||
val steps by ConnectionService.currentSteps.collectAsState()
|
||||
val goalStatus by ConnectionService.currentGoalStatus.collectAsState()
|
||||
val currentGoal by ConnectionService.currentGoal.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text("Logs", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
if (currentGoal.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = currentGoal,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Text(
|
||||
text = when (goalStatus) {
|
||||
GoalStatus.Running -> "Running"
|
||||
GoalStatus.Completed -> "Completed"
|
||||
GoalStatus.Failed -> "Failed"
|
||||
GoalStatus.Idle -> "Idle"
|
||||
},
|
||||
color = when (goalStatus) {
|
||||
GoalStatus.Running -> Color(0xFFFFC107)
|
||||
GoalStatus.Completed -> Color(0xFF4CAF50)
|
||||
GoalStatus.Failed -> MaterialTheme.colorScheme.error
|
||||
GoalStatus.Idle -> Color.Gray
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (steps.isEmpty()) {
|
||||
Text(
|
||||
text = "No steps recorded yet. Submit a goal to see agent activity here.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(steps) { step ->
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expanded = !expanded }
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = "Step ${step.step}: ${step.action}",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
if (expanded && step.reasoning.isNotEmpty()) {
|
||||
Text(
|
||||
text = step.reasoning,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.thisux.droidclaw.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.thisux.droidclaw.DroidClawApp
|
||||
import com.thisux.droidclaw.accessibility.DroidClawAccessibilityService
|
||||
import com.thisux.droidclaw.capture.ScreenCaptureManager
|
||||
import com.thisux.droidclaw.util.BatteryOptimization
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen() {
|
||||
val context = LocalContext.current
|
||||
val app = context.applicationContext as DroidClawApp
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val apiKey by app.settingsStore.apiKey.collectAsState(initial = "")
|
||||
val serverUrl by app.settingsStore.serverUrl.collectAsState(initial = "wss://localhost:8080")
|
||||
|
||||
var editingApiKey by remember(apiKey) { mutableStateOf(apiKey) }
|
||||
var editingServerUrl by remember(serverUrl) { mutableStateOf(serverUrl) }
|
||||
|
||||
val isAccessibilityEnabled by DroidClawAccessibilityService.isRunning.collectAsState()
|
||||
val isCaptureAvailable by ScreenCaptureManager.isAvailable.collectAsState()
|
||||
val isBatteryExempt = remember { BatteryOptimization.isIgnoringBatteryOptimizations(context) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Settings", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
OutlinedTextField(
|
||||
value = editingApiKey,
|
||||
onValueChange = { editingApiKey = it },
|
||||
label = { Text("API Key") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
singleLine = true
|
||||
)
|
||||
if (editingApiKey != apiKey) {
|
||||
OutlinedButton(
|
||||
onClick = { scope.launch { app.settingsStore.setApiKey(editingApiKey) } }
|
||||
) {
|
||||
Text("Save API Key")
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = editingServerUrl,
|
||||
onValueChange = { editingServerUrl = it },
|
||||
label = { Text("Server URL") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
if (editingServerUrl != serverUrl) {
|
||||
OutlinedButton(
|
||||
onClick = { scope.launch { app.settingsStore.setServerUrl(editingServerUrl) } }
|
||||
) {
|
||||
Text("Save Server URL")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text("Setup Checklist", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
ChecklistItem(
|
||||
label = "API key configured",
|
||||
isOk = apiKey.isNotBlank(),
|
||||
actionLabel = null,
|
||||
onAction = {}
|
||||
)
|
||||
|
||||
ChecklistItem(
|
||||
label = "Accessibility service",
|
||||
isOk = isAccessibilityEnabled,
|
||||
actionLabel = "Enable",
|
||||
onAction = { BatteryOptimization.openAccessibilitySettings(context) }
|
||||
)
|
||||
|
||||
ChecklistItem(
|
||||
label = "Screen capture permission",
|
||||
isOk = isCaptureAvailable,
|
||||
actionLabel = null,
|
||||
onAction = {}
|
||||
)
|
||||
|
||||
ChecklistItem(
|
||||
label = "Battery optimization disabled",
|
||||
isOk = isBatteryExempt,
|
||||
actionLabel = "Disable",
|
||||
onAction = { BatteryOptimization.requestExemption(context) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChecklistItem(
|
||||
label: String,
|
||||
isOk: Boolean,
|
||||
actionLabel: String?,
|
||||
onAction: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isOk) {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
|
||||
}
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isOk) Icons.Filled.CheckCircle else Icons.Filled.Error,
|
||||
contentDescription = if (isOk) "OK" else "Missing",
|
||||
tint = if (isOk) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(label)
|
||||
}
|
||||
if (!isOk && actionLabel != null) {
|
||||
OutlinedButton(onClick = onAction) {
|
||||
Text(actionLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.thisux.droidclaw.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
|
||||
object BatteryOptimization {
|
||||
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
|
||||
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return pm.isIgnoringBatteryOptimizations(context.packageName)
|
||||
}
|
||||
|
||||
fun requestExemption(context: Context) {
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun openAccessibilitySettings(context: Context) {
|
||||
context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user