Commit 3218aee2 authored by Lisa (AI Assistant)'s avatar Lisa (AI Assistant)

Add core features: screenshot, touch, notifications, settings

- ScreenshotService using MediaProjection API
- TouchService using AccessibilityService for gestures
- NotificationListener for notification mirroring
- SettingsRepository using DataStore
- NodeClient WebSocket implementation
- Domain models for commands/responses
- Hilt DI setup
- Improved UI with permission management
- Gradle wrapper for building
parent 1fee2c32
Pipeline #264 canceled with stages
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
<!-- Wake lock for keeping connection alive --> <!-- Wake lock for keeping connection alive -->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Query all packages for app listing -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application <application
android:name=".OpenClawApp" android:name=".OpenClawApp"
android:allowBackup="true" android:allowBackup="true"
...@@ -51,6 +55,30 @@ ...@@ -51,6 +55,30 @@
android:value="remote_control" /> android:value="remote_control" />
</service> </service>
<!-- Touch Accessibility Service -->
<service
android:name=".service.TouchService"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
<!-- Notification Listener Service -->
<service
android:name=".service.NotificationListener"
android:exported="false"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application> </application>
</manifest> </manifest>
package com.openclaw.node.data.remote
import com.openclaw.node.domain.model.NodeCapabilities
import com.openclaw.node.domain.model.NodeCommand
import com.openclaw.node.domain.model.NodeEvent
import com.openclaw.node.domain.model.NodeResponse
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import okhttp3.*
import org.json.JSONArray
import org.json.JSONObject
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NodeClient @Inject constructor() {
private var webSocket: WebSocket? = null
private val client = OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.pingInterval(20, TimeUnit.SECONDS)
.build()
private val _connectionStatus = Channel<ConnectionStatus>(Channel.BUFFERED)
val connectionStatus: Flow<ConnectionStatus> = _connectionStatus.receiveAsFlow()
private val _incomingCommands = Channel<NodeCommand>(Channel.BUFFERED)
val incomingCommands: Flow<NodeCommand> = _incomingCommands.receiveAsFlow()
private var isConnected = false
private var currentNodeId: String = ""
data class ConnectionStatus(val connected: Boolean, val nodeId: String)
fun connect(gatewayUrl: String, nodeId: String, token: String, capabilities: NodeCapabilities) {
currentNodeId = nodeId
val urlWithParams = "$gatewayUrl?token=$token&node_id=$nodeId"
val request = Request.Builder()
.url(urlWithParams)
.build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
isConnected = true
_connectionStatus.trySend(ConnectionStatus(true, nodeId))
// Send registration
val register = JSONObject().apply {
put("type", "register")
put("node_id", nodeId)
put("capabilities", JSONObject().apply {
put("screenshot", capabilities.screenshot)
put("touch", capabilities.touch)
put("shell", capabilities.shell)
put("notifications", capabilities.notifications)
put("app_control", capabilities.appControl)
put("sensors", capabilities.sensors)
})
}
sendJson(register)
}
override fun onMessage(webSocket: WebSocket, text: String) {
try {
val json = JSONObject(text)
val command = parseCommand(json)
command?.let { _incomingCommands.trySend(it) }
} catch (e: Exception) {
// Handle parsing error
}
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
webSocket.close(1000, null)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
isConnected = false
_connectionStatus.trySend(ConnectionStatus(false, nodeId))
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
isConnected = false
_connectionStatus.trySend(ConnectionStatus(false, nodeId))
}
})
}
fun disconnect() {
webSocket?.close(1000, "User disconnected")
webSocket = null
isConnected = false
}
fun sendResponse(response: NodeResponse) {
val json = when (response) {
is NodeResponse.Success -> JSONObject().apply {
put("type", "response")
put("id", response.id)
put("success", true)
put("data", JSONObject().apply {
response.data.forEach { (key, value) ->
put(key, value)
}
})
}
is NodeResponse.Error -> JSONObject().apply {
put("type", "error")
put("id", response.id)
put("error", response.error)
}
is NodeResponse.ScreenshotResponse -> JSONObject().apply {
put("type", "response")
put("id", response.id)
put("success", true)
put("data", JSONObject().apply {
put("screenshot", response.base64Image)
})
}
}
sendJson(json)
}
fun sendEvent(event: NodeEvent) {
val json = when (event) {
is NodeEvent.Notification -> JSONObject().apply {
put("type", "notification")
put("app", event.app)
put("title", event.title)
put("text", event.text)
put("timestamp", event.timestamp)
}
is NodeEvent.Battery -> JSONObject().apply {
put("type", "battery")
put("level", event.level)
put("charging", event.charging)
}
is NodeEvent.ForegroundAppChanged -> JSONObject().apply {
put("type", "foreground_app")
put("package", event.packageName)
put("app_name", event.appName)
}
is NodeEvent.ConnectionStatus -> JSONObject().apply {
put("type", "connection")
put("connected", event.connected)
put("node_id", event.nodeId)
}
}
sendJson(json)
}
private fun sendJson(json: JSONObject) {
webSocket?.send(json.toString())
}
private fun parseCommand(json: JSONObject): NodeCommand? {
val type = json.getString("type")
val id = json.optString("id", "")
return when (type) {
"screenshot" -> {
val quality = json.optJSONObject("params")?.optInt("quality", 80) ?: 80
NodeCommand.Screenshot(id, quality)
}
"tap" -> {
val params = json.getJSONObject("params")
NodeCommand.Tap(id, params.getInt("x"), params.getInt("y"))
}
"swipe" -> {
val params = json.getJSONObject("params")
NodeCommand.Swipe(
id,
params.getInt("x1"), params.getInt("y1"),
params.getInt("x2"), params.getInt("y2"),
params.optInt("duration", 300)
)
}
"long_press" -> {
val params = json.getJSONObject("params")
NodeCommand.LongPress(
id,
params.getInt("x"), params.getInt("y"),
params.optInt("duration", 500)
)
}
"launch_app" -> {
val params = json.getJSONObject("params")
NodeCommand.LaunchApp(id, params.getString("package"))
}
"shell" -> {
val params = json.getJSONObject("params")
NodeCommand.Shell(id, params.getString("command"))
}
"device_info" -> NodeCommand.DeviceInfo(id)
"list_apps" -> {
val systemApps = json.optJSONObject("params")?.optBoolean("system_apps", false) ?: false
NodeCommand.ListApps(id, systemApps)
}
"foreground_app" -> NodeCommand.GetForegroundApp(id)
"notification_listener" -> {
val enabled = json.optJSONObject("params")?.optBoolean("enabled", true) ?: true
NodeCommand.NotificationListener(id, enabled)
}
"key_event" -> {
val params = json.getJSONObject("params")
NodeCommand.KeyEvent(id, params.getInt("key_code"))
}
"ping" -> NodeCommand.Ping
else -> null
}
}
}
package com.openclaw.node.data.repository
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "openclaw_node_settings")
@Singleton
class SettingsRepository @Inject constructor(
@ApplicationContext private val context: Context
) {
companion object {
private val GATEWAY_URL = stringPreferencesKey("gateway_url")
private val NODE_TOKEN = stringPreferencesKey("node_token")
private val NODE_ID = stringPreferencesKey("node_id")
private val AUTO_CONNECT = booleanPreferencesKey("auto_connect")
private val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
private val SCREENSHOT_QUALITY = stringPreferencesKey("screenshot_quality")
}
val gatewayUrl: Flow<String> = context.dataStore.data.map { preferences ->
preferences[GATEWAY_URL] ?: ""
}
val nodeToken: Flow<String> = context.dataStore.data.map { preferences ->
preferences[NODE_TOKEN] ?: ""
}
val nodeId: Flow<String> = context.dataStore.data.map { preferences ->
preferences[NODE_ID] ?: "android_${android.os.Build.MODEL}"
}
val autoConnect: Flow<Boolean> = context.dataStore.data.map { preferences ->
preferences[AUTO_CONNECT] ?: false
}
val notificationsEnabled: Flow<Boolean> = context.dataStore.data.map { preferences ->
preferences[NOTIFICATIONS_ENABLED] ?: true
}
val screenshotQuality: Flow<Int> = context.dataStore.data.map { preferences ->
preferences[SCREENSHOT_QUALITY]?.toIntOrNull() ?: 80
}
suspend fun setGatewayUrl(url: String) {
context.dataStore.edit { preferences ->
preferences[GATEWAY_URL] = url
}
}
suspend fun setNodeToken(token: String) {
context.dataStore.edit { preferences ->
preferences[NODE_TOKEN] = token
}
}
suspend fun setNodeId(id: String) {
context.dataStore.edit { preferences ->
preferences[NODE_ID] = id
}
}
suspend fun setAutoConnect(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[AUTO_CONNECT] = enabled
}
}
suspend fun setNotificationsEnabled(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[NOTIFICATIONS_ENABLED] = enabled
}
}
suspend fun setScreenshotQuality(quality: Int) {
context.dataStore.edit { preferences ->
preferences[SCREENSHOT_QUALITY] = quality.toString()
}
}
suspend fun clearAll() {
context.dataStore.edit { preferences ->
preferences.clear()
}
}
}
package com.openclaw.node.di
import android.content.Context
import com.openclaw.node.data.remote.NodeClient
import com.openclaw.node.data.repository.SettingsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideNodeClient(): NodeClient {
return NodeClient()
}
@Provides
@Singleton
fun provideSettingsRepository(
@ApplicationContext context: Context
): SettingsRepository {
return SettingsRepository(context)
}
}
package com.openclaw.node.domain.model
/**
* Represents a command received from the OpenClaw gateway
*/
sealed class NodeCommand {
abstract val id: String
data class Screenshot(override val id: String, val quality: Int = 80) : NodeCommand()
data class Tap(override val id: String, val x: Int, val y: Int) : NodeCommand()
data class Swipe(override val id: String, val x1: Int, val y1: Int, val x2: Int, val y2: Int, val duration: Int = 300) : NodeCommand()
data class LongPress(override val id: String, val x: Int, val y: Int, val duration: Int = 500) : NodeCommand()
data class LaunchApp(override val id: String, val packageName: String) : NodeCommand()
data class Shell(override val id: String, val command: String) : NodeCommand()
data class DeviceInfo(override val id: String) : NodeCommand()
data class ListApps(override val id: String, val systemApps: Boolean = false) : NodeCommand()
data class GetForegroundApp(override val id: String) : NodeCommand()
data class NotificationListener(override val id: String, val enabled: Boolean) : NodeCommand()
data class KeyEvent(override val id: String, val keyCode: Int) : NodeCommand()
object Ping : NodeCommand() { override val id: String = "" }
}
/**
* Response sent back to the gateway
*/
sealed class NodeResponse {
abstract val id: String
data class Success(override val id: String, val data: Map<String, Any> = emptyMap()) : NodeResponse()
data class Error(override val id: String, val error: String) : NodeResponse()
data class ScreenshotResponse(override val id: String, val base64Image: String) : NodeResponse()
}
/**
* Events pushed to the gateway without request
*/
sealed class NodeEvent {
data class Notification(val app: String, val title: String, val text: String, val timestamp: Long) : NodeEvent()
data class Battery(val level: Int, val charging: Boolean) : NodeEvent()
data class ForegroundAppChanged(val packageName: String, val appName: String) : NodeEvent()
data class ConnectionStatus(val connected: Boolean, val nodeId: String) : NodeEvent()
}
/**
* Node capabilities advertised during registration
*/
data class NodeCapabilities(
val screenshot: Boolean = true,
val touch: Boolean = true,
val shell: Boolean = true,
val notifications: Boolean = true,
val appControl: Boolean = true,
val sensors: Boolean = true
)
/**
* Device information
*/
data class DeviceInfo(
val model: String,
val manufacturer: String,
val androidVersion: String,
val sdkInt: Int,
val batteryLevel: Int,
val charging: Boolean,
val storage: StorageInfo,
val network: NetworkInfo
)
data class StorageInfo(
val total: Long,
val available: Long
)
data class NetworkInfo(
val connected: Boolean,
val type: String, // wifi, mobile, ethernet
val ip: String?
)
package com.openclaw.node.service
import android.accessibilityservice.AccessibilityService
import android.content.Intent
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
/**
* Service for mirroring notifications to the OpenClaw gateway
*/
class NotificationListener : NotificationListenerService() {
companion object {
var instance: NotificationListener? = null
var onNotificationReceived: ((String, String, String) -> Unit)? = null
}
override fun onCreate() {
super.onCreate()
instance = this
}
override fun onDestroy() {
super.onDestroy()
instance = null
}
override fun onNotificationPosted(sbn: StatusBarNotification?) {
sbn ?: return
// Filter out our own notifications
if (sbn.packageName == "com.openclaw.node") {
return
}
val extras = sbn.notification.extras
val title = extras.getCharSequence("android.title")?.toString() ?: ""
val text = extras.getCharSequence("android.text")?.toString() ?: ""
onNotificationReceived?.invoke(sbn.packageName, title, text)
}
override fun onNotificationRemoved(sbn: StatusBarNotification?) {
// Handle notification removal if needed
}
}
package com.openclaw.node.service
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.ImageReader
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.DisplayMetrics
import android.view.WindowManager
import java.io.ByteArrayOutputStream
/**
* Service for capturing screenshots using MediaProjection API
*/
class ScreenshotService(private val context: Context) {
private var mediaProjection: MediaProjection? = null
private var virtualDisplay: VirtualDisplay? = null
private var imageReader: ImageReader? = null
private var isCapturing = false
private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val mainHandler = Handler(Looper.getMainLooper())
private var callback: ((Result<String>) -> Unit)? = null
/**
* Initialize the screenshot service with a media projection
*/
fun initialize(projection: MediaProjection) {
this.mediaProjection = projection
setupVirtualDisplay()
}
/**
* Handle the activity result from requesting screen capture permission
*/
fun onActivityResult(resultCode: Int, data: Intent?, onReady: () -> Unit) {
if (resultCode != Activity.RESULT_OK || data == null) {
callback?.invoke(Result.failure(Exception("Screen capture permission denied")))
return
}
val projectionManager = context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
mediaProjection = projectionManager.getMediaProjection(resultCode, data)
mediaProjection?.registerCallback(object : MediaProjection.Callback() {
override fun onStop() {
cleanup()
}
}, mainHandler)
setupVirtualDisplay()
onReady()
}
@SuppressLint("WrongConstant")
private fun setupVirtualDisplay() {
val metrics = DisplayMetrics()
windowManager.defaultDisplay.getRealMetrics(metrics)
val density = metrics.densityDpi
val width = metrics.widthPixels
val height = metrics.heightPixels
imageReader = ImageReader.newInstance(
width, height,
PixelFormat.RGBA_8888, 2
)
virtualDisplay = mediaProjection?.createVirtualDisplay(
"OpenClawScreenshot",
width, height, density,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
imageReader?.surface,
null, mainHandler
)
isCapturing = true
}
/**
* Capture a single screenshot
*/
fun captureScreenshot(quality: Int = 80, onResult: (Result<String>) -> Unit) {
callback = onResult
if (!isCapturing || imageReader == null) {
onResult(Result.failure(Exception("Screenshot service not initialized")))
return
}
try {
val image = imageReader?.acquireLatestImage()
if (image == null) {
onResult(Result.failure(Exception("Failed to acquire image")))
return
}
val planes = image.planes
val buffer = planes[0].buffer
val pixelStride = planes[0].pixelStride
val rowStride = planes[0].rowStride
val rowPadding = rowStride - pixelStride * image.width
val bitmap = Bitmap.createBitmap(
image.width + rowPadding / pixelStride,
image.height,
Bitmap.Config.ARGB_8888
)
bitmap.copyPixelsFromBuffer(buffer)
// Crop to actual screen size
val croppedBitmap = Bitmap.createBitmap(
bitmap, 0, 0,
image.width, image.height
)
val outputStream = ByteArrayOutputStream()
croppedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
val base64 = Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
bitmap.recycle()
croppedBitmap.recycle()
image.close()
onResult(Result.success(base64))
} catch (e: Exception) {
onResult(Result.failure(e))
}
}
/**
* Start continuous screenshot streaming
*/
fun startStream(fps: Int = 5, onFrame: (String) -> Unit) {
// Implementation for streaming mode
// Would use a coroutine to capture at regular intervals
}
/**
* Stop screenshot capture
*/
fun stop() {
isCapturing = false
cleanup()
}
private fun cleanup() {
virtualDisplay?.release()
virtualDisplay = null
imageReader?.close()
imageReader = null
mediaProjection?.stop()
mediaProjection = null
}
companion object {
const val REQUEST_CODE = 1001
}
}
package com.openclaw.node.service
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.content.Intent
import android.graphics.Path
import android.graphics.PixelFormat
import android.os.Build
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
/**
* AccessibilityService for performing touch gestures
*/
class TouchService : AccessibilityService() {
companion object {
var instance: TouchService? = null
private set
var isServiceRunning = false
private set
}
override fun onServiceConnected() {
super.onServiceConnected()
instance = this
isServiceRunning = true
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
// Could forward accessibility events to gateway here
}
override fun onInterrupt() {
// Handle interruption
}
override fun onDestroy() {
super.onDestroy()
instance = null
isServiceRunning = false
}
override fun onUnbind(intent: Intent?): Boolean {
instance = null
isServiceRunning = false
return super.onUnbind(intent)
}
/**
* Perform a tap gesture at coordinates
*/
fun tap(x: Int, y: Int, callback: ((Boolean) -> Unit)? = null): Boolean {
if (!isServiceRunning) {
callback?.invoke(false)
return false
}
val path = Path().apply {
moveTo(x.toFloat(), y.toFloat())
}
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, 100))
.build()
return dispatchGesture(gesture, object : GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription?) {
callback?.invoke(true)
}
override fun onCancelled(gestureDescription: GestureDescription?) {
callback?.invoke(false)
}
}, null)
}
/**
* Perform a swipe gesture from (x1,y1) to (x2,y2)
*/
fun swipe(x1: Int, y1: Int, x2: Int, y2: Int, duration: Int = 300, callback: ((Boolean) -> Unit)? = null): Boolean {
if (!isServiceRunning) {
callback?.invoke(false)
return false
}
val path = Path().apply {
moveTo(x1.toFloat(), y1.toFloat())
lineTo(x2.toFloat(), y2.toFloat())
}
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, duration.toLong()))
.build()
return dispatchGesture(gesture, object : GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription?) {
callback?.invoke(true)
}
override fun onCancelled(gestureDescription: GestureDescription?) {
callback?.invoke(false)
}
}, null)
}
/**
* Perform a long press gesture
*/
fun longPress(x: Int, y: Int, duration: Int = 500, callback: ((Boolean) -> Unit)? = null): Boolean {
if (!isServiceRunning) {
callback?.invoke(false)
return false
}
val path = Path().apply {
moveTo(x.toFloat(), y.toFloat())
}
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, duration.toLong()))
.build()
return dispatchGesture(gesture, object : GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription?) {
callback?.invoke(true)
}
override fun onCancelled(gestureDescription: GestureDescription?) {
callback?.invoke(false)
}
}, null)
}
/**
* Perform a pinch zoom gesture (stretch goal)
*/
fun pinchZoom(centerX: Int, centerY: Int, scale: Float, duration: Int = 500): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return false
}
// Simplified pinch - just a two-finger gesture
val offset = 100
val path1 = Path().apply {
moveTo((centerX - offset).toFloat(), centerY.toFloat())
lineTo((centerX - offset - (offset * scale)).toFloat(), centerY.toFloat())
}
val path2 = Path().apply {
moveTo((centerX + offset).toFloat(), centerY.toFloat())
lineTo((centerX + offset + (offset * scale)).toFloat(), centerY.toFloat())
}
val builder = GestureDescription.Builder()
builder.addStroke(GestureDescription.StrokeDescription(path1, 0, duration.toLong()))
builder.addStroke(GestureDescription.StrokeDescription(path2, 0, duration.toLong()))
return dispatchGesture(builder.build(), null, null)
}
/**
* Perform a double tap gesture
*/
fun doubleTap(x: Int, y: Int, callback: ((Boolean) -> Unit)? = null): Boolean {
// Double tap is two quick taps
val path = Path().apply {
moveTo(x.toFloat(), y.toFloat())
}
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, 50))
.addStroke(GestureDescription.StrokeDescription(path, 100, 50))
.build()
return dispatchGesture(gesture, object : GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription?) {
callback?.invoke(true)
}
override fun onCancelled(gestureDescription: GestureDescription?) {
callback?.invoke(false)
}
}, null)
}
}
package com.openclaw.node.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val Purple80 = Color(0xFFD0BCFF)
private val PurpleGrey80 = Color(0xFFCCC2DC)
private val Pink80 = Color(0xFFEFB8C8)
private val Purple40 = Color(0xFF6650a4)
private val PurpleGrey40 = Color(0xFF625b71)
private val Pink40 = Color(0xFF7D5260)
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
@Composable
fun OpenClawAndroidNodeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">OpenClaw Node</string> <string name="app_name">OpenClaw Node</string>
<string name="accessibility_service_description">OpenClaw Touch Control - Allows the app to perform touch gestures (tap, swipe, scroll) on your device. Required for remote control functionality.</string>
</resources> </resources>
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagRequestTouchExplorationMode"
android:canPerformGestures="true"
android:canRetrieveWindowContent="false"
android:description="@string/accessibility_service_description"
android:notificationTimeout="100"
android:settingsActivity="com.openclaw.node.ui.MainActivity" />
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment