Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,58 @@ import hk.uwu.reareye.internal.hostbridge.IHookHostBridgeBootstrap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

private class HookHostBridgeRequestState {
class Attempt(
val latch: CountDownLatch = CountDownLatch(1),
)

data class Lease(
val attempt: Attempt,
val started: Boolean,
)

private val lock = Any()
private var current: Attempt? = null

fun acquire(startRequest: (Attempt) -> Boolean): Lease = synchronized(lock) {
current?.let { return@synchronized Lease(it, started = false) }
Attempt().also { attempt ->
current = attempt
if (!startRequest(attempt)) {
current = null
attempt.latch.countDown()
}
}.let { Lease(it, started = true) }
}

fun abandon(attempt: Attempt) {
synchronized(lock) {
if (current === attempt) {
current = null
}
}
}

fun fail(attempt: Attempt) {
synchronized(lock) {
attempt.latch.countDown()
if (current === attempt) {
current = null
}
}
}

fun completeConnection(attempt: Attempt? = null) {
synchronized(lock) {
attempt?.latch?.countDown()
current?.let { pending ->
if (pending !== attempt) pending.latch.countDown()
}
current = null
}
}
}

abstract class HookHostBridgeClient<Remote : IInterface>(
private val hostPackage: String,
) {
Expand All @@ -26,8 +78,7 @@ abstract class HookHostBridgeClient<Remote : IInterface>(
@Volatile
private var remoteDeathRecipient: IBinder.DeathRecipient? = null

@Volatile
private var connectLatch: CountDownLatch? = null
private val requestState = HookHostBridgeRequestState()

@Volatile
private var closedListener: ((String) -> Unit)? = null
Expand Down Expand Up @@ -103,12 +154,9 @@ abstract class HookHostBridgeClient<Remote : IInterface>(
private fun requestBridge(forceSync: Boolean, timeoutMs: Long): Boolean {
remote?.let { return true }

val latch = synchronized(lock) {
remote?.let { return true }
connectLatch?.let { return@synchronized it }

CountDownLatch(1).also { pending ->
connectLatch = pending
val lease = requestState.acquire { pending ->
synchronized(lock) {
if (remote != null) return@synchronized false
val context = appContext
val ok = if (context == null) {
false
Expand All @@ -117,7 +165,7 @@ abstract class HookHostBridgeClient<Remote : IInterface>(
onBeforeRequest(forceSync)
val callback = object : IHookHostBridgeBootstrap.Stub() {
override fun onBinderReady(binder: IBinder?) {
installRemote(asRemoteInterface(binder))
installRemote(asRemoteInterface(binder), pending)
}
}
val bundle = Bundle().apply {
Expand All @@ -134,34 +182,28 @@ abstract class HookHostBridgeClient<Remote : IInterface>(
true
}.getOrDefault(false)
}

if (!ok) {
connectLatch = null
pending.countDown()
}
ok
}
}
val attempt = lease.attempt

if (timeoutMs <= 0L) {
if (lease.started) requestState.abandon(attempt)
return remote != null
}

val ok = runCatching { latch.await(timeoutMs, TimeUnit.MILLISECONDS) }
val ok = runCatching { attempt.latch.await(timeoutMs, TimeUnit.MILLISECONDS) }
.getOrDefault(false) && remote != null
synchronized(lock) {
if (connectLatch === latch) {
connectLatch = null
}
}
requestState.abandon(attempt)
return ok
}

private fun installRemote(candidate: Remote?) {
private fun installRemote(
candidate: Remote?,
attempt: HookHostBridgeRequestState.Attempt,
) {
if (candidate == null) {
synchronized(lock) {
connectLatch?.countDown()
connectLatch = null
}
requestState.fail(attempt)
return
}

Expand All @@ -180,21 +222,20 @@ abstract class HookHostBridgeClient<Remote : IInterface>(
true
}.getOrDefault(false)
if (!linked) {
connectLatch?.countDown()
connectLatch = null
false
} else {
remote = candidate
remoteBinder = binder
remoteDeathRecipient = deathRecipient
connectLatch?.countDown()
connectLatch = null
true
}
}

if (installed) {
requestState.completeConnection(attempt)
onRemoteConnected(candidate)
} else {
requestState.fail(attempt)
}
}

Expand All @@ -205,10 +246,9 @@ abstract class HookHostBridgeClient<Remote : IInterface>(
val hadRemote = synchronized(lock) {
val existed = remote != null
releaseRemoteLocked()
connectLatch?.countDown()
connectLatch = null
existed
}
requestState.completeConnection()

if (hadRemote) {
onRemoteDisconnected(reason)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ import android.os.IBinder
import hk.uwu.reareye.hook.hostbridge.HookHostBridgeClient
import hk.uwu.reareye.internal.notification.INotificationRouteBridgeService
import java.util.ArrayDeque
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong

internal const val NOTIFICATION_ROUTE_BIND_TIMEOUT_MS = 0L

private enum class NotificationRouteDrainResult {
EMPTY,
DISCONNECTED,
}

private fun <T> drainNotificationRouteQueue(
next: () -> T?,
deliver: (T) -> Boolean?,
remove: (T) -> Unit,
): NotificationRouteDrainResult {
while (true) {
val pending = next() ?: return NotificationRouteDrainResult.EMPTY
deliver(pending) ?: return NotificationRouteDrainResult.DISCONNECTED
remove(pending)
}
}

internal class NotificationRouteBridgeClient :
HookHostBridgeClient<INotificationRouteBridgeService>(
Expand All @@ -29,20 +51,25 @@ internal class NotificationRouteBridgeClient :

private val pendingDispatches = ArrayDeque<PendingDispatch>()
private val queueLock = Any()
private val drainScheduled = AtomicBoolean(false)
private val enqueueVersion = AtomicLong(0L)
private val drainExecutor = Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "REAREye-NotificationRoute").apply { isDaemon = true }
}

override fun asRemoteInterface(binder: IBinder?): INotificationRouteBridgeService? {
return INotificationRouteBridgeService.Stub.asInterface(binder)
}

override fun onRemoteConnected(remote: INotificationRouteBridgeService) {
drainPendingDispatches()
scheduleDrain()
}

fun bind(
context: Context,
onConnected: (() -> Unit)? = null,
onClosed: ((String) -> Unit)? = null,
timeoutMs: Long = 900L,
timeoutMs: Long = NOTIFICATION_ROUTE_BIND_TIMEOUT_MS,
): Boolean {
return bindToHost(
context = context,
Expand All @@ -56,17 +83,11 @@ internal class NotificationRouteBridgeClient :
val normalizedSubchannel = subchannel.trim()
if (normalizedSubchannel.isBlank()) return false

val payloadCopy = Bundle(payload)
callRemote { remote ->
remote.dispatch(normalizedSubchannel, payloadCopy)
}?.let { return it }

enqueuePendingDispatch(
subchannel = normalizedSubchannel,
payload = payloadCopy,
payload = Bundle(payload),
)
currentContext()?.let { bind(it, timeoutMs = 0L) }
requestRebind()
scheduleDrain()
return true
}

Expand All @@ -83,26 +104,52 @@ internal class NotificationRouteBridgeClient :
while (pendingDispatches.size > MAX_PENDING_DISPATCHES) {
pendingDispatches.removeFirst()
}
enqueueVersion.incrementAndGet()
}
}

private fun drainPendingDispatches() {
while (true) {
val next = synchronized(queueLock) {
pruneExpiredDispatchesLocked()
pendingDispatches.firstOrNull()
} ?: return

val delivered = callRemote { remote ->
remote.dispatch(next.subchannel, Bundle(next.payload))
} ?: return
if (!delivered) return

synchronized(queueLock) {
if (pendingDispatches.firstOrNull() === next) {
pendingDispatches.removeFirst()
} else {
pendingDispatches.remove(next)
private fun scheduleDrain() {
if (!drainScheduled.compareAndSet(false, true)) return
drainExecutor.execute {
val observedVersion = enqueueVersion.get()
var result = NotificationRouteDrainResult.DISCONNECTED
try {
result = drainNotificationRouteQueue(
next = {
synchronized(queueLock) {
pruneExpiredDispatchesLocked()
pendingDispatches.firstOrNull()
}
},
deliver = { pending ->
callRemote { remote ->
remote.dispatch(pending.subchannel, Bundle(pending.payload))
}
},
remove = { pending ->
synchronized(queueLock) {
if (pendingDispatches.firstOrNull() === pending) {
pendingDispatches.removeFirst()
} else {
pendingDispatches.remove(pending)
}
}
},
)
if (result == NotificationRouteDrainResult.DISCONNECTED) {
currentContext()?.let {
bind(it, timeoutMs = NOTIFICATION_ROUTE_BIND_TIMEOUT_MS)
} ?: requestRebind()
}
} finally {
drainScheduled.set(false)
val pendingWork = synchronized(queueLock) {
pruneExpiredDispatchesLocked()
pendingDispatches.isNotEmpty()
}
val receivedNewWork = enqueueVersion.get() != observedVersion
if (pendingWork && (isConnected() || receivedNewWork)) {
scheduleDrain()
}
}
}
Expand Down
Loading