diff options
6 files changed, 224 insertions, 32 deletions
diff --git a/common/device-side/util-axt/Android.bp b/common/device-side/util-axt/Android.bp index 11f851b78dd..720a74055b0 100644 --- a/common/device-side/util-axt/Android.bp +++ b/common/device-side/util-axt/Android.bp @@ -33,6 +33,7 @@ java_library_static { "ub-uiautomator", "mockito-target-minus-junit4", "androidx.annotation_annotation", + "androidx.test.uiautomator_uiautomator", "truth-prebuilt", "modules-utils-build_system", ], diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils.java index a9543c246ab..a162b9c9626 100644 --- a/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils.java +++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils.java @@ -36,6 +36,9 @@ import androidx.test.core.app.ApplicationProvider; import java.util.regex.Pattern; +/** + * @deprecated , Use {@link UiAutomatorUtils2}, which uses latest androidx automator classes. + */ public class UiAutomatorUtils { private UiAutomatorUtils() {} diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils2.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils2.java new file mode 100644 index 00000000000..b677dbf3449 --- /dev/null +++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils2.java @@ -0,0 +1,167 @@ +package com.android.compatibility.common.util; +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import static org.junit.Assert.assertNotNull; + +import android.graphics.Rect; +import android.util.Log; +import android.util.TypedValue; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.BySelector; +import androidx.test.uiautomator.StaleObjectException; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.UiObjectNotFoundException; +import androidx.test.uiautomator.UiScrollable; +import androidx.test.uiautomator.UiSelector; +import androidx.test.uiautomator.Until; + +import java.util.regex.Pattern; + +public class UiAutomatorUtils2 { + private UiAutomatorUtils2() {} + + private static final String LOG_TAG = "UiAutomatorUtils"; + + /** Default swipe deadzone percentage. See {@link UiScrollable}. */ + private static final double DEFAULT_SWIPE_DEADZONE_PCT = 0.1; + + /** Minimum view height accepted (before needing to scroll more). */ + private static final float MIN_VIEW_HEIGHT_DP = 8; + + private static Pattern sCollapsingToolbarResPattern = + Pattern.compile(".*:id/collapsing_toolbar"); + + public static UiDevice getUiDevice() { + return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + } + + public static UiObject2 waitFindObject(BySelector selector) throws UiObjectNotFoundException { + return waitFindObject(selector, 20_000); + } + + public static UiObject2 waitFindObject(BySelector selector, long timeoutMs) + throws UiObjectNotFoundException { + final UiObject2 view = waitFindObjectOrNull(selector, timeoutMs); + ExceptionUtils.wrappingExceptions(UiDumpUtils::wrapWithUiDump, () -> { + assertNotNull("View not found after waiting for " + timeoutMs + "ms: " + selector, + view); + }); + return view; + } + + public static UiObject2 waitFindObjectOrNull(BySelector selector) + throws UiObjectNotFoundException { + return waitFindObjectOrNull(selector, 20_000); + } + + private static int convertDpToPx(float dp) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, + ApplicationProvider.getApplicationContext().getResources().getDisplayMetrics())); + } + + public static UiObject2 waitFindObjectOrNull(BySelector selector, long timeoutMs) + throws UiObjectNotFoundException { + UiObject2 view = null; + long start = System.currentTimeMillis(); + + boolean isAtEnd = false; + boolean wasScrolledUpAlready = false; + boolean scrolledPastCollapsibleToolbar = false; + + final int minViewHeightPx = convertDpToPx(MIN_VIEW_HEIGHT_DP); + + while (view == null && start + timeoutMs > System.currentTimeMillis()) { + try { + view = getUiDevice().wait(Until.findObject(selector), 1000); + } catch (StaleObjectException exception) { + // UiDevice.wait() may cause StaleObjectException if the {@link View} attached to + // UiObject2 is no longer in the view tree. + Log.v(LOG_TAG, "UiObject2 view is no longer in the view tree.", exception); + getUiDevice().waitForIdle(); + continue; + } + + if (view == null || view.getVisibleBounds().height() < minViewHeightPx) { + final double deadZone = !(FeatureUtil.isWatch() || FeatureUtil.isTV()) + ? 0.25 : DEFAULT_SWIPE_DEADZONE_PCT; + UiScrollable scrollable = new UiScrollable(new UiSelector().scrollable(true)); + scrollable.setSwipeDeadZonePercentage(deadZone); + if (scrollable.exists()) { + if (!scrolledPastCollapsibleToolbar) { + scrollPastCollapsibleToolbar(scrollable, deadZone); + scrolledPastCollapsibleToolbar = true; + continue; + } + if (isAtEnd) { + if (wasScrolledUpAlready) { + return null; + } + scrollable.scrollToBeginning(Integer.MAX_VALUE); + isAtEnd = false; + wasScrolledUpAlready = true; + scrolledPastCollapsibleToolbar = false; + } else { + Rect boundsBeforeScroll = scrollable.getBounds(); + boolean scrollAtStartOrEnd = !scrollable.scrollForward(); + // The scrollable view may no longer be scrollable after the toolbar is + // collapsed. + if (scrollable.exists()) { + Rect boundsAfterScroll = scrollable.getBounds(); + isAtEnd = scrollAtStartOrEnd && boundsBeforeScroll.equals( + boundsAfterScroll); + } else { + isAtEnd = scrollAtStartOrEnd; + } + } + } else { + // There might be a collapsing toolbar, but no scrollable view. Try to collapse + scrollPastCollapsibleToolbar(null, deadZone); + } + } + } + return view; + } + + private static void scrollPastCollapsibleToolbar(UiScrollable scrollable, double deadZone) + throws UiObjectNotFoundException { + final UiObject2 collapsingToolbar = getUiDevice().findObject( + By.res(sCollapsingToolbarResPattern)); + if (collapsingToolbar == null) { + return; + } + + final int steps = 55; // == UiScrollable.SCROLL_STEPS + if (scrollable != null && scrollable.exists()) { + final Rect scrollableBounds = scrollable.getVisibleBounds(); + final int distanceToSwipe = collapsingToolbar.getVisibleBounds().height() / 2; + getUiDevice().drag(scrollableBounds.centerX(), scrollableBounds.centerY(), + scrollableBounds.centerX(), scrollableBounds.centerY() - distanceToSwipe, + steps); + } else { + // There might be a collapsing toolbar, but no scrollable view. Try to collapse + int maxY = getUiDevice().getDisplayHeight(); + int minY = (int) (deadZone * maxY); + maxY -= minY; + getUiDevice().drag(0, maxY, 0, minY, steps); + } + } +} diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java index f5c6af5b4a0..36e889d30b8 100644 --- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java +++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java @@ -1245,7 +1245,7 @@ public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest { assertThat(fileInPodcastsDirLowerCase.createNewFile()).isTrue(); } finally { fileInPodcastsDirLowerCase.delete(); - deleteAsLegacyApp(podcastsDirLowerCase); + deleteRecursivelyAsLegacyApp(podcastsDirLowerCase); podcastsDir.mkdirs(); } } @@ -2660,8 +2660,8 @@ public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest { // We can't rename a non-top level directory to a top level directory. assertCantRenameDirectory(nonTopLevelDir, topLevelDir2, null); } finally { - deleteAsLegacyApp(topLevelDir1); - deleteAsLegacyApp(topLevelDir2); + deleteRecursivelyAsLegacyApp(topLevelDir1); + deleteRecursivelyAsLegacyApp(topLevelDir2); deleteRecursively(nonTopLevelDir); } } @@ -2671,7 +2671,7 @@ public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest { final File podcastsDir = getPodcastsDir(); try { if (podcastsDir.exists()) { - deleteAsLegacyApp(podcastsDir); + deleteRecursivelyAsLegacyApp(podcastsDir); } assertThat(podcastsDir.mkdir()).isTrue(); } finally { @@ -2693,7 +2693,7 @@ public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest { if (cameraDir.exists()) { // This is a work around to address a known inode cache inconsistency issue // that occurs when test runs for the second time. - deleteAsLegacyApp(cameraDir); + deleteRecursivelyAsLegacyApp(cameraDir); } createDirectoryAsLegacyApp(cameraDir); @@ -2717,7 +2717,7 @@ public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest { } finally { deleteWithMediaProviderNoThrow(targetUri); deleteAsLegacyApp(nomediaFile); - deleteAsLegacyApp(cameraDir); + deleteRecursivelyAsLegacyApp(cameraDir); } } @@ -2770,7 +2770,7 @@ public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest { } finally { deleteAsLegacyApp(videoFile); deleteAsLegacyApp(nomediaFile); - deleteAsLegacyApp(directory); + deleteRecursivelyAsLegacyApp(directory); } } diff --git a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java index 2e694ed65a7..79735361339 100644 --- a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java +++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java @@ -995,33 +995,23 @@ public class TestUtils { final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace( callingPackageName, otherApp.getPackageName())); final File file = new File(otherAppExternalDataDir, fileName); + String absolutePath = file.getAbsolutePath(); + + final ContentValues valuesWithRelativePath = new ContentValues(); + final String absoluteDirectoryPath = otherAppExternalDataDir.getAbsolutePath(); + valuesWithRelativePath.put(MediaStore.MediaColumns.RELATIVE_PATH, + absoluteDirectoryPath.substring(absoluteDirectoryPath.indexOf("Android"))); + valuesWithRelativePath.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); + try { assertThat(createFileAs(otherApp, file.getPath())).isTrue(); + assertCantInsertDataValue(throwsExceptionForDataValue, absolutePath); + assertCantInsertDataValue(throwsExceptionForDataValue, + "/sdcard/" + absolutePath.substring(absolutePath.indexOf("Android"))); + assertCantInsertDataValue(throwsExceptionForDataValue, + "/storage/emulated/0/Pictures/../" + + absolutePath.substring(absolutePath.indexOf("Android"))); - final ContentValues valuesWithData = new ContentValues(); - valuesWithData.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath()); - try { - Uri uri = getContentResolver().insert( - MediaStore.Files.getContentUri(VOLUME_EXTERNAL), - valuesWithData); - - if (throwsExceptionForDataValue) { - fail("File insert expected to fail: " + file); - } else { - try (Cursor c = getContentResolver().query(uri, new String[]{ - MediaStore.MediaColumns.DATA}, null, null)) { - assertThat(c.moveToFirst()).isTrue(); - assertThat(c.getString(0)).isNotEqualTo(file.getAbsolutePath()); - } - } - } catch (IllegalArgumentException expected) { - } - - final ContentValues valuesWithRelativePath = new ContentValues(); - final String path = file.getAbsolutePath(); - valuesWithRelativePath.put(MediaStore.MediaColumns.RELATIVE_PATH, - path.substring(path.indexOf("Android"))); - valuesWithRelativePath.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); try { getContentResolver().insert(MediaStore.Files.getContentUri(VOLUME_EXTERNAL), valuesWithRelativePath); @@ -1033,6 +1023,34 @@ public class TestUtils { } } + private static void assertCantInsertDataValue(boolean throwsExceptionForDataValue, + String path) throws Exception { + if (throwsExceptionForDataValue) { + assertThrowsErrorOnInsertToOtherAppPrivateDirectories(path); + } else { + insertDataWithValue(path); + try (Cursor c = getContentResolver().query( + MediaStore.Files.getContentUri(VOLUME_EXTERNAL), + new String[] {MediaStore.MediaColumns.DATA}, + MediaStore.MediaColumns.DATA + "=?", new String[] {path}, null)) { + assertThat(c.getCount()).isEqualTo(0); + } + } + } + + private static void assertThrowsErrorOnInsertToOtherAppPrivateDirectories(String path) + throws Exception { + assertThrows(IllegalArgumentException.class, () -> insertDataWithValue(path)); + } + + private static void insertDataWithValue(String path) { + final ContentValues valuesWithData = new ContentValues(); + valuesWithData.put(MediaStore.MediaColumns.DATA, path); + + getContentResolver().insert(MediaStore.Files.getContentUri(VOLUME_EXTERNAL), + valuesWithData); + } + /** * Assert that app cannot update files in other app's private directories * diff --git a/tests/app/Android.bp b/tests/app/Android.bp index 2a9a925276c..11bc2047c3f 100644 --- a/tests/app/Android.bp +++ b/tests/app/Android.bp @@ -51,12 +51,15 @@ android_test { ], instrumentation_for: "CtsAppTestStubs", sdk_version: "test_current", - min_sdk_version: "14", + // 21 required for multi-dex. + min_sdk_version: "21", // Disable coverage since it pushes us over the dex limit and we don't // actually need to measure the tests themselves. jacoco: { exclude_filter: ["**"], }, + // Even with coverage disabled, we're close to the single dex limit, so allow use of multi-dex. + dxflags: ["--multi-dex"], data: [ ":CtsSimpleApp", ":CtsAppTestStubs", |