Skip to content

Commit

Permalink
Merge pull request #70 from square/py/fix_crash
Browse files Browse the repository at this point in the history
Fix 'lateinit property currentTrigger has not been initialized' on WIKO devices
  • Loading branch information
pyricau authored Oct 26, 2024
2 parents d7335e8 + 5d84314 commit 2ce2018
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 4 deletions.
1 change: 1 addition & 0 deletions papa-main-trace/api/papa-main-trace.api
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public final class papa/MainThreadMessageSpy {
public final fun addTracer (Lpapa/MainThreadMessageSpy$Tracer;)V
public final fun getCurrentMessageAsString ()Ljava/lang/String;
public final fun getEnabled ()Z
public final fun isInMainThreadMessage ()Z
public final fun removeTracer (Lpapa/MainThreadMessageSpy$Tracer;)V
public final fun startSpyingMainThreadDispatching ()V
public final fun stopSpyingMainThreadDispatching ()V
Expand Down
2 changes: 1 addition & 1 deletion papa-main-trace/src/main/java/papa/Handlers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ object Handlers {
*/
fun onCurrentMainThreadMessageFinished(block: () -> Unit) {
checkOnMainThread()
if (MainThreadMessageSpy.enabled) {
if (MainThreadMessageSpy.isInMainThreadMessage) {
MainThreadMessageSpy.onCurrentMessageFinished(block)
} else {
mainThreadHandler.postAtFrontOfQueueAsync(block)
Expand Down
23 changes: 21 additions & 2 deletions papa-main-trace/src/main/java/papa/MainThreadMessageSpy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ object MainThreadMessageSpy {
var enabled = false
private set

val isInMainThreadMessage: Boolean
get() = enabled && currentMessageAsString != null

/**
* Must be called only from the main thread.
* Null if [enabled] is false or if the code calling this (from the main thread) is running
* from outside the dispatching of a main thread message. For example,
* [MessageQueue.nativePollOnce] may invoke input event dispatching code directly.
*/
var currentMessageAsString: String? = null
private set
get() {
Expand Down Expand Up @@ -59,9 +68,18 @@ object MainThreadMessageSpy {
// Looper can log to a printer before and after each message. We leverage this to surface the
// beginning and end of every main thread message in system traces. This costs a few extra string
// concatenations for each message handling.
// The printer is called before ('>>' prefix) and after ('<<' prefix) every message.

// Looper.mLogging is extracted to a local variable inside the message loop, and the same
// reference is used for before and after. We're setting Looper.mLogging in the middle of a
// message but won't get the "finish" callback because that local variable still references
// null at that point.
var before = true
Looper.getMainLooper().setMessageLogging { messageAsString ->
val before = messageAsString.startsWith('>')
if (!enabled) {
// We still get called here for the last message finishing after we called
// stopSpyingMainThreadDispatching which called Looper.setMessageLogging(null)
return@setMessageLogging
}
if (before) {
currentMessageAsString = messageAsString
}
Expand All @@ -71,6 +89,7 @@ object MainThreadMessageSpy {
if (!before) {
currentMessageAsString = null
}
before = !before
}
}

Expand Down
44 changes: 44 additions & 0 deletions papa/src/androidTest/java/papa/test/MainThreadMessageSpyTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package papa.test

import android.os.Build.VERSION
import android.view.Choreographer
import android.view.MotionEvent
import android.widget.Button
import androidx.test.core.app.ActivityScenario
Expand Down Expand Up @@ -78,6 +79,49 @@ class MainThreadMessageSpyTest {
.inOrder()
}

@Test fun inputEventDispatching_not_in_MainThread_Message() {
var spyEnabled: Boolean? = null
var isInMainThreadMessage: Boolean? = null
var currentMessageAsString: String? = null

val handledTap = CountDownLatch(1)
ActivityScenario.launch(TestActivity::class.java).use { scenario ->

val blockFrame = CountDownLatch(1)
val waitForFrame = CountDownLatch(1)
scenario.onActivity { activity ->
activity.setContentView(Button(activity).apply {
text = "Click Me"
setOnTouchListener { _, _ ->
spyEnabled = MainThreadMessageSpy.enabled
isInMainThreadMessage = MainThreadMessageSpy.isInMainThreadMessage
currentMessageAsString = MainThreadMessageSpy.currentMessageAsString
handledTap.countDown()
false
}
})
Choreographer.getInstance().postFrameCallback {
// unblock clicking code.
waitForFrame.countDown()
check(blockFrame.await(5, SECONDS))
// Giving a little time to enqueue the tap.
Thread.sleep(500)
}
}

check(waitForFrame.await(5, SECONDS))
// unblock frame, at this point tap should be enqueued and dequeued immediately, and not
// during a choreographer frame.
blockFrame.countDown()
onView(withText("Click Me")).location.sendTap()

check(handledTap.await(5, SECONDS))
assertThat(spyEnabled).isTrue()
assertThat(currentMessageAsString).isNull()
assertThat(isInMainThreadMessage).isFalse()
}
}

@Test
fun no_ConcurrentModificationException_when_iterating_after_onCurrentMessageFinished_removes_its_tracer() {
runOnMainSync {
Expand Down
6 changes: 5 additions & 1 deletion papa/src/main/java/papa/Choreographers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ object Choreographers {
private val pendingRenderedCallbacks = mutableListOf<OnFrameRenderedListener>()

private val isInChoreographerFrameMessage by mainThreadMessageScopedLazy {
"android.view.Choreographer\$FrameDisplayEventReceiver" in MainThreadMessageSpy.currentMessageAsString!!
if (MainThreadMessageSpy.isInMainThreadMessage) {
"android.view.Choreographer\$FrameDisplayEventReceiver" in MainThreadMessageSpy.currentMessageAsString!!
} else {
false
}
}

/**
Expand Down

0 comments on commit 2ce2018

Please sign in to comment.