/*
* 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.
*
* <p>This is used to expose readonly copies of tests data dependencies and also provides a
* standardized way of exposing test output.
*
* <p>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.
}
}