Skip to content
Merged
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 @@ -30,17 +30,30 @@ static AccessibilityNodeInfo root(AccessibilityWindowInfo window) {
return window.getRoot();
}

interface Read<T> {
T read();
}

/**
* The child, or null when it is gone. When the child's window closes during the read, the
* framework can throw a NullPointerException from its result integrity check instead of answering
* null (seen on API 35 while an alert dialog closed), so that throw means the same as null.
*
* <p>A null {@code node} is a programmer error, but the same catch hides its NullPointerException
* as a gone child.
*/
static AccessibilityNodeInfo child(AccessibilityNodeInfo node, int index) {
return nullIfGone(
() ->
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
? node.getChild(index, PREFETCH_STRATEGY)
: node.getChild(index));
}

/** Runs a framework read that answers null for a vanished node and can throw instead. */
static <T> T nullIfGone(Read<T> read) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return node.getChild(index, PREFETCH_STRATEGY);
}
return node.getChild(index);
return read.read();
} catch (NullPointerException vanished) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ static void run() throws Exception {
releasesUnreadResultsAfterFailure();
waitsForReadersBeforeReleasingTheParent();
preservesInterruptionDuringCleanup();
readsAChildThatVanishedMidReadAsGone();
}

private static void queuesConcurrentReadsAndPreservesOrder() throws Exception {
Expand Down Expand Up @@ -134,6 +135,41 @@ private static void preservesInterruptionDuringCleanup() {
}
}

private static void readsAChildThatVanishedMidReadAsGone() {
List<Integer> recycled = new ArrayList<>();
try (AccessibilityReadBatch<Integer> batch =
new AccessibilityReadBatch<>(
0, 3,
index ->
AccessibilityNodeReads.nullIfGone(
() -> {
if (index == 1) {
throw new NullPointerException("window closed");
}
if (index == 2) {
throw new IllegalStateException("not a vanished child");
}
return index;
}),
recycled::add)) {
if (batch.get(0) != 0) {
throw new AssertionError("Expected the first child");
}
if (batch.get(1) != null) {
throw new AssertionError("A child that vanished mid-read must read as gone");
}
try {
batch.get(2);
throw new AssertionError("Other read failures must propagate");
} catch (IllegalStateException expected) {
// Only the vanished-window throw means a gone child.
}
}
if (!recycled.equals(Arrays.asList(0))) {
throw new AssertionError("Only the present child must be released: " + recycled);
}
}

private static void await(CountDownLatch latch) {
try {
if (!latch.await(2, TimeUnit.SECONDS)) {
Expand Down
Loading