diff --git a/BUILD.bazel b/BUILD.bazel index 7202301f9..eed92289c 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -32,6 +32,7 @@ maven_repository( "//runner/monitor/java/androidx/test:monitor_maven_artifact", "//runner/rules/java/androidx/test:rules_maven_artifact", "//services:test_services_maven_artifact", + "//services/storage/java/androidx/test/services/storage:test_storage_maven_artifact", ], ) diff --git a/WORKSPACE b/WORKSPACE index 8529790ba..00a273bb7 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -58,6 +58,7 @@ maven_install( name = "maven", artifacts = [ "androidx.annotation:annotation:" + ANDROIDX_VERSION, + "androidx.annotation:annotation-experimental:jar:" + ANDROIDX_VERSION, "androidx.appcompat:appcompat:" + ANDROIDX_VERSION, "androidx.core:core:" + ANDROIDX_VERSION, "androidx.cursoradapter:cursoradapter:" + ANDROIDX_VERSION, @@ -139,4 +140,12 @@ http_archive( ) load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kotlin_repositories", "kt_register_toolchains") kotlin_repositories() -kt_register_toolchains() \ No newline at end of file +kt_register_toolchains() + +# Android bazel rules +http_archive( + name = "build_bazel_rules_android", + urls = ["https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"], + sha256 = "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", + strip_prefix = "rules_android-0.1.1", +) \ No newline at end of file diff --git a/build_extensions/android_app_instrumentation_tests.bzl b/build_extensions/android_app_instrumentation_tests.bzl index 672c49777..bf2db94d7 100644 --- a/build_extensions/android_app_instrumentation_tests.bzl +++ b/build_extensions/android_app_instrumentation_tests.bzl @@ -9,7 +9,7 @@ load( "infer_android_package_name", ) -def android_app_instrumentation_tests(name, binary_target, srcs, deps, target_devices, custom_package = None, **kwargs): +def android_app_instrumentation_tests(name, binary_target, srcs, deps, target_devices, custom_package = None, manifest_values = {}, **kwargs): """A rule for an instrumentation test whose target under test is an android_binary. The intent of this wrapper is to simplify the build API for creating instrumentation test rules @@ -30,6 +30,7 @@ def android_app_instrumentation_tests(name, binary_target, srcs, deps, target_de deps: the build dependencies to use for the generated test binary target_devices: array of device targets to execute on custom_package: Optional. Package name of the library. It could be inferred if unset + manifest_values: Optional. A dictionary of values to be overridden in the manifest **kwargs: arguments to pass to generated android_instrumentation_test rules """ library_name = name @@ -43,14 +44,17 @@ def android_app_instrumentation_tests(name, binary_target, srcs, deps, target_de testonly = 1, deps = deps, ) + + _manifest_values = { + "applicationId": android_package_name + ".tests", + "instrumentationTargetPackage": android_package_name, + } + _manifest_values.update(manifest_values) native.android_binary( name = "%s_binary" % library_name, instruments = binary_target, manifest = "//build_extensions:AndroidManifest_instrumentation_test_template.xml", - manifest_values = { - "applicationId": android_package_name + ".tests", - "instrumentationTargetPackage": android_package_name, - }, + manifest_values = _manifest_values, testonly = 1, deps = [name], ) diff --git a/build_extensions/android_library_instrumentation_tests.bzl b/build_extensions/android_library_instrumentation_tests.bzl index 6bc4a33ef..a6beb5e56 100644 --- a/build_extensions/android_library_instrumentation_tests.bzl +++ b/build_extensions/android_library_instrumentation_tests.bzl @@ -9,7 +9,7 @@ load( "infer_android_package_name", ) -def android_library_instrumentation_tests(name, srcs, deps, target_devices, custom_package = None, nocompress_extensions = None, **kwargs): +def android_library_instrumentation_tests(name, srcs, deps, target_devices, custom_package = None, nocompress_extensions = None, manifest_values = {}, **kwargs): """A rule for an instrumentation test whose target under test is an android_library. Will generate a 'self-instrumentating' test binary and other associated rules @@ -33,6 +33,7 @@ def android_library_instrumentation_tests(name, srcs, deps, target_devices, cust target_devices: array of device targets to execute on custom_package: Optional. Package name of the library. It could be inferred if unset nocompress_extensions: Optional. A list of file extensions to leave uncompressed in the resource apk. + manifest_values: Optional. A dictionary of values to be overridden in the manifest **kwargs: arguments to pass to generated android_instrumentation_test rules """ library_name = name @@ -52,16 +53,20 @@ def android_library_instrumentation_tests(name, srcs, deps, target_devices, cust testonly = 1, deps = deps, ) + + _manifest_values = { + "applicationId": android_package_name, + "instrumentationTargetPackage": android_package_name, + } + _manifest_values.update(manifest_values) native.android_binary( name = "%s_binary" % library_name, instruments = ":target_stub_binary", manifest = "//build_extensions:AndroidManifest_instrumentation_test_template.xml", - manifest_values = { - "applicationId": android_package_name, - "instrumentationTargetPackage": android_package_name, - }, + manifest_values = _manifest_values, nocompress_extensions = nocompress_extensions, testonly = 1, + multidex = kwargs.pop("multidex", "off"), deps = [name], ) android_multidevice_instrumentation_test( diff --git a/build_extensions/axt_versions.bzl b/build_extensions/axt_versions.bzl index e4c6fe2ac..94e888fe7 100644 --- a/build_extensions/axt_versions.bzl +++ b/build_extensions/axt_versions.bzl @@ -4,13 +4,14 @@ Ensure UsageTrackerRegistry is updated accordingly when incrementing version num """ # AXT versions -RUNNER_VERSION = "1.3.0-alpha03" -ESPRESSO_VERSION = "3.3.0-alpha03" -CORE_VERSION = "1.3.0-alpha03" -ANDROIDX_JUNIT_VERSION = "1.1.2-alpha03" -ANDROIDX_TRUTH_VERSION = "1.3.0-alpha03" +RUNNER_VERSION = "1.3.0-alpha04" +ESPRESSO_VERSION = "3.3.0-alpha04" +CORE_VERSION = "1.3.0-alpha04" +ANDROIDX_JUNIT_VERSION = "1.1.2-alpha04" +ANDROIDX_TRUTH_VERSION = "1.3.0-alpha04" UIAUTOMATOR_VERSION = "2.2.0" JANK_VERSION = "1.0.1" +SERVICES_VERSION = RUNNER_VERSION # Maven dependency versions ANDROIDX_VERSION = "1.0.0" diff --git a/espresso/core/java/androidx/test/espresso/base/IdlingResourceRegistry.java b/espresso/core/java/androidx/test/espresso/base/IdlingResourceRegistry.java index 27667c41d..0e3ca7ac0 100644 --- a/espresso/core/java/androidx/test/espresso/base/IdlingResourceRegistry.java +++ b/espresso/core/java/androidx/test/espresso/base/IdlingResourceRegistry.java @@ -272,7 +272,7 @@ boolean allResourcesAreIdle() { return false; } } - Log.i(TAG, "All idling resources are idle."); + Log.d(TAG, "All idling resources are idle."); return true; } diff --git a/espresso/core/java/androidx/test/espresso/base/Interrogator.java b/espresso/core/java/androidx/test/espresso/base/Interrogator.java index b91a33f96..92426f377 100644 --- a/espresso/core/java/androidx/test/espresso/base/Interrogator.java +++ b/espresso/core/java/androidx/test/espresso/base/Interrogator.java @@ -116,7 +116,7 @@ interface InterrogationHandler extends QueueInterrogationHandler { /** Called when the looper / message queue being interrogated is about to quit. */ public void quitting(); - public void setMessage(String m); + public void setMessage(Message m); public String getMessage(); } @@ -151,7 +151,7 @@ static R loopAndInterrogate(InterrogationHandler handler) { return handler.get(); } stillInterested = handler.beforeTaskDispatch(); - handler.setMessage(m.toString()); + handler.setMessage(m); m.getTarget().dispatchMessage(m); // ensure looper invariants diff --git a/espresso/core/java/androidx/test/espresso/base/LooperIdlingResourceInterrogationHandler.java b/espresso/core/java/androidx/test/espresso/base/LooperIdlingResourceInterrogationHandler.java index fc914841b..8a3149549 100644 --- a/espresso/core/java/androidx/test/espresso/base/LooperIdlingResourceInterrogationHandler.java +++ b/espresso/core/java/androidx/test/espresso/base/LooperIdlingResourceInterrogationHandler.java @@ -18,6 +18,7 @@ import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.os.MessageQueue; import androidx.test.espresso.IdlingResource; import java.util.Locale; @@ -106,7 +107,7 @@ public void run() { } @Override - public void setMessage(String m) {} + public void setMessage(Message m) {} @Override public String getMessage() { diff --git a/espresso/core/java/androidx/test/espresso/base/UiControllerImpl.java b/espresso/core/java/androidx/test/espresso/base/UiControllerImpl.java index 3b7e9d245..5ac42f7e9 100644 --- a/espresso/core/java/androidx/test/espresso/base/UiControllerImpl.java +++ b/espresso/core/java/androidx/test/espresso/base/UiControllerImpl.java @@ -594,8 +594,14 @@ private static final class MainThreadInterrogation } @Override - public void setMessage(String m) { - lastMessage = m; + public void setMessage(Message m) { + try { + lastMessage = m.toString(); + } catch (NullPointerException npe) { + // toString can fail with an NPE on getClass() + // This field is just for diagnosing Espresso test failures; suppress the error. + lastMessage = "NPE calling message toString(): " + npe; + } } @Override diff --git a/ext/junit/java/androidx/test/ext/junit/runners/AndroidJUnit4.java b/ext/junit/java/androidx/test/ext/junit/runners/AndroidJUnit4.java index cc170adfd..59f20163d 100644 --- a/ext/junit/java/androidx/test/ext/junit/runners/AndroidJUnit4.java +++ b/ext/junit/java/androidx/test/ext/junit/runners/AndroidJUnit4.java @@ -16,7 +16,9 @@ package androidx.test.ext.junit.runners; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.util.List; import org.junit.runner.Description; import org.junit.runner.Runner; import org.junit.runner.manipulation.Filter; @@ -46,11 +48,6 @@ public AndroidJUnit4(Class klass) throws InitializationError { delegate = loadRunner(klass); } - private static Runner loadRunner(Class testClass) throws InitializationError { - String runnerClassName = getRunnerClassName(); - return loadRunner(testClass, runnerClassName); - } - private static String getRunnerClassName() { String runnerClassName = System.getProperty("android.junit.runner", null); if (runnerClassName == null) { @@ -64,34 +61,79 @@ private static String getRunnerClassName() { return runnerClassName; } - private static Runner loadRunner(Class testClass, String className) + private static Runner loadRunner(Class testClass) throws InitializationError { + String runnerClassName = getRunnerClassName(); + return loadRunner(testClass, runnerClassName); + } + + @SuppressWarnings("unchecked") + private static Runner loadRunner(Class testClass, String runnerClassName) throws InitializationError { + + Class runnerClass = null; try { - @SuppressWarnings("unchecked") - Class runnerClass = (Class) Class.forName(className); - return runnerClass.getConstructor(Class.class).newInstance(testClass); + runnerClass = (Class) Class.forName(runnerClassName); } catch (ClassNotFoundException e) { - throwInitializationError(className, e); + throwInitializationError( + String.format( + "Delegate runner %s for AndroidJUnit4 could not be found.\n", runnerClassName), + e); + } + + Constructor constructor = null; + try { + constructor = runnerClass.getConstructor(Class.class); } catch (NoSuchMethodException e) { - throwInitializationError(className, e); + throwInitializationError( + String.format( + "Delegate runner %s for AndroidJUnit4 requires a public constructor that takes a" + + " Class.\n", + runnerClassName), + e); + } + + try { + return constructor.newInstance(testClass); } catch (IllegalAccessException e) { - throwInitializationError(className, e); + throwInitializationError( + String.format("Illegal constructor access for test runner %s\n", runnerClassName), e); } catch (InstantiationException e) { - throwInitializationError(className, e); + throwInitializationError( + String.format("Failed to instantiate test runner %s\n", runnerClassName), e); } catch (InvocationTargetException e) { - throwInitializationError(className, e); + String details = getInitializationErrorDetails(e, testClass); + throwInitializationError( + String.format("Failed to instantiate test runner %s\n%s\n", runnerClass, details), e); } throw new IllegalStateException("Should never reach here"); } - private static void throwInitializationError(String delegateRunner, Throwable cause) + private static void throwInitializationError(String details, Throwable cause) throws InitializationError { - // wrap the cause in a RuntimeException with a more detailed error message - throw new InitializationError( - new RuntimeException( - String.format( - "Delegate runner '%s' for AndroidJUnit4 could not be loaded.", delegateRunner), - cause)); + throw new InitializationError(new RuntimeException(details, cause)); + } + + private static String getInitializationErrorDetails(Throwable throwable, Class testClass) { + StringBuilder innerCause = new StringBuilder(); + final Throwable cause = throwable.getCause(); + + if (cause == null) { + return ""; + } + + final Class causeClass = cause.getClass(); + if (causeClass == InitializationError.class) { + final InitializationError initializationError = (InitializationError) cause; + final List testClassProblemList = initializationError.getCauses(); + innerCause.append( + String.format( + "Test class %s is malformed. (%s problems):\n", + testClass, testClassProblemList.size())); + for (Throwable testClassProblem : testClassProblemList) { + innerCause.append(testClassProblem).append("\n"); + } + } + return innerCause.toString(); } @Override diff --git a/runner/android_junit_runner/java/androidx/test/internal/runner/tracker/UsageTrackerRegistry.java b/runner/android_junit_runner/java/androidx/test/internal/runner/tracker/UsageTrackerRegistry.java index 00c5c21d8..ad9151713 100644 --- a/runner/android_junit_runner/java/androidx/test/internal/runner/tracker/UsageTrackerRegistry.java +++ b/runner/android_junit_runner/java/androidx/test/internal/runner/tracker/UsageTrackerRegistry.java @@ -28,9 +28,11 @@ public final class UsageTrackerRegistry { /** Contains versions for AXT libraries */ public interface AxtVersions { // Espresso version includes: Espresso, Espresso-Web, Intents, Espresso-MPE - String ESPRESSO_VERSION = "3.3.0-alpha03"; + String ESPRESSO_VERSION = "3.3.0-alpha04"; // Runner version includes: Runner, Rules, ATO, Monitor - String RUNNER_VERSION = "1.3.0-alpha03"; + String RUNNER_VERSION = "1.3.0-alpha04"; + // Test services version. + String SERVICES_VERSION = RUNNER_VERSION; } diff --git a/runner/android_junit_runner/java/androidx/test/orchestrator/instrumentationlistener/OrchestratedInstrumentationListener.java b/runner/android_junit_runner/java/androidx/test/orchestrator/instrumentationlistener/OrchestratedInstrumentationListener.java index 7fc4499dd..079b2def3 100644 --- a/runner/android_junit_runner/java/androidx/test/orchestrator/instrumentationlistener/OrchestratedInstrumentationListener.java +++ b/runner/android_junit_runner/java/androidx/test/orchestrator/instrumentationlistener/OrchestratedInstrumentationListener.java @@ -116,6 +116,8 @@ public void testRunFinished(Result result) { @Override public void testStarted(Description description) { + testFinishedCondition.close(); + isTestFailed.set(false); this.description = description; // Caches the test description in case of a crash try { sendTestNotification( @@ -144,7 +146,12 @@ public void testFailure(Failure failure) { // We'd like to make sure only one failure gets sent so that the isTestFailed variable is // checked and set without possibly racing between two thread calls. if (isTestFailed.compareAndSet(false, true)) { - Log.d(TAG, "Sending TestFailure event " + failure.getException().getMessage()); + if (Description.TEST_MECHANISM.equals(failure.getDescription())) { + // If an internal test runner exception occurred, the Description is "Test mechanism", + // so replace it with the test description previously received in testStarted(). + failure = new Failure(description, failure.getException()); + } + Log.d(TAG, "Sending TestFailure event: " + failure.getException().getMessage()); try { sendTestNotification( TestEvent.TEST_FAILURE, BundleJUnitUtils.getBundleFromFailure(failure)); diff --git a/runner/android_junit_runner/javatests/androidx/test/internal/runner/listener/SuiteAssignmentPrinterTest.java b/runner/android_junit_runner/javatests/androidx/test/internal/runner/listener/SuiteAssignmentPrinterTest.java index eae1f5293..125a0f745 100644 --- a/runner/android_junit_runner/javatests/androidx/test/internal/runner/listener/SuiteAssignmentPrinterTest.java +++ b/runner/android_junit_runner/javatests/androidx/test/internal/runner/listener/SuiteAssignmentPrinterTest.java @@ -18,8 +18,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Matchers.contains; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/runner/android_junit_runner/javatests/androidx/test/orchestrator/instrumentationlistener/OrchestratedInstrumentationListenerTest.java b/runner/android_junit_runner/javatests/androidx/test/orchestrator/instrumentationlistener/OrchestratedInstrumentationListenerTest.java index 394047d00..6cea98936 100644 --- a/runner/android_junit_runner/javatests/androidx/test/orchestrator/instrumentationlistener/OrchestratedInstrumentationListenerTest.java +++ b/runner/android_junit_runner/javatests/androidx/test/orchestrator/instrumentationlistener/OrchestratedInstrumentationListenerTest.java @@ -20,6 +20,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.os.Bundle; @@ -119,6 +121,15 @@ public void testStarted() throws RemoteException { assertFalse(listener.isTestFailed()); } + @Test + public void testStarted_resetsIsTestFailed() throws RemoteException { + listener.testFailure(jUnitFailure); + assertTrue(listener.isTestFailed()); + + listener.testStarted(jUnitDescription); + assertFalse(listener.isTestFailed()); + } + @Test public void testFinished() throws RemoteException { listener.testFinished(jUnitDescription); @@ -141,6 +152,27 @@ public void testFailure() throws RemoteException { assertTrue(listener.isTestFailed()); } + @Test + public void testFailure_calledTwice_onlyOneNotificationSent() throws RemoteException { + listener.testFailure(jUnitFailure); + listener.testFailure(jUnitFailure); + verify(mockCallback, times(1)).sendTestNotification(any()); + } + + @Test + public void testFailure_testMechanismFailure_useCachedTestDescription() throws RemoteException { + listener.testStarted(jUnitDescription); // Cache the description... + + Failure jUnitInternalFailure = + new Failure(Description.TEST_MECHANISM, jUnitFailure.getException()); + ArgumentCaptor argument = ArgumentCaptor.forClass(Bundle.class); + listener.testFailure(jUnitInternalFailure); + verify(mockCallback, times(2)).sendTestNotification(argument.capture()); + + ParcelableFailure failure = BundleJUnitUtils.getFailure(argument.getValue()); + compareFailure(failure, jUnitFailure); + } + @Test public void testAssumptionFailure() throws RemoteException { listener.testAssumptionFailure(jUnitFailure); diff --git a/runner/android_junit_runner/javatests/androidx/test/runner/UsageTrackerFacilitatorTest.java b/runner/android_junit_runner/javatests/androidx/test/runner/UsageTrackerFacilitatorTest.java index 45788a096..baaa8db09 100644 --- a/runner/android_junit_runner/javatests/androidx/test/runner/UsageTrackerFacilitatorTest.java +++ b/runner/android_junit_runner/javatests/androidx/test/runner/UsageTrackerFacilitatorTest.java @@ -18,7 +18,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; diff --git a/runner/monitor/javatests/androidx/test/internal/runner/InstrumentationConnectionTest.java b/runner/monitor/javatests/androidx/test/internal/runner/InstrumentationConnectionTest.java index 7edc62e14..4e046479f 100644 --- a/runner/monitor/javatests/androidx/test/internal/runner/InstrumentationConnectionTest.java +++ b/runner/monitor/javatests/androidx/test/internal/runner/InstrumentationConnectionTest.java @@ -24,7 +24,7 @@ import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; diff --git a/runner/rules/javatests/androidx/test/rule/ActivityTestRuleTest.java b/runner/rules/javatests/androidx/test/rule/ActivityTestRuleTest.java index 3b8960fd6..39ef14054 100644 --- a/runner/rules/javatests/androidx/test/rule/ActivityTestRuleTest.java +++ b/runner/rules/javatests/androidx/test/rule/ActivityTestRuleTest.java @@ -27,7 +27,7 @@ import static org.junit.Assert.fail; import static org.junit.runner.JUnitCore.runClasses; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/services/AndroidManifest.xml b/services/AndroidManifest.xml index 247e665d7..8dcdf469d 100644 --- a/services/AndroidManifest.xml +++ b/services/AndroidManifest.xml @@ -25,6 +25,8 @@ + + + + + + + diff --git a/services/BUILD.bazel b/services/BUILD.bazel index 9fcf835cb..775a60211 100644 --- a/services/BUILD.bazel +++ b/services/BUILD.bazel @@ -25,11 +25,12 @@ android_binary( "//services/shellexecutor:exec_server", "//services/speakeasy/java/androidx/test/services/speakeasy:protocol", "//services/speakeasy/java/androidx/test/services/speakeasy/server", + "//services/storage/java/androidx/test/services/storage/provider:storage_content_providers", ], ) load("//build_extensions:maven_repo.bzl", "maven_artifact") -load("//build_extensions:axt_versions.bzl", "RUNNER_VERSION") +load("//build_extensions:axt_versions.bzl", "SERVICES_VERSION") load("//build_extensions:combine_jars.bzl", "combine_jars") combine_jars( @@ -38,6 +39,7 @@ combine_jars( "//services/shellexecutor:shellexecuter_src", "//services/speakeasy/java/androidx/test/services/speakeasy:libprotocol-src.jar", "//services/speakeasy/java/androidx/test/services/speakeasy/server:libspeak_easy_service-src.jar", + "//services/storage/java/androidx/test/services/storage/provider:libstorage_content_providers-src.jar", ], ) @@ -46,7 +48,7 @@ maven_artifact( src = ":test_services.apk", artifact_id = "test-services", group_id = "androidx.test.services", - last_updated = "20170622000000", + last_updated = "20191210000000", src_jar = ":test_services_jars.jar", - version = "%s" % RUNNER_VERSION, + version = "%s" % SERVICES_VERSION, ) diff --git a/services/proguard_library.cfg b/services/proguard_library.cfg index 521adce46..326afd656 100644 --- a/services/proguard_library.cfg +++ b/services/proguard_library.cfg @@ -9,3 +9,13 @@ -dontwarn java.lang.instrument.** -dontwarn java.lang.management.** -dontwarn javax.management.** + +# Ignore missing Kotlin meta-annotations so that Java-only projects can depend +# on projects that happen to be written in Kotlin but do not have a run-time +# dependency on the Kotlin standard library. Note these annotations are RUNTIME +# retention, but we won't need them available in Java-only projects. +-dontwarn kotlin.Metadata +-dontwarn kotlin.annotation.AnnotationRetention +-dontwarn kotlin.annotation.AnnotationTarget +-dontwarn kotlin.annotation.Retention +-dontwarn kotlin.annotation.Target \ No newline at end of file diff --git a/services/storage/java/androidx/test/services/storage/AndroidManifest.xml b/services/storage/java/androidx/test/services/storage/AndroidManifest.xml new file mode 100644 index 000000000..e559602c9 --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/services/storage/java/androidx/test/services/storage/BUILD.bazel b/services/storage/java/androidx/test/services/storage/BUILD.bazel new file mode 100644 index 000000000..42d4c866a --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/BUILD.bazel @@ -0,0 +1,86 @@ +# Description: +# Exposes sd card storage to tests regardless of permissions. +load("@build_bazel_rules_android//android:rules.bzl", "android_library") +load("//build_extensions:maven_repo.bzl", "maven_artifact") +load("//build_extensions:release.bzl", "axt_release_lib") +load("//build_extensions:axt_versions.bzl", "RUNNER_VERSION", "SERVICES_VERSION") + +package( + default_visibility = [ + "//visibility:public", + ], + features = ["-android_resources_strict_deps"], +) + +licenses(["notice"]) + +java_library( + name = "experimental_storage_annotation", + srcs = ["ExperimentalTestStorage.java"], + deps = [ + "@maven//:androidx_annotation_annotation_experimental", + ], +) + +android_library( + name = "storage", + srcs = [ + "TestStorage.java", + "TestStorageException.java", + ], + deps = [ + ":experimental_storage_annotation", + "//runner/android_junit_runner", + "//services/storage/java/androidx/test/services/storage/file", + "@maven//:com_google_code_findbugs_jsr305", + ], +) + +# Constants shared between on-device (android) and host-side (java code) testing +# infrastructure for the storage service. +java_library( + name = "test_storage_constants", + srcs = ["TestStorageConstants.java"], + deps = [ + ":experimental_storage_annotation", + ], +) + +proto_library( + name = "storage_service_pb", + srcs = ["test_storage_service.proto"], +) + +java_lite_proto_library( + name = "storage_service_pb_java_proto_lite", + strict_deps = 0, + deps = [":storage_service_pb"], +) + +java_proto_library( + name = "storage_service_pb_java_proto", + deps = [":storage_service_pb"], +) + +# Generate release artifacts for the test storage. +axt_release_lib( + name = "test_storage_release", + keep_spec = "androidx/test/services/storage", + deps = [ + ":storage", + ], +) + +maven_artifact( + name = "test_storage_maven_artifact", + src = ":test_storage_release.aar", + artifact_deps = [ + "androidx.test:runner:%s" % RUNNER_VERSION, + "com.google.code.findbugs:jsr305:2.0.1", + ], + artifact_id = "storage", + group_id = "androidx.test.services", + last_updated = "2020020700000", + src_jar = ":libstorage-src.jar", + version = "%s" % SERVICES_VERSION, +) diff --git a/services/storage/java/androidx/test/services/storage/ExperimentalTestStorage.java b/services/storage/java/androidx/test/services/storage/ExperimentalTestStorage.java new file mode 100644 index 000000000..55ccfeb2c --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/ExperimentalTestStorage.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; + +import androidx.annotation.experimental.Experimental; +import androidx.annotation.experimental.Experimental.Level; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Annotation denoting this test storage library is experimental. */ +@Retention(RetentionPolicy.CLASS) +@Target({TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE}) +@Experimental(level = Level.ERROR) +public @interface ExperimentalTestStorage {} diff --git a/services/storage/java/androidx/test/services/storage/TestStorage.java b/services/storage/java/androidx/test/services/storage/TestStorage.java new file mode 100644 index 000000000..107acefed --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/TestStorage.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage; + +import static androidx.test.internal.util.Checks.checkNotNull; + +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; +import androidx.test.internal.runner.tracker.UsageTrackerRegistry; +import androidx.test.internal.runner.tracker.UsageTrackerRegistry.AxtVersions; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.services.storage.file.HostedFile; +import androidx.test.services.storage.file.PropertyFile; +import androidx.test.services.storage.file.PropertyFile.Authority; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * Provides convenient I/O operations for reading/writing testing relevant files, properties in a + * test. + */ +@ExperimentalTestStorage +public final class TestStorage { + static { + UsageTrackerRegistry.getInstance() + .trackUsage("Test Storage Service-API", AxtVersions.SERVICES_VERSION); + } + + private static final String TAG = TestStorage.class.getSimpleName(); + private static final String PROPERTIES_FILE_NAME = "properties.dat"; + + private final ContentResolver contentResolver; + + /** + * Default constructor. + * + *

This class is supposed to be used mostly in the Instrumentation process, e.g. in an Android + * Instrumentation test. Thus by default, we use the content resolver of the app under test as the + * one to resolve a URI in this storage service. + */ + public TestStorage() { + this(InstrumentationRegistry.getInstrumentation().getTargetContext().getContentResolver()); + } + + /** + * Constructor. + * + * @param contentResolver the content resolver that shall be used to resolve a URI in the test + * storage service. Should not be null. + */ + public TestStorage(@Nonnull ContentResolver contentResolver) { + this.contentResolver = contentResolver; + } + + /** + * Provides a Uri to a test file dependency. + * + *

In most of the cases, you would use {@link #openInputFile(String)} for opening up an + * InputStream to the input file content immediately. Only use this method if you would like to + * store the file Uri and use it for I/O operations later. + * + * @param pathname path to the test file dependency. Should not be null. This is a relative path + * to where the storage service stores the input files. For example, if the storage service + * stores the input files under "/sdcard/test_input_files", with a pathname + * "/path/to/my_input.txt", the file will end up at + * "/sdcard/test_input_files/path/to/my_input.txt" on device. + * @return a content Uri to the test file dependency. + */ + public static Uri getInputFileUri(@Nonnull String pathname) { + checkNotNull(pathname); + return HostedFile.buildUri(HostedFile.FileHost.TEST_FILE, pathname); + } + + /** + * Provides a Uri to a test output file. + * + *

In most of the cases, you would use {@link #openOutputFile(String)} for opening up an + * OutputStream to the output file content immediately. Only use this method if you would like to + * store the file Uri and use it for I/O operations later. + * + * @param pathname path to the test output file. Should not be null. This is a relative path to + * where the storage service stores the output files. For example, if the storage service + * stores the output files under "/sdcard/test_output_files", with a pathname + * "/path/to/my_output.txt", the file will end up at + * "/sdcard/test_output_files/path/to/my_output.txt" on device. + */ + public static Uri getOutputFileUri(@Nonnull String pathname) { + checkNotNull(pathname); + return HostedFile.buildUri(HostedFile.FileHost.OUTPUT, pathname); + } + + /** + * Provides an InputStream to a test file dependency. + * + * @param pathname path to the test file dependency. Should not be null. This is a relative path + * to where the storage service stores the input files. For example, if the storage service + * stores the input files under "/sdcard/test_input_files", with a pathname + * "/path/to/my_input.txt", the file will end up at + * "/sdcard/test_input_files/path/to/my_input.txt" on device. + * @return an InputStream to the given test file. + */ + public InputStream openInputFile(@Nonnull String pathname) throws FileNotFoundException { + Uri dataUri = getInputFileUri(pathname); + return getInputStream(dataUri); + } + + /** + * Returns the value of a given argument name. + * + *

There should be one and only one argument defined with the given argument name. Otherwise, + * it will throw a TestStorageException if zero or more than one arguments are found. + * + *

We suggest using some naming convention when defining the argument name to avoid possible + * conflict, e.g. defining "namespaces" for your arguments which helps clarify how the argument is + * used and also its scope. For example, for arguments used for authentication purposes, you could + * name the account email argument as something like "google_account.email" and its password as + * "google_account.password". + * + * @param argName the argument name. Should not be null. + */ + public String getInputArg(@Nonnull String argName) { + checkNotNull(argName); + + Uri testArgUri = PropertyFile.buildUri(Authority.TEST_ARGS, argName); + Cursor cursor = null; + try { + cursor = doQuery(contentResolver, testArgUri); + if (cursor.getCount() == 0) { + throw new TestStorageException( + String.format( + "Query for URI '%s' did not return any results." + + " Make sure the argName is actually being passed in as a test argument.", + testArgUri)); + } + if (cursor.getCount() > 1) { + throw new TestStorageException( + String.format("Query for URI '%s' returned more than one result. Weird.", testArgUri)); + } + cursor.moveToFirst(); + return cursor.getString(PropertyFile.Column.VALUE.getPosition()); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Returns the name/value map of all test arguments or an empty map if no arguments are defined. + */ + public Map getInputArgs() { + Uri testArgUri = PropertyFile.buildUri(Authority.TEST_ARGS); + Cursor cursor = null; + try { + cursor = doQuery(contentResolver, testArgUri); + return getProperties(cursor); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Provides an OutputStream to a test output file. + * + * @param pathname path to the test output file. Should not be null. This is a relative path to + * where the storage service stores the output files. For example, if the storage service + * stores the output files under "/sdcard/test_output_files", with a pathname + * "/path/to/my_output.txt", the file will end up at + * "/sdcard/test_output_files/path/to/my_output.txt" on device. + * @return an OutputStream to the given output file. + */ + public OutputStream openOutputFile(@Nonnull String pathname) throws FileNotFoundException { + checkNotNull(pathname); + + Uri outputUri = getOutputFileUri(pathname); + return getOutputStream(outputUri); + } + + /** + * Adds the given properties. + * + *

Adding a property with the same name would append new values and overwrite the old values if + * keys already exist. + */ + public void addOutputProperties(Map properties) { + if (properties == null || properties.isEmpty()) { + return; + } + + Map allProperties = getOutputProperties(); + allProperties.putAll(properties); + + Uri propertyFileUri = getPropertyFileUri(); + ObjectOutputStream objectOutputStream = null; + try { + // Buffered to improve performance and avoid the unbuffered IO violation when running under + // strict mode. + OutputStream outputStream = new BufferedOutputStream(getOutputStream(propertyFileUri)); + objectOutputStream = new ObjectOutputStream(outputStream); + objectOutputStream.writeObject(allProperties); + } catch (FileNotFoundException ex) { + throw new TestStorageException("Unable to create file", ex); + } catch (IOException e) { + throw new TestStorageException("I/O error occurred during reading test properties.", e); + } finally { + silentlyClose(objectOutputStream); + } + } + + /** + * Returns a map of all the output test properties. If no properties exist, an empty map will be + * returned. + */ + public Map getOutputProperties() { + Uri propertyFileUri = getPropertyFileUri(); + + ObjectInputStream in = null; + InputStream rawStream = null; + try { + rawStream = getInputStream(propertyFileUri); + in = new ObjectInputStream(rawStream); + @SuppressWarnings("unchecked") + Map recordedProperties = (Map) in.readObject(); + if (recordedProperties == null) { + return new HashMap<>(); + } else { + return recordedProperties; + } + } catch (FileNotFoundException fnfe) { + Log.i(TAG, String.format("%s: does not exist, we must be the first call.", propertyFileUri)); + } catch (IOException | ClassNotFoundException e) { + Log.w(TAG, "Failed to read recorded stats!", e); + } finally { + silentlyClose(in); + silentlyClose(rawStream); + } + return new HashMap<>(); + } + + private static Uri getPropertyFileUri() { + return HostedFile.buildUri(HostedFile.FileHost.EXPORT_PROPERTIES, PROPERTIES_FILE_NAME); + } + + private static ContentProviderClient makeContentProviderClient( + ContentResolver resolver, Uri uri) { + checkNotNull(resolver); + + ContentProviderClient providerClient = resolver.acquireContentProviderClient(uri); + if (null == providerClient) { + throw new TestStorageException( + String.format( + "No content provider registered for: %s. Are all test services apks installed?", + uri)); + } + return providerClient; + } + + /** + * Caller of this method is responsible for closing the cursor instance to avoid possible resource + * leaks. + */ + private static Cursor doQuery(ContentResolver resolver, Uri uri) { + checkNotNull(resolver); + checkNotNull(uri); + + Cursor cursor = + resolver.query( + uri, + null /* projection */, + null /* selection */, + null /* selectionArgs */, + null /* sortOrder */); + if (cursor == null) { + throw new TestStorageException(String.format("Failed to resolve query for URI: %s", uri)); + } + return cursor; + } + + private static Map getProperties(Cursor cursor) { + checkNotNull(cursor); + + Map properties = new HashMap<>(); + while (cursor.moveToNext()) { + properties.put( + cursor.getString(PropertyFile.Column.NAME.getPosition()), + cursor.getString(PropertyFile.Column.VALUE.getPosition())); + } + return properties; + } + + private static void silentlyClose(InputStream in) { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + // do nothing. + } + } + } + + private static void silentlyClose(OutputStream out) { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + // do nothing. + } + } + } + + /** + * Gets the input stream for a given Uri. + * + * @param uri The Uri for which the InputStream is required. + */ + InputStream getInputStream(Uri uri) throws FileNotFoundException { + checkNotNull(uri); + + ContentProviderClient providerClient = null; + try { + providerClient = makeContentProviderClient(contentResolver, uri); + // Assignment to a variable is required. Do not inline. + ParcelFileDescriptor pfd = providerClient.openFile(uri, "r"); + // Buffered to improve performance. + return new BufferedInputStream(new ParcelFileDescriptor.AutoCloseInputStream(pfd)); + } catch (RemoteException re) { + throw new TestStorageException("Unable to access content provider: " + uri, re); + } finally { + if (providerClient != null) { + // Uses #release() to be compatible with API < 24. + providerClient.release(); + } + } + } + + /** + * Gets the output stream for a given Uri. + * + *

The returned OutputStream is essentially a {@link java.io.FileOutputStream} which likely + * should be buffered to avoid {@code UnbufferedIoViolation} when running under strict mode. + * + * @param uri The Uri for which the OutputStream is required. + */ + OutputStream getOutputStream(Uri uri) throws FileNotFoundException { + checkNotNull(uri); + + ContentProviderClient providerClient = null; + try { + providerClient = makeContentProviderClient(contentResolver, uri); + return new ParcelFileDescriptor.AutoCloseOutputStream(providerClient.openFile(uri, "w")); + } catch (RemoteException re) { + throw new TestStorageException("Unable to access content provider: " + uri, re); + } finally { + if (providerClient != null) { + // Uses #release() to be compatible with API < 24. + providerClient.release(); + } + } + } +} diff --git a/services/storage/java/androidx/test/services/storage/TestStorageConstants.java b/services/storage/java/androidx/test/services/storage/TestStorageConstants.java new file mode 100644 index 000000000..292acd1b5 --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/TestStorageConstants.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage; + +/** Holds constants that are shared between on-device and host-side testing infrastructure. */ +@ExperimentalTestStorage +public final class TestStorageConstants { + + /** The parent folder name for all the test related files. */ + public static final String ON_DEVICE_PATH_ROOT = "googletest/"; + + /** The folder for internal use. */ + public static final String ON_DEVICE_PATH_INTERNAL_USE = ON_DEVICE_PATH_ROOT + "internal_use/"; + + /** The provider authority for internal use. */ + public static final String INTERNAL_USE_PROVIDER_AUTHORITY = + "androidx.test.services.storage._internal_use_files"; + + /** The folder where the test output files are written. */ + public static final String ON_DEVICE_PATH_TEST_OUTPUT = ON_DEVICE_PATH_ROOT + "test_outputfiles/"; + + /** The provider authority for test output files. */ + public static final String TEST_OUTPUT_PROVIDER_AUTHORITY = + "androidx.test.services.storage.outputfiles"; + + /** The folder for test properties that shall be exported to the testing infra. */ + public static final String ON_DEVICE_PATH_TEST_PROPERTIES = + ON_DEVICE_PATH_ROOT + "test_exportproperties/"; + + /** The provider authority for output properties. */ + public static final String OUTPUT_PROPERTIES_PROVIDER_AUTHORITY = + "androidx.test.services.storage.properties"; + + /** The folder where the fixture test scripts are pushed on device. */ + public static final String ON_DEVICE_FIXTURE_SCRIPTS = ON_DEVICE_PATH_ROOT + "fixture_scripts/"; + + /** The folder where files needed in test runtime are pushed. */ + public static final String ON_DEVICE_TEST_RUNFILES = ON_DEVICE_PATH_ROOT + "test_runfiles/"; + + /** The provider authority for files needed in test runtime. */ + public static final String TEST_RUNFILES_PROVIDER_AUTHORITY = + "androidx.test.services.storage.runfiles"; + + /** The provider authority for test arguments. */ + public static final String TEST_ARGS_PROVIDER_AUTHORITY = + "androidx.test.services.storage.testargs"; + + /** The name of the file where test arguments are stored. */ + public static final String TEST_ARGS_FILE_NAME = "test_args.dat"; + + /** The name of the test argument that indicates whether qemu ips should be used. */ + public static final String USE_QEMU_IPS_IF_POSSIBLE_ARG_TAG = "infra_use_qemu_ips"; + + private TestStorageConstants() {} +} diff --git a/services/storage/java/androidx/test/services/storage/TestStorageException.java b/services/storage/java/androidx/test/services/storage/TestStorageException.java new file mode 100644 index 000000000..3f16509dc --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/TestStorageException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage; + +/** A RuntimeException thrown out of the test storage service. */ +public class TestStorageException extends RuntimeException { + + public TestStorageException(String message) { + super(message); + } + + public TestStorageException(String message, Throwable t) { + super(message, t); + } +} diff --git a/services/storage/java/androidx/test/services/storage/file/BUILD.bazel b/services/storage/java/androidx/test/services/storage/file/BUILD.bazel new file mode 100644 index 000000000..c90cc99cc --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/file/BUILD.bazel @@ -0,0 +1,18 @@ +load("@build_bazel_rules_android//android:rules.bzl", "android_library") + +package( + default_visibility = ["//services/storage:__subpackages__"], + features = ["-android_resources_strict_deps"], +) + +licenses(["notice"]) + +# Used by TestStorage to build URIs to the test file content providers. +android_library( + name = "file", + srcs = glob(["*.java"]), + deps = [ + "//runner/monitor", + "//services/storage/java/androidx/test/services/storage:test_storage_constants", + ], +) diff --git a/services/storage/java/androidx/test/services/storage/file/HostedFile.java b/services/storage/java/androidx/test/services/storage/file/HostedFile.java new file mode 100644 index 000000000..3c614e91f --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/file/HostedFile.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.file; + +import android.net.Uri; +import android.provider.OpenableColumns; +import androidx.test.services.storage.TestStorageConstants; + +/** Constants to access hosted file data and convenience methods for building Uris. */ +public final class HostedFile { + + /** An enum of the columns returned by the hosted file service. */ + public enum HostedFileColumn { + NAME("name", String.class, 3 /* Cursor.FIELD_TYPE_STRING since api 11 */, 0), + TYPE("type", String.class, 3 /* Cursor.FIELD_TYPE_STRING since api 11 */, 1), + SIZE("size", Long.class, 1 /* Cursor.FIELD_TYPE_INTEGER since api 11 */, 2), + DATA("_data", Byte[].class, 4 /* Cursor.FIELD_TYPE_BLOB since api 11 */, 3), + DISPLAY_NAME(OpenableColumns.DISPLAY_NAME, String.class, 3, 4), + SIZE_2(OpenableColumns.SIZE, Long.class, 2, 5); + + private final String columnName; + private final Class columnType; + private final int androidType; + private final int position; + + private HostedFileColumn( + String columnName, Class columnType, int androidType, int position) { + this.columnName = checkNotNull(columnName); + this.columnType = checkNotNull(columnType); + this.androidType = androidType; + this.position = position; + } + + public String getColumnName() { + return columnName; + } + + public Class getColumnType() { + return columnType; + } + + public int getAndroidType() { + return androidType; + } + + public int getPosition() { + return position; + } + + public static String[] getColumnNames() { + HostedFileColumn[] columns = values(); + String[] names = new String[columns.length]; + for (int i = 0; i < names.length; i++) { + names[i] = columns[i].getColumnName(); + } + return names; + } + } + + /** Enum used to indicate whether a file is a directory or regular file. */ + public enum FileType { + FILE("f"), + DIRECTORY("d"); + private String type; + + private FileType(String type) { + this.type = checkNotNull(type); + } + + public String getTypeCode() { + return type; + } + + public static FileType fromTypeCode(String type) { + for (FileType fileType : values()) { + if (fileType.getTypeCode().equals(type)) { + return fileType; + } + } + throw new IllegalArgumentException("unknown type: " + type); + } + } + + /** An enum containing all known storage services. */ + public enum FileHost { + TEST_FILE(TestStorageConstants.TEST_RUNFILES_PROVIDER_AUTHORITY, false), + EXPORT_PROPERTIES(TestStorageConstants.OUTPUT_PROPERTIES_PROVIDER_AUTHORITY, true), + OUTPUT(TestStorageConstants.TEST_OUTPUT_PROVIDER_AUTHORITY, true), + INTERNAL_USE_ONLY(TestStorageConstants.INTERNAL_USE_PROVIDER_AUTHORITY, true); + + private final String authority; + private final boolean writeable; + + FileHost(String authority, boolean writeable) { + this.authority = checkNotNull(authority); + this.writeable = writeable; + } + + /** The content resolver authority. */ + public String getAuthority() { + return authority; + } + + /** True if writable location, false otherwise. */ + public boolean isWritable() { + return writeable; + } + } + + public static Uri buildUri(FileHost host, String fileName) { + return new Uri.Builder() + .scheme("content") + .authority(host.getAuthority()) + .path(fileName) + .build(); + } + + private static T checkNotNull(T reference) { + if (reference == null) { + throw new NullPointerException(); + } + return reference; + } + + private HostedFile() {} +} diff --git a/services/storage/java/androidx/test/services/storage/file/PropertyFile.java b/services/storage/java/androidx/test/services/storage/file/PropertyFile.java new file mode 100644 index 000000000..c0b83eb7c --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/file/PropertyFile.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.file; + +import static androidx.test.internal.util.Checks.checkNotNull; + +import android.net.Uri; +import androidx.test.services.storage.TestStorageConstants; + +/** + * Constants to access property file data (holding name/value pairs) and convenience methods for + * build URIs. + */ +public final class PropertyFile { + + /** Represents columns returned by the property file service. */ + public enum Column { + NAME("name", 0), + VALUE("value", 1); + + private final String columnName; + private final int position; + + private Column(String columnName, int position) { + this.columnName = checkNotNull(columnName); + this.position = position; + } + + public String getName() { + return columnName; + } + + public int getPosition() { + return position; + } + + public static String[] getNames() { + Column[] columns = values(); + String[] names = new String[values().length]; + for (int i = 0; i < names.length; i++) { + names[i] = columns[i].getName(); + } + return names; + } + } + + /** Enumerates authorities for property-based (i.e. key/value pair) content providers. */ + public enum Authority { + TEST_ARGS(TestStorageConstants.TEST_ARGS_PROVIDER_AUTHORITY); + + private final String authority; + + Authority(String authority) { + this.authority = checkNotNull(authority); + } + + public String getAuthority() { + return authority; + } + } + + /** Returns URI for retrieving all properties. */ + public static Uri buildUri(Authority host) { + checkNotNull(host); + return new Uri.Builder().scheme("content").authority(host.getAuthority()).build(); + } + + /** Returns URI for retrieving a specific property. */ + public static Uri buildUri(Authority host, String property) { + checkNotNull(host); + checkNotNull(property); + return new Uri.Builder() + .scheme("content") + .authority(host.getAuthority()) + .path(property) + .build(); + } + + private PropertyFile() {} +} diff --git a/services/storage/java/androidx/test/services/storage/provider/AbstractFileContentProvider.java b/services/storage/java/androidx/test/services/storage/provider/AbstractFileContentProvider.java new file mode 100644 index 000000000..c3fa31198 --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/provider/AbstractFileContentProvider.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.provider; + +import static androidx.test.internal.util.Checks.checkNotNull; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.util.Log; +import android.webkit.MimeTypeMap; +import androidx.test.services.storage.file.HostedFile; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * Content provider that allows access to reading and (optionally) writing files. + * + *

This is used to expose readonly copies of tests data dependencies and also provides a + * standardized way of exposing test output. + * + *

By placing these IO activities inside a content provider that is installed as an APK separate + * from the test apks, we ensure that the test or app doesn't need any extra permissions such as + * WRITE_EXTERNAL_STORAGE. + */ +abstract class AbstractFileContentProvider extends ContentProvider { + private static final String TAG = AbstractFileContentProvider.class.getSimpleName(); + + private final File hostedDirectory; + private final Access access; + + enum Access { + READ_ONLY, + READ_WRITE + } + + /** + * Called during onCreate(). Subclasses should return true if they are ready to serve data and + * false if there is something wrong accessing their data. Such as the sdcard not being mounted. + */ + protected abstract boolean onCreateHook(); + + AbstractFileContentProvider(File hostedDirectory, Access access) { + super(); + try { + this.hostedDirectory = checkNotNull(hostedDirectory).getCanonicalFile(); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + this.access = access; + } + + @Override + public boolean onCreate() { + if (onCreateHook()) { + if (!hostedDirectory.exists()) { + if (!hostedDirectory.mkdirs()) { + Log.e(TAG, "Cannot create hosted directory: " + hostedDirectory); + return false; + } + } + if (!hostedDirectory.isDirectory()) { + Log.e(TAG, "Hosted directory not a directory: " + hostedDirectory); + return false; + } + if ((Access.READ_WRITE == access) && !hostedDirectory.canWrite()) { + Log.e(TAG, "Hosted directory is not writable and write was requested: " + hostedDirectory); + return false; + } + return true; + } else { + Log.e(TAG, "Subclass claims hosted directory not ready: " + hostedDirectory); + return false; + } + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + checkNotNull(uri); + checkNotNull(mode); + String lowerMode = mode.toLowerCase(); + boolean callWillWrite = lowerMode.contains("w") || lowerMode.contains("t"); + + if ((Access.READ_ONLY == access) && callWillWrite) { + throw new SecurityException( + String.format("Location '%s' is read only (Requested mode: '%s')", uri, lowerMode)); + } + File requestedFile = fromUri(uri); + if (!requestedFile.exists() && callWillWrite) { + try { + requestedFile.getParentFile().mkdirs(); + if (!requestedFile.getParentFile().exists()) { + throw new FileNotFoundException(String.format("No parent directory for '%s'", uri)); + } + + if (!requestedFile.createNewFile()) { + throw new FileNotFoundException("Could not create file: " + uri); + } + } catch (IOException ioe) { + throw new FileNotFoundException( + String.format("Could not access file: %s Exception: %s", uri, ioe.getMessage())); + } + } + Log.i( + TAG, + String.format( + "file '%s': %s", requestedFile, requestedFile.exists() ? "found" : "not found")); + return openFileHelper(uri, mode); + } + + private File fromUri(Uri inUri) throws FileNotFoundException { + File requestedFile = null; + try { + requestedFile = new File(hostedDirectory, inUri.getPath()).getCanonicalFile(); + } catch (IOException ioe) { + throw new FileNotFoundException( + String.format( + "'%s': error resolving to canonical path - %s", requestedFile, ioe.getMessage())); + } + + File checkFile = requestedFile.getAbsoluteFile(); + + while (null != checkFile) { + if (checkFile.equals(hostedDirectory)) { + return requestedFile; + } + checkFile = checkFile.getParentFile(); + } + + // Hmm... our requested file is not under the expected parent directory. + throw new SecurityException( + String.format("URI '%s' refers to a file not managed by this provider", inUri)); + } + + @Override + public Cursor query( + Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + + File requestedFile = null; + try { + requestedFile = fromUri(uri); + } catch (FileNotFoundException fnfe) { + Log.w(TAG, "could not find file for query.", fnfe); + throw new RuntimeException(fnfe); + } + + File[] children = requestedFile.listFiles(); + String[] cols = HostedFile.HostedFileColumn.getColumnNames(); + if (null != children) { + MatrixCursor cursor = new MatrixCursor(cols, children.length); + for (File child : children) { + MatrixCursor.RowBuilder row = cursor.newRow(); + row.add(uri.getPath() + "/" + Uri.encode(child.getName())); + if (child.isDirectory()) { + row.add(HostedFile.FileType.DIRECTORY.getTypeCode()); + row.add(child.listFiles().length); + } else { + row.add(HostedFile.FileType.FILE.getTypeCode()); + row.add(child.length()); + } + row.add(child.getAbsolutePath()); + row.add(child.getName()); + row.add(child.length()); + } + return cursor; + } else if (requestedFile.exists()) { + MatrixCursor cursor = new MatrixCursor(cols, 1); + MatrixCursor.RowBuilder row = cursor.newRow(); + row.add(uri.getPath()); + row.add(HostedFile.FileType.FILE.getTypeCode()); + row.add(requestedFile.length()); + row.add(requestedFile.getAbsolutePath()); + row.add(requestedFile.getName()); + row.add(requestedFile.length()); + return cursor; + } else { + Log.i( + TAG, + String.format( + "%s: does not exist. Mapped from uri: '%s'", requestedFile.getAbsolutePath(), uri)); + return new MatrixCursor(cols, 0); + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + // not allowed. + return 0; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + // not allowed. + return 0; + } + + @Override + public String getType(Uri uri) { + checkNotNull(uri); + // Takes a wild guess at the mime type by looking for the file extension. + String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); + MimeTypeMap map = MimeTypeMap.getSingleton(); + return map.getMimeTypeFromExtension(extension); + } + + @Override + public Uri insert(Uri uri, ContentValues contentValues) { + throw new UnsupportedOperationException("Insertion is not allowed."); + } + + // @Override since api 11 + public void shutdown() { + // no open services, this just suppresses a logger warning. + } +} diff --git a/services/storage/java/androidx/test/services/storage/provider/AndroidManifest.xml b/services/storage/java/androidx/test/services/storage/provider/AndroidManifest.xml new file mode 100644 index 000000000..2a371605f --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/provider/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/services/storage/java/androidx/test/services/storage/provider/AndroidManifest_opensource.xml b/services/storage/java/androidx/test/services/storage/provider/AndroidManifest_opensource.xml new file mode 100644 index 000000000..acc83c847 --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/provider/AndroidManifest_opensource.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + diff --git a/services/storage/java/androidx/test/services/storage/provider/BUILD.bazel b/services/storage/java/androidx/test/services/storage/provider/BUILD.bazel new file mode 100644 index 000000000..6eeb5f1fe --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/provider/BUILD.bazel @@ -0,0 +1,22 @@ +load("@build_bazel_rules_android//android:rules.bzl", "android_library") + +package( + default_visibility = ["//services:__subpackages__"], + features = ["-android_resources_strict_deps"], +) + +licenses(["notice"]) + +# Content providers that provide read write or read only access to the SDCard. +android_library( + name = "storage_content_providers", + srcs = glob(["*.java"]), + manifest = "AndroidManifest_opensource.xml", + deps = [ + "//runner/monitor", + "//services/storage/java/androidx/test/services/storage:storage_service_pb_java_proto_lite", + "//services/storage/java/androidx/test/services/storage:test_storage_constants", + "//services/storage/java/androidx/test/services/storage/file", + "@com_google_protobuf_javalite//:protobuf_java_lite", + ], +) diff --git a/services/storage/java/androidx/test/services/storage/provider/ExportTestPropertiesContentProvider.java b/services/storage/java/androidx/test/services/storage/provider/ExportTestPropertiesContentProvider.java new file mode 100644 index 000000000..942d49f57 --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/provider/ExportTestPropertiesContentProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.provider; + +import androidx.test.services.storage.TestStorageConstants; + +/** Hosts the properties file that are exported to the testing infrastructure for tests. */ +public final class ExportTestPropertiesContentProvider extends TestFileContentProvider { + + public ExportTestPropertiesContentProvider() { + super( + TestStorageConstants.ON_DEVICE_PATH_TEST_PROPERTIES, + AbstractFileContentProvider.Access.READ_WRITE); + } +} diff --git a/services/storage/java/androidx/test/services/storage/provider/InternalUseOnlyFilesContentProvider.java b/services/storage/java/androidx/test/services/storage/provider/InternalUseOnlyFilesContentProvider.java new file mode 100644 index 000000000..0e29e8113 --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/provider/InternalUseOnlyFilesContentProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.provider; + +import android.os.Environment; +import android.util.Log; +import androidx.test.services.storage.TestStorageConstants; +import java.io.File; + +/** Hosts an SD Card directory for the test framework to read/write internal files to. */ +public final class InternalUseOnlyFilesContentProvider extends AbstractFileContentProvider { + private static final String TAG = "InternalUseOnlyFilesContentProvider"; + + private final File outputDirectory; + + public InternalUseOnlyFilesContentProvider() { + super( + new File( + Environment.getExternalStorageDirectory(), + TestStorageConstants.ON_DEVICE_PATH_INTERNAL_USE), + AbstractFileContentProvider.Access.READ_WRITE); + outputDirectory = + new File( + Environment.getExternalStorageDirectory(), + TestStorageConstants.ON_DEVICE_PATH_INTERNAL_USE); + } + + @Override + protected boolean onCreateHook() { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Log.e(TAG, "sdcard in bad state: " + Environment.getExternalStorageState()); + return false; + } else { + if (!outputDirectory.exists()) { + if (!outputDirectory.mkdirs()) { + Log.e(TAG, String.format("'%s': could not create output dir! ", outputDirectory)); + return false; + } + } + return true; + } + } +} diff --git a/services/storage/java/androidx/test/services/storage/provider/TestArgsContentProvider.java b/services/storage/java/androidx/test/services/storage/provider/TestArgsContentProvider.java new file mode 100644 index 000000000..1e0f17741 --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/provider/TestArgsContentProvider.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.provider; + +import static androidx.test.internal.util.Checks.checkNotNull; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Environment; +import android.util.Log; +import androidx.test.services.storage.TestStorageConstants; +import androidx.test.services.storage.TestStorageServiceProto.TestArgument; +import androidx.test.services.storage.TestStorageServiceProto.TestArguments; +import androidx.test.services.storage.file.PropertyFile; +import androidx.test.services.storage.file.PropertyFile.Authority; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Provides access to test arguments stored as a file on external device storage. This provider + * supports only the query api. Use {@link PropertyFile#buildUri(Authority)} to retrieve all test + * arguments. To retrieve a specific argument, build the URI with the arg name as path by calling + * {@link PropertyFile#buildUri(Authority, String)}. + */ +@SuppressWarnings("javadoc") +public final class TestArgsContentProvider extends ContentProvider { + + private static final String TAG = "TestArgCP"; + private static final String ANDROID_TEST_SERVER_SPEC_FORMAT = "_server_address"; + + private static final String SYSTEM_PROPERTY_CLAZZ = "android.os.SystemProperties"; + private static final String GET_METHOD = "get"; + + private String systemPropertyClassName; + private Method getString; + + void setSystemPropertyClassNameForTest(String className) { + this.systemPropertyClassName = className; + } + + private String getQemuHost() { + try { + if (null == getString) { + if (null == systemPropertyClassName) { + systemPropertyClassName = SYSTEM_PROPERTY_CLAZZ; + } + + Class clazz = Class.forName(systemPropertyClassName); + getString = clazz.getMethod(GET_METHOD, String.class, String.class); + } + return (String) getString.invoke(null, "qemu.host.hostname", ""); + } catch (ClassNotFoundException cnfe) { + Log.w(TAG, "Couldn't access SysProps for qemu hostname.", cnfe); + return ""; + } catch (SecurityException se) { + Log.w(TAG, "Couldn't access SysProps for qemu hostname.", se); + return ""; + } catch (NoSuchMethodException nsme) { + Log.w(TAG, "Couldn't access SysProps for qemu hostname.", nsme); + return ""; + } catch (InvocationTargetException ite) { + Log.w(TAG, "Couldn't access SysProps for qemu hostname.", ite); + return ""; + } catch (IllegalAccessException iae) { + Log.w(TAG, "Couldn't access SysProps for qemu hostname.", iae); + return ""; + } catch (IllegalArgumentException iae) { + Log.w(TAG, "Couldn't access SysProps for qemu hostname.", iae); + return ""; + } + } + + @Override + public int delete(Uri arg0, String arg1, String[] arg2) { + // Not allowed. + return 0; + } + + @Override + public String getType(Uri arg0) { + // mime types not supported + return null; + } + + @Override + public Uri insert(Uri arg0, ContentValues arg1) { + throw new UnsupportedOperationException("Insertion is not allowed."); + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query( + Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + checkNotNull(uri); + + Map argMap = buildArgMapFromFile(); + + MatrixCursor cursor = new MatrixCursor(PropertyFile.Column.getNames()); + String argName = uri.getLastPathSegment(); + if (argName != null) { + // Return the specific arg name/value. + if (argMap.containsKey(argName)) { + String[] row = {argName, argMap.get(argName)}; + cursor.addRow(row); + } + } else { + // No specific arg specified. Return the entire argMap. + for (Entry entry : argMap.entrySet()) { + String[] row = {entry.getKey(), entry.getValue()}; + cursor.addRow(row); + } + } + return cursor; + } + + private Map buildArgMapFromFile() { + Map cleanArgMap = new HashMap<>(); + Map qemuArgMap = new HashMap<>(); + String qemuHost = getQemuHost(); + + for (TestArgument testArg : readProtoFromFile().getArgList()) { + String key = testArg.getName(); + String val = testArg.getValue(); + cleanArgMap.put(key, val); + + if (!"".equals(qemuHost) && key.endsWith(ANDROID_TEST_SERVER_SPEC_FORMAT)) { + String serverHost = val.split(":")[0]; + if (serverHost.startsWith(qemuHost)) { + // TODO: remove startswith check once the emulator launcher passes in FQDN. + // val.replace(qemuHost, "10.0.2.2"); + // b/c the system property should be a FQDN (just like the test args are FQDN) + val = val.replace(serverHost, "10.0.2.2"); + } + } + qemuArgMap.put(key, val); + } + if ("true" + .equalsIgnoreCase(cleanArgMap.get(TestStorageConstants.USE_QEMU_IPS_IF_POSSIBLE_ARG_TAG))) { + return qemuArgMap; + } else { + return cleanArgMap; + } + } + + private static TestArguments readProtoFromFile() { + File testArgsFile = + new File( + Environment.getExternalStorageDirectory(), + TestStorageConstants.ON_DEVICE_PATH_INTERNAL_USE + + TestStorageConstants.TEST_ARGS_FILE_NAME); + if (!testArgsFile.exists()) { + return TestArguments.getDefaultInstance(); + } + try { + return TestArguments.parseFrom(new FileInputStream(testArgsFile)); + } catch (IOException e) { + throw new RuntimeException("Not able to read from file: " + testArgsFile.getName(), e); + } + } + + @Override + public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) { + // Not allowed. + return 0; + } +} diff --git a/services/storage/java/androidx/test/services/storage/provider/TestDataContentProvider.java b/services/storage/java/androidx/test/services/storage/provider/TestDataContentProvider.java new file mode 100644 index 000000000..3c603e4cf --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/provider/TestDataContentProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.provider; + +import android.os.Environment; +import android.util.Log; +import androidx.test.services.storage.TestStorageConstants; +import java.io.File; + +/** Provides access to files in the test data section. */ +public final class TestDataContentProvider extends AbstractFileContentProvider { + private static final String TAG = TestDataContentProvider.class.getSimpleName(); + + public TestDataContentProvider() { + super( + new File( + Environment.getExternalStorageDirectory(), + TestStorageConstants.ON_DEVICE_TEST_RUNFILES), + AbstractFileContentProvider.Access.READ_ONLY); + } + + @Override + protected boolean onCreateHook() { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Log.e(TAG, "sdcard in bad state: " + Environment.getExternalStorageState()); + return false; + } else { + return true; + } + } +} diff --git a/services/storage/java/androidx/test/services/storage/provider/TestFileContentProvider.java b/services/storage/java/androidx/test/services/storage/provider/TestFileContentProvider.java new file mode 100644 index 000000000..3dd92ccf6 --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/provider/TestFileContentProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.provider; + +import android.os.Environment; +import android.util.Log; +import java.io.File; + +/** + * Content Provider that allows access to reading/writing files that were written to disk for tests. + */ +abstract class TestFileContentProvider extends AbstractFileContentProvider { + private static final String TAG = TestFileContentProvider.class.getSimpleName(); + + private final File outputDirectory; + + public TestFileContentProvider(String filePath, AbstractFileContentProvider.Access access) { + super(new File(Environment.getExternalStorageDirectory(), filePath), access); + outputDirectory = new File(Environment.getExternalStorageDirectory(), filePath); + } + + @Override + protected boolean onCreateHook() { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Log.e(TAG, "sdcard in bad state: " + Environment.getExternalStorageState()); + return false; + } else { + if (!outputDirectory.exists()) { + if (!outputDirectory.mkdirs()) { + Log.e(TAG, String.format("'%s': could not create output dir! ", outputDirectory)); + return false; + } + } + return true; + } + } +} diff --git a/services/storage/java/androidx/test/services/storage/provider/TestOutputFilesContentProvider.java b/services/storage/java/androidx/test/services/storage/provider/TestOutputFilesContentProvider.java new file mode 100644 index 000000000..642917c4c --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/provider/TestOutputFilesContentProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.provider; + +import androidx.test.services.storage.TestStorageConstants; + +/** Hosts the output directory for tests. */ +public final class TestOutputFilesContentProvider extends TestFileContentProvider { + + public TestOutputFilesContentProvider() { + super( + TestStorageConstants.ON_DEVICE_PATH_TEST_OUTPUT, + AbstractFileContentProvider.Access.READ_WRITE); + } +} diff --git a/services/storage/java/androidx/test/services/storage/test_storage_service.proto b/services/storage/java/androidx/test/services/storage/test_storage_service.proto new file mode 100644 index 000000000..3d12a61c0 --- /dev/null +++ b/services/storage/java/androidx/test/services/storage/test_storage_service.proto @@ -0,0 +1,20 @@ +// Protos for the storage service. +syntax = "proto3"; + +package androidx.test.services.storage; + +option java_package = "androidx.test.services.storage"; +option java_outer_classname = 'TestStorageServiceProto'; + +// Defines the test argument passed to the tests. +message TestArgument { + // Name of the test argument. + string name = 1; + // Value of the test argument. + string value = 2; +} + +// Defines all the test arguments passed to the tests. +message TestArguments { + repeated TestArgument arg = 1; +} diff --git a/services/storage/javatests/androidx/test/services/storage/AndroidManifest.xml b/services/storage/javatests/androidx/test/services/storage/AndroidManifest.xml new file mode 100644 index 000000000..39255a676 --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/services/storage/javatests/androidx/test/services/storage/AndroidManifest_opensource.xml b/services/storage/javatests/androidx/test/services/storage/AndroidManifest_opensource.xml new file mode 100644 index 000000000..58d1e6f26 --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/AndroidManifest_opensource.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/services/storage/javatests/androidx/test/services/storage/BUILD.bazel b/services/storage/javatests/androidx/test/services/storage/BUILD.bazel new file mode 100644 index 000000000..66fa2a026 --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/BUILD.bazel @@ -0,0 +1,69 @@ +# Description: +# Tests for the test storage. +load("@build_bazel_rules_android//android:rules.bzl", "android_library", "android_binary", "android_instrumentation_test") + +licenses(["notice"]) + +DEVICE_MODEL = [ + "android_15_x86", + "android_16_x86", + "android_17_x86", + "android_19_x86", + "android_21_x86", + "android_23_x86", +] + +android_library( + name = "dummy_app_lib", + testonly = 1, + srcs = ["testapp/DummyActivity.java"], + manifest = "testapp/AndroidManifest_stub.xml", +) + +android_binary( + name = "dummy_app_binary", + testonly = 1, + manifest = "testapp/AndroidManifest_stub.xml", + deps = [ + ":dummy_app_lib", + ], +) + +android_library( + name = "test_lib", + testonly = 1, + srcs = ["TestStorageTest.java"], + manifest = "AndroidManifest_opensource.xml", + deps = [ + ":dummy_app_lib", + "//core/java/androidx/test/core", + "//ext/junit", + "//runner/android_junit_runner", + "//services/storage/java/androidx/test/services/storage", + "//services/storage/java/androidx/test/services/storage/file", + "@androidsdk//:legacy_test-28", + "@maven//:com_google_guava_guava", + "@maven//:junit_junit", + ], +) + +android_binary( + name = "test_binary", + testonly = 1, + instruments = ":dummy_app_binary", + manifest = "AndroidManifest_opensource.xml", + deps = [ + ":test_lib", + ], +) + +[android_instrumentation_test( + name = "TestStorageTest_%s" % device_model, + size = "large", + args = [ + "--clear_package_data", + "--install_test_services=True", + ], + target_device = "//tools/android/emulated_devices/generic_phone:%s" % (device_model), + test_app = ":test_binary", +) for device_model in DEVICE_MODEL] diff --git a/services/storage/javatests/androidx/test/services/storage/TestStorageTest.java b/services/storage/javatests/androidx/test/services/storage/TestStorageTest.java new file mode 100644 index 000000000..423c5f788 --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/TestStorageTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage; + +import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertEquals; + +import android.net.Uri; +import androidx.test.core.app.ActivityScenario; +import androidx.test.services.storage.file.HostedFile; +import androidx.test.services.storage.testapp.DummyActivity; +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Serializable; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit test cases for {@link TestStorage}. */ +@RunWith(JUnit4.class) +public final class TestStorageTest { + + private static final String OUTPUT_PATH = "parent_dir/output_file"; + + private final TestStorage testStorage = new TestStorage(); + + @Before + public void setUp() { + ActivityScenario.launch(DummyActivity.class); + } + + @Test + public void testReadNonExistentFile() { + try { + testStorage.openInputFile("not/here"); + fail("Should throw FileNotFoundException."); + } catch (FileNotFoundException e) { + // Exception excepted. + } + } + + @Test + public void testWriteFile() throws Exception { + OutputStream rawStream = testStorage.openOutputFile(OUTPUT_PATH); + Writer writer = new BufferedWriter(new OutputStreamWriter(rawStream)); + try { + writer.write("Four score and 7 years ago\n"); + writer.write("Our forefathers executed some tests."); + } finally { + writer.close(); + } + } + + @Test + public void testAddOutputProperties() throws Exception { + Map propertyMap = new HashMap(); + propertyMap.put("property-a", "test"); + // Pass in a cloned copy since addStatsToSponge may modify the propertyMap instance. + testStorage.addOutputProperties(new HashMap(propertyMap)); + propertyMap.put("property-b", "test"); + testStorage.addOutputProperties(new HashMap(propertyMap)); + // Test property value updated. + propertyMap.put("property-b", "test-updated"); + testStorage.addOutputProperties(new HashMap(propertyMap)); + + Uri dataUri = HostedFile.buildUri(HostedFile.FileHost.EXPORT_PROPERTIES, "properties.dat"); + InputStream rawStream = testStorage.getInputStream(dataUri); + + ObjectInputStream in = null; + try { + in = new ObjectInputStream(rawStream); + Map recordedStats = (Map) in.readObject(); + assertEquals("Properties not written to the properties file", propertyMap, recordedStats); + } catch (IOException | ClassNotFoundException e) { + closeInputStream(in); + } + } + + + private void closeInputStream(ObjectInputStream in) { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + // do nothing. + } + } + } +} diff --git a/services/storage/javatests/androidx/test/services/storage/provider/AndroidManifest.xml b/services/storage/javatests/androidx/test/services/storage/provider/AndroidManifest.xml new file mode 100644 index 000000000..098d49c22 --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/provider/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/services/storage/javatests/androidx/test/services/storage/provider/AndroidManifest_opensource.xml b/services/storage/javatests/androidx/test/services/storage/provider/AndroidManifest_opensource.xml new file mode 100644 index 000000000..1b502f138 --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/provider/AndroidManifest_opensource.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/services/storage/javatests/androidx/test/services/storage/provider/AndroidManifest_stub.xml b/services/storage/javatests/androidx/test/services/storage/provider/AndroidManifest_stub.xml new file mode 100644 index 000000000..bf41e63ba --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/provider/AndroidManifest_stub.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/services/storage/javatests/androidx/test/services/storage/provider/BUILD.bazel b/services/storage/javatests/androidx/test/services/storage/provider/BUILD.bazel new file mode 100644 index 000000000..523e51ffd --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/provider/BUILD.bazel @@ -0,0 +1,55 @@ +# Description: +# Tests for the sdcard data content providers +load("@build_bazel_rules_android//android:rules.bzl", "android_library", "android_binary", "android_instrumentation_test") + +licenses(["notice"]) + +API_LEVELS = [ + "android_15_x86", + "android_16_x86", + "android_17_x86", + "android_19_x86", + "android_21_x86", + "android_22_x86", + "android_23_x86", +] + +android_library( + name = "storage_test_lib", + testonly = 1, + srcs = glob(["*.java"]), + manifest = "AndroidManifest_opensource.xml", + deps = [ + "//runner/android_junit_runner", + "//services/storage/java/androidx/test/services/storage/file", + "//services/storage/java/androidx/test/services/storage/provider:storage_content_providers", + "//services/storage/java/androidx/test/services/storage:storage_service_pb_java_proto_lite", + "//services/storage/java/androidx/test/services/storage:test_storage_constants", + "@androidsdk//:legacy_test-28", + "@maven//:com_google_guava_guava", + ], +) + +android_binary( + name = "storage_binary", + testonly = 1, + manifest = "AndroidManifest_stub.xml", +) + +android_binary( + name = "storage_test_binary", + testonly = 1, + instruments = ":storage_binary", + manifest = "AndroidManifest_opensource.xml", + deps = [":storage_test_lib"], +) + +[android_instrumentation_test( + name = "storage_provider_test_%s" % api_level, + size = "large", + args = [ + "--install_test_services=True", + ], + target_device = "//tools/android/emulated_devices/generic_phone:%s" % (api_level), + test_app = ":storage_test_binary", +) for api_level in API_LEVELS] diff --git a/services/storage/javatests/androidx/test/services/storage/provider/TestArgsContentProviderTest.java b/services/storage/javatests/androidx/test/services/storage/provider/TestArgsContentProviderTest.java new file mode 100644 index 000000000..88dd1c802 --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/provider/TestArgsContentProviderTest.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.provider; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.test.ProviderTestCase2; +import androidx.test.services.storage.TestStorageConstants; +import androidx.test.services.storage.TestStorageServiceProto.TestArgument; +import androidx.test.services.storage.TestStorageServiceProto.TestArguments; +import androidx.test.services.storage.file.PropertyFile; +import com.google.common.base.Optional; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Unit tests for {@link TestArgsContentProvider}. + * + * TODO(b/145236542): Converts the tests to JUnit4. + */ +public class TestArgsContentProviderTest extends ProviderTestCase2 { + + private static final String[] ARGS = {"arg1", "arg2", "arg3", "someth_server_address"}; + private static final String[] VALUES = {"value1", "value2", "value3", "foo:124"}; + + private File testArgsFile; + + public TestArgsContentProviderTest() { + super(TestArgsContentProvider.class, PropertyFile.Authority.TEST_ARGS.getAuthority()); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + getProvider().setSystemPropertyClassNameForTest(FakeSystemProperties.class.getName()); + } + + @Override + public void tearDown() throws Exception { + if (testArgsFile != null) { + testArgsFile.delete(); + } + super.tearDown(); + } + + public void testGetOneArg() throws IOException { + createTestArgsFileWithMultipleArgs(); + + int argIndex = 2; + Uri uri = PropertyFile.buildUri(PropertyFile.Authority.TEST_ARGS, ARGS[argIndex]); + Cursor cursor = getMockContentResolver().query(uri, null, null, null, null); + + try { + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals(cursor.getString(PropertyFile.Column.VALUE.getPosition()), VALUES[argIndex]); + } finally { + cursor.close(); + } + } + + public void testGetMultipleArgs() throws IOException { + createTestArgsFileWithMultipleArgs(); + + Uri uri = PropertyFile.buildUri(PropertyFile.Authority.TEST_ARGS); + Cursor cursor = getMockContentResolver().query(uri, null, null, null, null); + Map argMap = getProperties(cursor); + + try { + assertEquals(argMap.entrySet().size(), cursor.getCount()); + for (int i = 0; i < ARGS.length; i++) { + assertEquals(VALUES[i], argMap.get(ARGS[i])); + } + } finally { + cursor.close(); + } + } + + public void testGetWrongArg() throws IOException { + createTestArgsFileWithMultipleArgs(); + + Uri uri = PropertyFile.buildUri(PropertyFile.Authority.TEST_ARGS, "wrong"); + Cursor cursor = getMockContentResolver().query(uri, null, null, null, null); + + try { + assertEquals(0, cursor.getCount()); + } finally { + cursor.close(); + } + } + + public void testEmptyArgsDataFile() throws IOException { + createTestArgsFile(TestArguments.getDefaultInstance()); + Uri uri = PropertyFile.buildUri(PropertyFile.Authority.TEST_ARGS); + Cursor cursor = getMockContentResolver().query(uri, null, null, null, null); + assertEquals(0, cursor.getCount()); + cursor.close(); + } + + public void testServerSpecOverride_behavesWellWhenNotSet() throws IOException { + String localhost = "fake.machine.company.com"; + FakeSystemProperties.props.put("qemu.host.hostname", ""); + createTestArgsFile(makeSomeServerSpecArgs(localhost)); + + Uri uri = PropertyFile.buildUri(PropertyFile.Authority.TEST_ARGS); + Cursor cursor = getMockContentResolver().query(uri, null, null, null, null); + Map argMap = getProperties(cursor); + + assertEquals(localhost + ":12345", argMap.get("local_server_address")); + assertEquals(localhost + ":984", argMap.get("local_2_server_address")); + assertEquals("www.google.com:80", argMap.get("non_local_server_address")); + assertEquals("fake.machine.company.com:100", argMap.get("val_is_a_spec_but_not_key")); + } + + public void testServerSpecOverride() throws IOException { + String localhost = "fake.machine.company.com"; + FakeSystemProperties.props.put("qemu.host.hostname", localhost); + createTestArgsFile(makeSomeServerSpecArgs(localhost)); + + Uri uri = PropertyFile.buildUri(PropertyFile.Authority.TEST_ARGS); + Cursor cursor = getMockContentResolver().query(uri, null, null, null, null); + Map argMap = getProperties(cursor); + + assertEquals("10.0.2.2:12345", argMap.get("local_server_address")); + assertEquals("10.0.2.2:984", argMap.get("local_2_server_address")); + assertEquals("www.google.com:80", argMap.get("non_local_server_address")); + assertEquals("fake.machine.company.com:100", argMap.get("val_is_a_spec_but_not_key")); + } + + static class FakeSystemProperties { + private static final Map props = + Collections.synchronizedMap(new HashMap()); + + public static String get(String key, String def) { + return Optional.fromNullable(props.get(key)).or(def); + } + } + + private static TestArguments makeSomeServerSpecArgs(String localhost) { + return TestArguments.newBuilder() + .addArg( + TestArgument.newBuilder() + .setName("non_local_server_address") + .setValue("www.google.com:80") + .build()) + .addArg( + TestArgument.newBuilder() + .setName("local_server_address") + .setValue(localhost + ":12345") + .build()) + .addArg( + TestArgument.newBuilder() + .setName("local_2_server_address") + .setValue(localhost + ":984") + .build()) + .addArg( + TestArgument.newBuilder() + .setName("val_is_a_spec_but_not_key") + .setValue(localhost + ":100") + .build()) + .addArg( + TestArgument.newBuilder() + .setName(TestStorageConstants.USE_QEMU_IPS_IF_POSSIBLE_ARG_TAG) + .setValue(String.valueOf(true)) + .build()) + .build(); + } + + private static void createTestArgsFileWithMultipleArgs() throws IOException { + TestArguments.Builder builder = TestArguments.newBuilder(); + for (int i = 0; i < ARGS.length; i++) { + builder.addArg(TestArgument.newBuilder().setName(ARGS[i]).setValue(VALUES[i]).build()); + } + createTestArgsFile(builder.build()); + } + + private static void createTestArgsFile(TestArguments proto) throws IOException { + File args = + new File( + Environment.getExternalStorageDirectory(), + TestStorageConstants.ON_DEVICE_PATH_INTERNAL_USE + + TestStorageConstants.TEST_ARGS_FILE_NAME); + Files.write(proto.toByteArray(), args); + } + + private Map getProperties(Cursor cursor) { + Map properties = new HashMap<>(); + while (cursor.moveToNext()) { + properties.put( + cursor.getString(PropertyFile.Column.NAME.getPosition()), + cursor.getString(PropertyFile.Column.VALUE.getPosition())); + } + return properties; + } +} diff --git a/services/storage/javatests/androidx/test/services/storage/provider/TestFileContentProviderTest.java b/services/storage/javatests/androidx/test/services/storage/provider/TestFileContentProviderTest.java new file mode 100644 index 000000000..d49a8afb0 --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/provider/TestFileContentProviderTest.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.provider; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.test.ProviderTestCase2; +import android.test.suitebuilder.annotation.Suppress; +import androidx.test.services.storage.file.HostedFile; +import androidx.test.services.storage.provider.AbstractFileContentProvider.Access; +import com.google.common.base.Charsets; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.Lists; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.List; + +/** + * Unit tests for {@link TestFileContentProvider}. + * + * TODO(b/145236542): Converts the tests to JUnit4. + */ +public class TestFileContentProviderTest + extends ProviderTestCase2 { + + private static final Object TEST_RESOLVER_INIT_LOCK = new Object(); + private static final File BOGUS_DIRECTORY = + new File(Environment.getExternalStorageDirectory(), "fcp_test/bogus"); + private static volatile File resolverHostedDirectory = BOGUS_DIRECTORY; + private static volatile Access resolverAccess = Access.READ_ONLY; + private static volatile Predicate resolverOnCreateHookResult = Predicates.alwaysTrue(); + private static final String TEST_AUTHORITY = "test_files"; + private static final String[] EMPTY_ARRAY = {}; + + private Access access = Access.READ_ONLY; + private File hostedDirectory; + private ContentResolver resolver; + + public TestFileContentProviderTest() { + super(TestFileContentProvider.class, TEST_AUTHORITY); + } + + @Override + public void setUp() throws Exception { + hostedDirectory = new File(Environment.getExternalStorageDirectory(), "fcp_test/" + getName()); + hostedDirectory.getParentFile().mkdirs(); + access = Access.READ_ONLY; + super.setUp(); + initResolver(); + } + + private Uri makeUri(String path) { + return new Uri.Builder().scheme("content").authority(TEST_AUTHORITY).path(path).build(); + } + + public void testReadFile() throws Exception { + File testFile = new File(hostedDirectory, "test-data.txt"); + assertTrue("Couldnt create test file.", testFile.createNewFile()); + write("hello world", testFile, Charsets.UTF_8); + ParcelFileDescriptor providerFile = resolver.openFileDescriptor(makeUri("test-data.txt"), "r"); + String contents = read(providerFile, Charsets.UTF_8); + assertEquals("Unexpected file contents!", "hello world", contents); + } + + public void testReadFile_WithNameThatNeedsEncoding() throws Exception { + File testFile = new File(hostedDirectory, "oh wow & aren't modern day file-systems [SO] great"); + assertTrue("Couldnt create test file.", testFile.createNewFile()); + write("8 ascii chars and a 3 letter extension", testFile, Charsets.UTF_8); + ParcelFileDescriptor providerFile = + resolver.openFileDescriptor( + makeUri("oh wow & aren't modern day file-systems [SO] great"), "r"); + String contents = read(providerFile, Charsets.UTF_8); + assertEquals("Unexpected file contents!", "8 ascii chars and a 3 letter extension", contents); + } + + public void testRead_DoesNotExist() throws Exception { + try { + resolver.openFileDescriptor(makeUri("does-not-exist.txt"), "r"); + fail("file doesnt exist, shouldnt be able to open it."); + } catch (FileNotFoundException expected) { + /*expected*/ + } + } + + public void testWrite_OnReadOnlyFileSystem() throws Exception { + File testFile = new File(hostedDirectory, "test-data.txt"); + assertTrue("Couldnt create test file.", testFile.createNewFile()); + try { + resolver.openFileDescriptor(makeUri("test-data.txt"), "w"); + fail("shouldnt be able to write to ro fs"); + } catch (SecurityException expected) { + /*expected*/ + } + } + + public void testWrite_FileInExistingDirectory() throws Exception { + access = Access.READ_WRITE; + initResolver(); + ParcelFileDescriptor providerFile = resolver.openFileDescriptor(makeUri("test-data.txt"), "w"); + write( + "hello world", + new ParcelFileDescriptor.AutoCloseOutputStream(providerFile), + Charsets.UTF_8); + File expectedFile = new File(hostedDirectory, "test-data.txt"); + assertTrue("file not in expected place.", expectedFile.exists()); + String fileContent = read(expectedFile, Charsets.UTF_8); + assertEquals("contents unexpected", "hello world", fileContent); + } + + public void testWriteFile_WithNameThatNeedsEncoding() throws Exception { + access = Access.READ_WRITE; + initResolver(); + ParcelFileDescriptor providerFile = + resolver.openFileDescriptor( + makeUri("oh wow & aren't modern day file-systems [SO] great"), "w"); + write( + "hello world", + new ParcelFileDescriptor.AutoCloseOutputStream(providerFile), + Charsets.UTF_8); + File expectedFile = + new File(hostedDirectory, "oh wow & aren't modern day file-systems [SO] great"); + assertTrue("file not in expected place.", expectedFile.exists()); + String fileContent = read(expectedFile, Charsets.UTF_8); + assertEquals("contents unexpected", "hello world", fileContent); + } + + public void testWrite_FileInNewDirectory() throws Exception { + access = Access.READ_WRITE; + initResolver(); + ParcelFileDescriptor providerFile = + resolver.openFileDescriptor(makeUri("subdir/test-data.txt"), "w"); + write( + "hello world", + new ParcelFileDescriptor.AutoCloseOutputStream(providerFile), + Charsets.UTF_8); + File expectedFile = new File(hostedDirectory, "subdir/test-data.txt"); + assertTrue("file not in expected place.", expectedFile.exists()); + String fileContent = read(expectedFile, Charsets.UTF_8); + assertEquals("contents unexpected", "hello world", fileContent); + } + + public void testWrite_RelativePath() throws Exception { + access = Access.READ_WRITE; + initResolver(); + ParcelFileDescriptor providerFile = + resolver.openFileDescriptor(makeUri("subdir/../test-data.txt"), "w"); + write( + "hello world", + new ParcelFileDescriptor.AutoCloseOutputStream(providerFile), + Charsets.UTF_8); + File expectedFile = new File(hostedDirectory, "test-data.txt"); + assertTrue("file not in expected place.", expectedFile.exists()); + String fileContent = read(expectedFile, Charsets.UTF_8); + assertEquals("contents unexpected", "hello world", fileContent); + } + + public void testWrite_OutsideHostedDirectory() throws Exception { + hostedDirectory = new File(hostedDirectory, "resolver_dir"); + hostedDirectory.mkdirs(); + access = Access.READ_WRITE; + initResolver(); + try { + resolver.openFileDescriptor(makeUri("../test-data.txt"), "w"); + fail("shouldnt be able to write outside of hosted directory."); + } catch (SecurityException expected) { + /*expected*/ + } + assertFalse(new File(hostedDirectory.getParent(), "test-data.txt").exists()); + } + + public void testRead_OutsideHostedDirectory() throws Exception { + hostedDirectory = new File(hostedDirectory, "resolver_dir"); + hostedDirectory.mkdirs(); + initResolver(); + write("secrets", new File(hostedDirectory.getParent(), "secret.dat"), Charsets.UTF_8); + try { + resolver.openFileDescriptor(makeUri("../secrets.dat"), "w"); + fail("shouldnt be able to write outside of hosted directory."); + } catch (SecurityException expected) { + /*expected*/ + } + } + + public void testReadAndWrite() throws Exception { + access = Access.READ_WRITE; + initResolver(); + ParcelFileDescriptor providerFile = resolver.openFileDescriptor(makeUri("test-data.txt"), "w"); + write( + "hello world", + new ParcelFileDescriptor.AutoCloseOutputStream(providerFile), + Charsets.UTF_8); + ParcelFileDescriptor readInFile = resolver.openFileDescriptor(makeUri("test-data.txt"), "r"); + String fileContent = read(readInFile, Charsets.UTF_8); + assertEquals("cannot read content back", "hello world", fileContent); + } + + @Suppress + public void testQueryDirectory() throws Exception { + write("file1 contents", new File(hostedDirectory, "file1.txt"), Charsets.UTF_8); + write("brown cow", new File(hostedDirectory, "file2.txt"), Charsets.UTF_8); + new File(hostedDirectory, "subdir").mkdirs(); + + Cursor cursor = + resolver.query( + makeUri(""), HostedFile.HostedFileColumn.getColumnNames(), "", EMPTY_ARRAY, ""); + List listedFileNames = Lists.newArrayList(); + try { + assertEquals(2, cursor.getCount()); + int nameIndex = HostedFile.HostedFileColumn.NAME.getPosition(); + int typeIndex = HostedFile.HostedFileColumn.TYPE.getPosition(); + + while (cursor.moveToNext()) { + listedFileNames.add(cursor.getString(nameIndex)); + HostedFile.FileType expectedFileType = HostedFile.FileType.FILE; + if (cursor.getString(nameIndex).equals("subdir")) { + expectedFileType = HostedFile.FileType.DIRECTORY; + } + HostedFile.FileType actualType = + HostedFile.FileType.fromTypeCode(cursor.getString(typeIndex)); + assertEquals("Wrong file type: " + cursor, expectedFileType, actualType); + } + } finally { + cursor.close(); + } + assertContents(listedFileNames, "file1.txt", "file2.txt", "subdir"); + } + + private void assertContents(Collection collection, T... expectedItems) { + for (T item : expectedItems) { + assertTrue("missing: " + item + " from " + collection, collection.contains(item)); + } + assertEquals( + "size mismatch: " + collection, expectedItems.length, collection.size()); + } + + @Suppress + public void testQueryFile() throws Exception { + write("file1 contents", new File(hostedDirectory, "file1.txt"), Charsets.UTF_8); + Cursor cursor = + resolver.query( + makeUri("file1.txt"), + HostedFile.HostedFileColumn.getColumnNames(), + "", + EMPTY_ARRAY, + ""); + try { + assertEquals(1, cursor.getCount()); + int nameIndex = HostedFile.HostedFileColumn.NAME.getPosition(); + int typeIndex = HostedFile.HostedFileColumn.TYPE.getPosition(); + int sizeIndex = HostedFile.HostedFileColumn.SIZE.getPosition(); + assertEquals("file1.txt", cursor.getString(nameIndex)); + assertEquals( + HostedFile.FileType.FILE, HostedFile.FileType.fromTypeCode(cursor.getString(typeIndex))); + assertEquals("file1 contents".length(), cursor.getString(sizeIndex)); + } finally { + cursor.close(); + } + } + + private String read(File file, Charset inputCharset) throws IOException { + BufferedReader reader = + new BufferedReader(new InputStreamReader(new FileInputStream(file), inputCharset)); + return read(reader); + } + + private String read(ParcelFileDescriptor fileDescriptor, Charset inputCharset) + throws IOException { + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + new ParcelFileDescriptor.AutoCloseInputStream(fileDescriptor), inputCharset)); + return read(reader); + } + + private String read(BufferedReader reader) throws IOException { + StringBuilder builder = new StringBuilder(); + String lineIn = null; + while (null != (lineIn = reader.readLine())) { + builder.append(lineIn); + } + return builder.toString(); + } + + private void write(String content, OutputStream outStream, Charset outputCharset) + throws IOException { + try { + // FYI: getBytes(Charset) from api 9 and up. + // getBytes(String charset) from api 1 + outStream.write(content.getBytes(outputCharset.name())); + } finally { + outStream.close(); + } + } + + private void write(String content, File output, Charset outputCharset) throws IOException { + write(content, new FileOutputStream(output), outputCharset); + } + + private void initResolver() throws Exception { + synchronized (TEST_RESOLVER_INIT_LOCK) { + try { + resolverHostedDirectory = hostedDirectory; + resolverAccess = access; + // Holy type safe language batman! + resolver = + ProviderTestCase2 + . + newResolverWithContentProviderFromSql( + getContext(), + "foo", + TestFileContentProvider.class, + TEST_AUTHORITY, + "bogus", + 0, + ""); + } finally { + resolverHostedDirectory = BOGUS_DIRECTORY; + resolverAccess = Access.READ_ONLY; + } + } + } + + public static class TestFileContentProvider extends AbstractFileContentProvider { + private final Predicate onCreateHookResult; + + public TestFileContentProvider() { + super(resolverHostedDirectory, resolverAccess); + this.onCreateHookResult = resolverOnCreateHookResult; + } + + @Override + public boolean onCreateHook() { + return onCreateHookResult.apply(null); + } + } +} diff --git a/services/storage/javatests/androidx/test/services/storage/testapp/AndroidManifest_stub.xml b/services/storage/javatests/androidx/test/services/storage/testapp/AndroidManifest_stub.xml new file mode 100644 index 000000000..250ff4b21 --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/testapp/AndroidManifest_stub.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/storage/javatests/androidx/test/services/storage/testapp/DummyActivity.java b/services/storage/javatests/androidx/test/services/storage/testapp/DummyActivity.java new file mode 100644 index 000000000..19818ed48 --- /dev/null +++ b/services/storage/javatests/androidx/test/services/storage/testapp/DummyActivity.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2019 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. + */ +package androidx.test.services.storage.testapp; + +import android.app.Activity; + +/** A dummy activity for testing. */ +public class DummyActivity extends Activity { + /** just need this for some test methods. */ +}