public final class

TestStorage

extends java.lang.Object

implements PlatformTestStorage

 java.lang.Object

↳androidx.test.services.storage.TestStorage

Gradle dependencies

compile group: 'androidx.test.services', name: 'storage', version: '1.5.0'

  • groupId: androidx.test.services
  • artifactId: storage
  • version: 1.5.0

Artifact androidx.test.services:storage:1.5.0 it located at Google repository (https://maven.google.com/)

Overview

An implementation of PlatformTestStorage that uses a content provider housed in the androidx.test.services apk.

As such, in order to use this implementation the test services apk must be installed on the device, and thus isn't supported in environments where test services is not installed such as Robolectric.

Users should ideally not reference this class directly, but rather retrieve the PlatformTestStorage implementation appropriate for their execution environment from PlatformTestStorageRegistry.

Summary

Constructors
publicTestStorage()

Default constructor.

publicTestStorage(ContentResolver contentResolver)

Constructor.

Methods
public voidaddOutputProperties(java.util.Map<java.lang.String, java.io.Serializable> properties)

Adds the given properties.

public java.lang.StringgetInputArg(java.lang.String argName)

Returns the value of a given argument name.

public java.util.Map<java.lang.String, java.lang.String>getInputArgs()

Returns the name/value map of all test arguments or an empty map if no arguments are defined.

public UrigetInputFileUri(java.lang.String pathname)

Provides a Uri to a test file dependency.

public UrigetOutputFileUri(java.lang.String pathname)

Provides a Uri to a test output file.

public java.util.Map<java.lang.String, java.io.Serializable>getOutputProperties()

Returns a map of all the output test properties.

public booleanisTestStorageFilePath(java.lang.String pathname)

Returns true if pathname corresponds to a file or directory that is in a directory where the storage service stores files.

public java.io.InputStreamopenInputFile(java.lang.String pathname)

Provides an InputStream to a test file dependency.

public java.io.InputStreamopenInternalInputFile(java.lang.String pathname)

Provides an InputStream to an internal file used by the testing infrastructure.

public java.io.OutputStreamopenInternalOutputFile(java.lang.String pathname)

Provides an OutputStream to an internal file used by the testing infrastructure.

public java.io.OutputStreamopenOutputFile(java.lang.String pathname)

Provides an OutputStream to a test output file.

public java.io.OutputStreamopenOutputFile(java.lang.String pathname, boolean append)

Provides an OutputStream to a test output file.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public TestStorage()

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(ContentResolver contentResolver)

Constructor.

Parameters:

contentResolver: the content resolver that shall be used to resolve a URI in the test storage service. Should not be null.

Methods

public Uri getInputFileUri(java.lang.String pathname)

Provides a Uri to a test file dependency.

In most of the cases, you would use TestStorage.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.

Parameters:

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.

Returns:

a content Uri to the test file dependency.

public Uri getOutputFileUri(java.lang.String pathname)

Provides a Uri to a test output file.

In most of the cases, you would use TestStorage.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.

Parameters:

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 boolean isTestStorageFilePath(java.lang.String pathname)

Returns true if pathname corresponds to a file or directory that is in a directory where the storage service stores files.

Parameters:

pathname: path to a file or directory. Should not be null. This is an absolute path to a file that may be a part of the storage service.

public java.io.InputStream openInputFile(java.lang.String pathname)

Provides an InputStream to a test file dependency.

Parameters:

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.

Returns:

an InputStream to the given test file.

public java.lang.String getInputArg(java.lang.String argName)

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".

Parameters:

argName: the argument name. Should not be null.

public java.util.Map<java.lang.String, java.lang.String> getInputArgs()

Returns the name/value map of all test arguments or an empty map if no arguments are defined.

public java.io.OutputStream openOutputFile(java.lang.String pathname)

Provides an OutputStream to a test output file.

Parameters:

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.

Returns:

an OutputStream to the given output file.

public java.io.OutputStream openOutputFile(java.lang.String pathname, boolean append)

Provides an OutputStream to a test output file.

Parameters:

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.
append: if true, then the lines will be added to the end of the file rather than overwriting.

Returns:

an OutputStream to the given output file.

public void addOutputProperties(java.util.Map<java.lang.String, java.io.Serializable> properties)

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 java.util.Map<java.lang.String, java.io.Serializable> getOutputProperties()

Returns a map of all the output test properties. If no properties exist, an empty map will be returned.

public java.io.InputStream openInternalInputFile(java.lang.String pathname)

Provides an InputStream to an internal file used by the testing infrastructure.

Parameters:

pathname: path to the internal file. Should not be null. This is a relative path to where the storage service stores the internal files. For example, if the storage service stores the input files under "/sdcard/internal_only", with a pathname "/path/to/my_input.txt", the file will end up at "/sdcard/internal_only/path/to/my_input.txt" on device.

Returns:

an InputStream to the given test file.

public java.io.OutputStream openInternalOutputFile(java.lang.String pathname)

Provides an OutputStream to an internal file used by the testing infrastructure.

Parameters:

pathname: path to the internal 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/internal_only", with a pathname "/path/to/my_output.txt", the file will end up at "/sdcard/internal_only/path/to/my_output.txt" on device.

Returns:

an OutputStream to the given output file.

Source

/*
 * 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.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.platform.io.PlatformTestStorage;
import androidx.test.services.storage.file.HostedFile;
import androidx.test.services.storage.file.HostedFile.FileHost;
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.File;
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;

/**
 * An implementation of {@link PlatformTestStorage} that uses a content provider housed in the
 * androidx.test.services apk.
 *
 * <p>As such, in order to use this implementation the test services apk must be installed on the
 * device, and thus isn't supported in environments where test services is not installed such as
 * Robolectric.
 *
 * <p>Users should ideally not reference this class directly, but rather retrieve the
 * PlatformTestStorage implementation appropriate for their execution environment from {@link
 * androidx.test.platform.io.PlatformTestStorageRegistry}.
 *
 * @hide
 */
@RestrictTo(Scope.LIBRARY_GROUP)
public final class TestStorage implements PlatformTestStorage {
  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.
   */
  @Override
  public 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.
   */
  @Override
  public Uri getOutputFileUri(@NonNull String pathname) {
    checkNotNull(pathname);
    return HostedFile.buildUri(HostedFile.FileHost.OUTPUT, pathname);
  }

  /**
   * Returns true if {@code pathname} corresponds to a file or directory that is in a directory
   * where the storage service stores files.
   *
   * @param pathname path to a file or directory. Should not be null. This is an absolute path to a
   *     file that may be a part of the storage service.
   */
  @Override
  public boolean isTestStorageFilePath(@NonNull String pathname) {
    File onDevicePathRoot =
        new File(
            HostedFile.getOutputRootDirectory(
                InstrumentationRegistry.getInstrumentation().getTargetContext()),
            TestStorageConstants.ON_DEVICE_PATH_ROOT);
    // Append a trailing slash because ON_DEVICE_PATH_ROOT has a trailing slash. If pathname already
    // has a trailing slash or other suffix, this won't affect the startsWith() matching logic.
    pathname = pathname + "/";
    return pathname.startsWith(onDevicePathRoot.getAbsolutePath());
  }

  /**
   * 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.
   * @throws FileNotFoundException if pathname does not exist
   */
  @Override
  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.
   */
  @Override
  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.
   */
  @Override
  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.
   */
  @Override
  public OutputStream openOutputFile(@NonNull String pathname) throws FileNotFoundException {
    return openOutputFile(pathname, false);
  }

  /**
   * 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.
   * @param append if true, then the lines will be added to the end of the file rather than
   *     overwriting.
   * @return an OutputStream to the given output file.
   */
  @Override
  public OutputStream openOutputFile(@NonNull String pathname, boolean append)
      throws FileNotFoundException {
    checkNotNull(pathname);
    Uri outputUri = getOutputFileUri(pathname);
    return TestStorageUtil.getOutputStream(outputUri, contentResolver, append);
  }

  /**
   * 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.
   */
  @Override
  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.
   */
  @Override
  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<>();
  }

  /**
   * Provides an InputStream to an internal file used by the testing infrastructure.
   *
   * @param pathname path to the internal file. Should not be null. This is a relative path to where
   *     the storage service stores the internal files. For example, if the storage service stores
   *     the input files under "/sdcard/internal_only", with a pathname "/path/to/my_input.txt", the
   *     file will end up at "/sdcard/internal_only/path/to/my_input.txt" on device.
   * @return an InputStream to the given test file.
   * @hide
   */
  // TODO(b/335660740): remove this unused method
  @RestrictTo(Scope.LIBRARY)
  @Override
  public InputStream openInternalInputFile(String pathname) throws FileNotFoundException {
    checkNotNull(pathname);
    Uri outputUri = HostedFile.buildUri(FileHost.INTERNAL_USE_ONLY, pathname);
    return TestStorageUtil.getInputStream(outputUri, contentResolver);
  }

  /**
   * Provides an OutputStream to an internal file used by the testing infrastructure.
   *
   * @param pathname path to the internal 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/internal_only", with a pathname "/path/to/my_output.txt", the
   *     file will end up at "/sdcard/internal_only/path/to/my_output.txt" on device.
   * @return an OutputStream to the given output file.
   * @hide
   */
  @RestrictTo(Scope.LIBRARY)
  @Override
  public OutputStream openInternalOutputFile(String pathname) throws FileNotFoundException {
    checkNotNull(pathname);
    Uri outputUri = HostedFile.buildUri(FileHost.INTERNAL_USE_ONLY, pathname);
    return TestStorageUtil.getOutputStream(outputUri, contentResolver);
  }

  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.
      }
    }
  }
}