-
Notifications
You must be signed in to change notification settings - Fork 50
Allow any type of ScannableView #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,5 @@ | ||
| 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.ScanScopes.AllWindowsScope | ||
|
|
@@ -15,17 +11,15 @@ import java.util.concurrent.TimeUnit.SECONDS | |
|
|
||
| /** | ||
| * Utility class to scan through a view hierarchy and pretty print it to a [String]. | ||
| * Call [scan] or [View.scan]. | ||
| * Call [scan] or [android.view.View.scan]. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: the kotlin formatter should keep the import even if it's only used by kdoc. |
||
| */ | ||
| public object Radiography { | ||
|
|
||
| /** | ||
| * Scans the view hierarchies and pretty print them to a [String]. | ||
| * | ||
| * You should generally call this method from the main thread, as views are meant to be accessed | ||
| * from a single thread. If you call this from a background thread, this will schedule a message | ||
| * to the main thread to retrieve the view hierarchy from there and will wait up to 5 seconds | ||
| * or return an error message. This method will never throw, any thrown exception will have | ||
| * from a single thread. This method will never throw, any thrown exception will have | ||
| * its message included in the returned string. | ||
| * | ||
| * @param scanScope the [ScanScope] that determines what to scan. [AllWindowsScope] by default. | ||
|
|
@@ -52,27 +46,11 @@ public object Radiography { | |
| } | ||
|
|
||
| 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" | ||
| } | ||
| } | ||
| scanSingleRoot(scanRoot, viewStateRenderers, viewFilter) | ||
| } | ||
| } | ||
|
|
||
| private fun StringBuilder.scanFromLooperThread( | ||
| private fun StringBuilder.scanSingleRoot( | ||
| rootView: ScannableView, | ||
| viewStateRenderers: List<ViewStateRenderer>, | ||
| viewFilter: ViewFilter | ||
|
|
@@ -83,17 +61,8 @@ public object Radiography { | |
| appendLine() | ||
| } | ||
|
|
||
| val androidView = (rootView as? AndroidView)?.view | ||
| val layoutParams = androidView?.layoutParams | ||
| val title = (layoutParams as? WindowManager.LayoutParams)?.title?.toString() | ||
| ?: rootView.displayName | ||
| appendLine("$title:") | ||
|
|
||
| val startPosition = length | ||
| try { | ||
| androidView?.let { | ||
| appendLine("window-focus:${it.hasWindowFocus()}") | ||
| } | ||
| renderScannableViewTree(this, rootView, viewStateRenderers, viewFilter) | ||
| } catch (e: Throwable) { | ||
| insert( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package radiography | |
|
|
||
| import android.view.View | ||
| import android.view.ViewGroup | ||
| import android.view.WindowManager | ||
| import androidx.compose.ui.Modifier | ||
| import radiography.ScannableView.AndroidView | ||
| import radiography.ScannableView.ComposeView | ||
|
|
@@ -18,16 +19,24 @@ import radiography.internal.mightBeComposeView | |
| * Can either be an actual Android [View] ([AndroidView]) or a grouping of Composables that roughly | ||
| * represents the concept of a logical "view" ([ComposeView]). | ||
| */ | ||
| public sealed class ScannableView { | ||
| public interface ScannableView { | ||
|
|
||
| /** The string that be used to identify the type of the view in the rendered output. */ | ||
| public abstract val displayName: String | ||
| public val displayName: String | ||
|
|
||
| /** The children of this view. */ | ||
| public abstract val children: Sequence<ScannableView> | ||
| public val children: Sequence<ScannableView> | ||
|
|
||
| public class AndroidView(public val view: View) : ScannableView() { | ||
| override val displayName: String get() = view::class.java.simpleName | ||
| public class AndroidView(public val view: View) : ScannableView { | ||
| override val displayName: String get() { | ||
| val classSimpleName = view::class.java.simpleName | ||
| val windowTitle = (view.layoutParams as? WindowManager.LayoutParams)?.title?.toString() | ||
| if (windowTitle != null) { | ||
| return "$classSimpleName in $windowTitle window-focus:${view.hasWindowFocus()}" | ||
| } else { | ||
| return classSimpleName | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not report focus in this case?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there's no window title, then this isn't the root view for an attached window. In which case window focus doesn't make sense. Good question though, should add as comment. |
||
| } | ||
| } | ||
| override val children: Sequence<ScannableView> = view.scannableChildren() | ||
|
|
||
| override fun toString(): String = "${AndroidView::class.java.simpleName}($displayName)" | ||
|
|
@@ -45,7 +54,7 @@ public sealed class ScannableView { | |
| public val height: Int, | ||
| public val modifiers: List<Modifier>, | ||
| override val children: Sequence<ScannableView> | ||
| ) : ScannableView() { | ||
| ) : ScannableView { | ||
| override fun toString(): String = "${ComposeView::class.java.simpleName}($displayName)" | ||
| } | ||
|
|
||
|
|
@@ -57,7 +66,7 @@ public sealed class ScannableView { | |
| * return the error message along with any portion of the tree that was rendered before the | ||
| * exception was thrown. | ||
| */ | ||
| public class ChildRenderingError(private val message: String) : ScannableView() { | ||
| public class ChildRenderingError(private val message: String) : ScannableView { | ||
| override val displayName: String get() = message | ||
| override val children: Sequence<ScannableView> get() = emptySequence() | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also worth noting this break backward compatibility (as you can't switch over ScannableView anymore) but I don't expect any consumer to be impacted.
If we really really want to avoid that we could keep ScannableView as abstract class and make it implement an interface. But then I don't know how to name that interface
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's possible to add direct subclasses to a sealed class in any way without technically breaking compatibility. Any consuming bytecode that assumes it has handled all the cases will be wrong.
Technically I think this means we need to bump the major version number.