diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/android/app/Service.kt b/tools/chatgui-android/router-tests/src/main/kotlin/android/app/Service.kt new file mode 100644 index 00000000..93ccd3ff --- /dev/null +++ b/tools/chatgui-android/router-tests/src/main/kotlin/android/app/Service.kt @@ -0,0 +1,31 @@ +package android.app + +import android.content.Context +import android.content.Intent +import android.os.IBinder + +/** OS lifecycle контролирует тест: stopSelf лишь запрашивает stop, onDestroy доставляется отдельно. */ +open class Service : Context() { + companion object { const val START_NOT_STICKY = 2; const val STOP_FOREGROUND_REMOVE = 1 } + var foregroundId = 0 + var foregroundTypes = 0 + var foregroundAttempts = 0 + var failForeground = false + var failForegroundAfter = false + var stops = 0 + var destroys = 0 + open fun onCreate() {} + open fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = START_NOT_STICKY + open fun onDestroy() { destroys++ } + open fun onBind(intent: Intent?): IBinder? = null + fun startForeground(id: Int, notification: Notification) { + foregroundAttempts++ + check(!failForeground) { "foreground promotion unavailable" } + foregroundId = id + requireNotNull(notificationManager).notify(id, notification) + check(!failForegroundAfter) { "foreground promotion failed after effect" } + } + fun stopForeground(flags: Int) { if (flags == STOP_FOREGROUND_REMOVE && foregroundId != 0) notificationManager?.cancel(foregroundId) } + fun stopSelf() { stops++ } + fun stopSelf(startId: Int) { stops++ } +} diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/android/content/Context.kt b/tools/chatgui-android/router-tests/src/main/kotlin/android/content/Context.kt index 33081efa..20c85846 100644 --- a/tools/chatgui-android/router-tests/src/main/kotlin/android/content/Context.kt +++ b/tools/chatgui-android/router-tests/src/main/kotlin/android/content/Context.kt @@ -13,11 +13,12 @@ import com.utun.chat.data.HostConfigProvider import java.io.File /** Только host-тест: управляемые Android managers, без устройств/разрешений ОС. */ -class Context { +open class Context { companion object { const val AUDIO_SERVICE = "audio"; const val SENSOR_SERVICE = "sensor"; const val VIBRATOR_SERVICE = "vibrator" const val POWER_SERVICE = "power" const val WIFI_SERVICE = "wifi" + const val WINDOW_SERVICE = "window" const val RECEIVER_NOT_EXPORTED = 4 } val packageName = "com.utun.chat" @@ -26,6 +27,22 @@ class Context { var vibrator: Vibrator? = Vibrator() var powerManager: PowerManager? = PowerManager() var failPowerService = false + val resources = HostResources() + var windowManager: android.view.WindowManager? = android.view.WindowManager() + val serviceStarts = mutableListOf() + val serviceStops = mutableListOf() + var failServiceStart = false + var failServiceStop = false + var acceptServiceStop = true + fun startForegroundService(intent: Intent) { + serviceStarts.add(Intent(intent)) + check(!failServiceStart) { "foreground service start unavailable" } + } + fun stopService(intent: Intent): Boolean { + serviceStops.add(Intent(intent)) + check(!failServiceStop) { "service stop unavailable" } + return acceptServiceStop + } val activityAttempts = mutableListOf() var onStartActivity: (Intent) -> Unit = {} var notificationManager: NotificationManager? = NotificationManager() @@ -64,6 +81,7 @@ class Context { VIBRATOR_SERVICE -> vibrator POWER_SERVICE -> { check(!failPowerService) { "power service lookup unavailable" }; powerManager } WIFI_SERVICE -> { check(!failWifiService) { "Wi-Fi service unavailable" }; wifiManager } + WINDOW_SERVICE -> windowManager else -> error("Unknown service: $name") } fun getSystemService(type: Class): T? { @@ -79,3 +97,9 @@ class Context { } fun checkSelfPermission(name: String): Int = if (permissionGranted) 0 else -1 } + +class HostResources { + val displayMetrics = HostDisplayMetrics() + fun getResourceEntryName(id: Int) = "resource_$id" +} +class HostDisplayMetrics { var density = 1f; var widthPixels = 1080 } diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/android/view/HostOverlay.kt b/tools/chatgui-android/router-tests/src/main/kotlin/android/view/HostOverlay.kt new file mode 100644 index 00000000..55596df7 --- /dev/null +++ b/tools/chatgui-android/router-tests/src/main/kotlin/android/view/HostOverlay.kt @@ -0,0 +1,61 @@ +package android.view + +import android.content.Context + +/** Модель оконного IPC: add/remove могут отказать до или после эффекта; callbacks доставляются вручную. */ +class WindowManager { + class LayoutParams(var width: Int, var height: Int, val type: Int, val flags: Int, val format: Int) { + companion object { + const val WRAP_CONTENT = -2; const val TYPE_APPLICATION_OVERLAY = 2038; const val TYPE_PHONE = 2002 + const val FLAG_NOT_FOCUSABLE = 8 + } + var gravity = 0; var x = 0; var y = 0 + } + val windows = mutableMapOf() + var failures = emptySet() + var additions = 0; var removals = 0; var updates = 0 + fun addView(view: View, params: LayoutParams) { + additions++ + check("add" !in failures) { "window add unavailable" } + windows[view] = params; view.layout = params + check("add after" !in failures) { "window add failed after effect" } + } + fun removeView(view: View) { + removals++ + check("remove" !in failures) { "window remove unavailable" } + require(windows.remove(view) != null) { "window not attached" } + check("remove after" !in failures) { "window remove failed after effect" } + } + fun updateViewLayout(view: View, params: LayoutParams) { + updates++ + check("update" !in failures) { "window layout unavailable" } + require(windows.containsKey(view)) { "window not attached" } + windows[view] = params; view.layout = params + } +} +open class View { + fun interface OnTouchListener { fun onTouch(view: View, event: MotionEvent): Boolean } + var background: Any? = null + var layout: WindowManager.LayoutParams? = null + var isLaidOut = true + val width get() = layout?.width ?: 0 + val height get() = layout?.height ?: 0 + var listener: OnTouchListener? = null + val animations = mutableListOf() + val layouts = mutableListOf<() -> Unit>() + fun setOnTouchListener(value: OnTouchListener) { listener = value } + fun touch(event: MotionEvent) = listener?.onTouch(this, event) ?: false + fun postOnAnimation(task: Runnable) { animations.add(task) } + fun dispatchAnimations() { val pending = animations.toList(); animations.clear(); pending.forEach { it.run() } } + fun getLocationOnScreen(out: IntArray) { out[0] = layout?.x ?: 0; out[1] = layout?.y ?: 0 } +} +class MotionEvent(val action: Int, val rawX: Float = 0f, val rawY: Float = 0f, val eventTime: Long = 1, + val downTime: Long = eventTime) { + companion object { const val ACTION_DOWN = 0; const val ACTION_UP = 1; const val ACTION_MOVE = 2; const val ACTION_CANCEL = 3 } +} +object Gravity { const val CENTER = 17; const val TOP = 48; const val LEFT = 3 } +class ViewConfiguration { + val scaledTouchSlop = 8 + companion object { fun get(context: Context) = ViewConfiguration(); fun getDoubleTapTimeout() = 300 } +} +class LayoutInflater