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
15 changes: 14 additions & 1 deletion radiography/api/radiography.api
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,20 @@ public final class radiography/Radiography {
public static final fun scan (Lradiography/ScanScope;)Ljava/lang/String;
public static final fun scan (Lradiography/ScanScope;Ljava/util/List;)Ljava/lang/String;
public static final fun scan (Lradiography/ScanScope;Ljava/util/List;Lradiography/ViewFilter;)Ljava/lang/String;
public static synthetic fun scan$default (Lradiography/ScanScope;Ljava/util/List;Lradiography/ViewFilter;ILjava/lang/Object;)Ljava/lang/String;
public static final fun scan (Lradiography/ScanScope;Ljava/util/List;Lradiography/ViewFilter;Lradiography/ScanExecutor;)Ljava/lang/String;
public static synthetic fun scan$default (Lradiography/ScanScope;Ljava/util/List;Lradiography/ViewFilter;Lradiography/ScanExecutor;ILjava/lang/Object;)Ljava/lang/String;
}

public abstract interface class radiography/ScanExecutor {
public abstract fun execute (Ljava/util/concurrent/Callable;)Ljava/lang/String;
}

public final class radiography/ScanExecutors {
public static final field INSTANCE Lradiography/ScanExecutors;
public static final field PassthroughExecutor Lradiography/ScanExecutor;
public static final fun HandlerPostingExecutor (Landroid/os/Handler;JLjava/util/concurrent/TimeUnit;)Lradiography/ScanExecutor;
public static final fun LooperEnforcingExecutor (Landroid/os/Looper;)Lradiography/ScanExecutor;
public static final fun NeverThrowingExecutor (Lradiography/ScanExecutor;)Lradiography/ScanExecutor;
}

public abstract interface class radiography/ScanScope {
Expand Down
47 changes: 17 additions & 30 deletions radiography/src/main/java/radiography/Radiography.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package radiography

import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.WindowManager
import androidx.annotation.VisibleForTesting
import radiography.Radiography.scan
import radiography.ScanExecutors.HandlerPostingExecutor
import radiography.ScanExecutors.NeverThrowingExecutor
import radiography.ScanExecutors.mainHandler
import radiography.ScanScopes.AllWindowsScope
import radiography.ScannableView.AndroidView
import radiography.ViewStateRenderers.DefaultsNoPii
import radiography.internal.renderTreeString
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit.SECONDS

/**
Expand Down Expand Up @@ -42,37 +42,24 @@ public object Radiography {
public fun scan(
scanScope: ScanScope = AllWindowsScope,
viewStateRenderers: List<ViewStateRenderer> = DefaultsNoPii,
viewFilter: ViewFilter = ViewFilters.NoFilter
): String = buildString {
val roots = try {
scanScope.findRoots()
} catch (e: Throwable) {
append("Exception when finding scan roots: ${e.message}")
return@buildString
}

roots.forEach { scanRoot ->
// The entire view tree is single threaded, and that's typically the main thread, but
// it doesn't have to be, and we don't know where the passed in view is coming from.
val viewLooper = (scanRoot as? AndroidView)?.view?.handler?.looper
?: Looper.getMainLooper()!!

if (viewLooper.thread == Thread.currentThread()) {
scanFromLooperThread(scanRoot, viewStateRenderers, viewFilter)
} else {
val latch = CountDownLatch(1)
Handler(viewLooper).post {
scanFromLooperThread(scanRoot, viewStateRenderers, viewFilter)
latch.countDown()
}
if (!latch.await(5, SECONDS)) {
return "Could not retrieve view hierarchy from main thread after 5 seconds wait"
}
viewFilter: ViewFilter = ViewFilters.NoFilter,
scanExecutor: ScanExecutor = NeverThrowingExecutor(
HandlerPostingExecutor(
mainHandler,
5,
SECONDS
)
)
): String = scanExecutor.execute {
buildString {
val roots = scanScope.findRoots()
roots.forEach { scanRoot ->
scanRoot(scanRoot, viewStateRenderers, viewFilter)
}
}
}

private fun StringBuilder.scanFromLooperThread(
private fun StringBuilder.scanRoot(
rootView: ScannableView,
viewStateRenderers: List<ViewStateRenderer>,
viewFilter: ViewFilter
Expand Down
19 changes: 19 additions & 0 deletions radiography/src/main/java/radiography/ScanExecutor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package radiography

import java.util.concurrent.Callable

/**
* Ensures that scanning happens on the right thread (by e.g. posting work, throwing, or simply
* not checking, depending on implementation)
*
* Some commons executors are:
* - [ScanExecutors.NeverThrowingExecutor]
* - [ScanExecutors.PassthroughExecutor]
* - [ScanExecutors.HandlerPostingExecutor]
* - [ScanExecutors.LooperEnforcingExecutor]
*/
public fun interface ScanExecutor {

/** Returns the result of executing [callable] */
public fun execute(callable: Callable<String>): String
}
82 changes: 82 additions & 0 deletions radiography/src/main/java/radiography/ScanExecutors.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package radiography

import android.os.Handler
import android.os.Looper
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

public object ScanExecutors {

internal val mainHandler by lazy {
Handler(Looper.getMainLooper())
}

/**
* Runs the scanning tasks synchronously without any thread check, posting or exception catching.
*/
@JvmField
public val PassthroughExecutor: ScanExecutor = ScanExecutor { callable ->
callable.call()
}

/**
* Executes the scanning tasks on the provided [delegate] ScanExecutor but catches any
* exception and returns an error string instead.
*/
@JvmStatic
public fun NeverThrowingExecutor(delegate: ScanExecutor): ScanExecutor = ScanExecutor { callable ->
try {
delegate.execute(callable)
} catch (throwable: Throwable) {
"Exception when scanning: ${throwable.message}"
}
}

/**
* Runs the scanning tasks and blocks the current thread until completion. If the current thread
* is the same as the [handler] thread, then the runnable runs immediately without being enqueued.
* Otherwise, posts the runnable to [handler] and waits for it to complete before returning,
* throwing if timeout occurs or if the work could not be scheduled, and rethrowing any main
* thread exception to the calling thread.
*/
@JvmStatic
public fun HandlerPostingExecutor(
handler: Handler,
timeout: Long,
timeoutUnit: TimeUnit
): ScanExecutor = ScanExecutor { callable ->
if (handler.looper === Looper.myLooper()) {
callable.call()
} else {
var result: Result<String>? = null
val latch = CountDownLatch(1)
val posted = handler.post {
result = try {
Result.success(callable.call())
} catch (throwable: Throwable) {
Result.failure(throwable)
}
latch.countDown()
}
check(posted) {
"Callback not posted, probably because the looper processing the message queue is exiting."
}
check(latch.await(timeout, timeoutUnit)) {
"Could not scan hierarchy from main thread, timed out"
}
result!!.getOrThrow()
}
}

/**
* Runs the scanning tasks synchronously, throwing if the current thread is not the same as the
* thread of the provided [Looper]. Does not catch any exception.
*/
@JvmStatic
public fun LooperEnforcingExecutor(looper: Looper): ScanExecutor = ScanExecutor { callable ->
check(looper === Looper.myLooper()) {
"Should be called from ${looper.thread.name}, not ${Thread.currentThread().name}"
}
callable.call()
}
}