Skip to content

Commit b834c3d

Browse files
Hyung Tae KimGerrit Code Review
authored andcommitted
Merge "uiautomator: Desktop multi-window support #2" into androidx-main
2 parents 9a36414 + 19b2f85 commit b834c3d

2 files changed

Lines changed: 85 additions & 2 deletions

File tree

  • test/uiautomator
    • integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp
    • uiautomator/src/main/java/androidx/test/uiautomator

test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiWindowTest.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import static org.junit.Assert.assertNotNull;
2626
import static org.junit.Assert.assertNull;
2727
import static org.junit.Assert.assertTrue;
28+
import static org.junit.Assert.fail;
2829
import static org.junit.Assume.assumeTrue;
2930

3031
import android.app.Activity;
3132
import android.content.Intent;
3233

3334
import androidx.test.filters.LargeTest;
3435
import androidx.test.uiautomator.By;
36+
import androidx.test.uiautomator.StaleObjectException;
3537
import androidx.test.uiautomator.UiObject2;
3638
import androidx.test.uiautomator.UiWindow;
3739

@@ -127,6 +129,7 @@ public void testUiDevice_closeNonActiveDesktopWindow() {
127129
UiWindow nonActiveWindow = appWindows.get(1);
128130
assertNotNull(nonActiveWindow);
129131
assertFalse(nonActiveWindow.isActive());
132+
int nonActiveWindowId = nonActiveWindow.getId();
130133

131134
List<UiWindow> windows =
132135
mDevice.findWindows(
@@ -147,7 +150,7 @@ public void testUiDevice_closeNonActiveDesktopWindow() {
147150
// Wait for the app window to no longer be present, identified by its unique window ID.
148151
assertTrue(
149152
"Window should no longer be present if the close button is clicked.",
150-
mDevice.wait(d -> !d.hasWindow(By.Window.id(nonActiveWindow.getId())), TIMEOUT_MS));
153+
mDevice.wait(d -> !d.hasWindow(By.Window.id(nonActiveWindowId)), TIMEOUT_MS));
151154
}
152155

153156
@Test
@@ -244,4 +247,22 @@ public void testUiWindow_getRootObjectAndPackageName() {
244247
assertEquals("com.android.systemui", root.getApplicationPackage());
245248
}
246249
}
250+
251+
@Test(expected = StaleObjectException.class)
252+
public void testUiWindow_staleWindowException() {
253+
// Get a window reference.
254+
UiWindow window = mDevice.findWindow(By.Window.pkg(TEST_APP).active(true));
255+
assertNotNull(window);
256+
257+
// Close the activity to make the window stale
258+
UiObject2 button = mDevice.findObject(By.res(TEST_APP, "close_task_window_button"));
259+
assertNotNull(button);
260+
button.click();
261+
mDevice.wait(d -> !d.hasWindow(By.Window.pkg(TEST_APP)), TIMEOUT_MS);
262+
263+
// If the window is gone and stale, accessing it will throw a StaleObjectException.
264+
window.getRootObject();
265+
fail("Should not reach here. Expected exception was not caught when accessing stale "
266+
+ "window");
267+
}
247268
}

test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiWindow.java

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,72 @@ private AccessibilityWindowInfo getAccessibilityWindowInfo() {
147147
if (mCachedWindow == null) {
148148
throw new IllegalStateException("This window has already been recycled.");
149149
}
150-
// TODO(b/405371739): Implement the window staleness check when refresh() is accessible.
150+
151+
getDevice().waitForIdle();
152+
if (!refresh()) {
153+
getDevice().runWatchers();
154+
if (!refresh()) {
155+
throw new StaleObjectException();
156+
}
157+
}
151158
return mCachedWindow;
152159
}
153160

161+
/**
162+
* Refreshes this window's state if it is stale.
163+
*
164+
* <p>Ideally, this would call the hidden {@code AccessibilityWindowInfo.refresh()} method
165+
* for an in-place update. Since it is unavailable, for older API levels, it uses a fallback
166+
* ({@link #refreshFromPool()}) that finds the window info from the refreshed root node.
167+
*
168+
* @return {@code true} if the refresh succeeded, {@code false} if this window is stale and
169+
* no longer exists.
170+
*/
171+
private boolean refresh() {
172+
// TODO(b/405371739): Extend this to handle different API level behaviors when the
173+
// refresh API is available in the future SDK.
174+
return refreshFromPool();
175+
}
176+
177+
/**
178+
* Refreshes this window by replacing it with an updated instance from the pool.
179+
*
180+
* <p>This is a fallback for SDKs where the refresh system API is unavailable. To refresh the
181+
* info, it returns the old cached window to the pool, then obtains a new one (which may or
182+
* may not be the one just recycled) from the pool based on the refreshed root node.
183+
* <p>Note: This will effectively update the window but not in-place, so the underlying cached
184+
* window may reference a new object each time this method is called.
185+
*/
186+
private boolean refreshFromPool() {
187+
AccessibilityNodeInfo rootNode = mCachedWindow.getRoot();
188+
if (rootNode == null) {
189+
return false;
190+
}
191+
AccessibilityWindowInfo refreshedWindow = null;
192+
try {
193+
if (!rootNode.refresh()) {
194+
return false;
195+
}
196+
refreshedWindow = rootNode.getWindow();
197+
if (refreshedWindow == null || refreshedWindow.getId() != mCachedWindow.getId()) {
198+
return false;
199+
}
200+
// This is a best effort to keep the cached window referencing the same object from
201+
// the pool by recycling the old info then immediately acquiring it back for the
202+
// refreshed info.
203+
mCachedWindow.recycle();
204+
mCachedWindow = AccessibilityWindowInfo.obtain(refreshedWindow);
205+
return true;
206+
} finally {
207+
if (rootNode != null) {
208+
rootNode.recycle();
209+
}
210+
if (refreshedWindow != null) {
211+
refreshedWindow.recycle();
212+
}
213+
}
214+
}
215+
154216
/** Recycles this window. */
155217
private void recycle() {
156218
mCachedWindow.recycle();

0 commit comments

Comments
 (0)