Model overlay window IPC and foreground service lifetime for Android host tests

This commit is contained in:
evgeny committed 2026-10-29 13:34:23 +01:00
1 parent 6d5b2bd176
commit d1dc3bb8f7
3 files changed
+117 -1

No files matched your search

@@ -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++ }
}
@@ -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<Intent>()
val serviceStops = mutableListOf<Intent>()
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<Intent>()
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 <T> getSystemService(type: Class<T>): 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 }
@@ -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<View, LayoutParams>()
var failures = emptySet<String>()
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<Runnable>()
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