/*
* 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.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
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 androidx.test.services.storage.internal.TestStorageUtil;
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 {
private static final String TAG = TestStorage.class.getSimpleName();
private static final String PROPERTIES_FILE_NAME = "properties.dat";
private final ContentResolver contentResolver;
/**
* Default constructor.
*
* <p>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.
*
* <p>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.
*
* <p>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 TestStorageUtil.getInputStream(dataUri, contentResolver);
}
/**
* Returns the value of a given argument name.
*
* <p>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.
*
* <p>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<String, String> 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 TestStorageUtil.getOutputStream(outputUri, contentResolver);
}
/**
* Adds the given properties.
*
* <p>Adding a property with the same name would append new values and overwrite the old values if
* keys already exist.
*/
public void addOutputProperties(Map<String, Serializable> properties) {
if (properties == null || properties.isEmpty()) {
return;
}
Map<String, Serializable> 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(
TestStorageUtil.getOutputStream(propertyFileUri, contentResolver));
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<String, Serializable> getOutputProperties() {
Uri propertyFileUri = getPropertyFileUri();
ObjectInputStream in = null;
InputStream rawStream = null;
try {
rawStream = TestStorageUtil.getInputStream(propertyFileUri, contentResolver);
in = new ObjectInputStream(rawStream);
@SuppressWarnings("unchecked")
Map<String, Serializable> recordedProperties = (Map<String, Serializable>) 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);
}
/**
* 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<String, String> getProperties(Cursor cursor) {
checkNotNull(cursor);
Map<String, String> 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.
}
}
}
}