blob: 30fd3be4dc3cdf38b5a2c78ba8bed9d013963ad9 [file] [log] [blame]
/*
* Copyright (C) 2016 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 com.android.providers.contacts.tests2;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.CancellationSignal;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.SyncState;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest;
import android.util.Log;
import junit.framework.AssertionFailedError;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
/*
* TODO The following operations would fail, not because they're not supported, but because of
* missing parameters. Fix them.
insert for 'content://com.android.contacts/contacts' failed: Aggregate contacts are created automatically
insert for 'content://com.android.contacts/raw_contacts/1/data' failed: mimetype is required
update for 'content://com.android.contacts/raw_contacts/1/stream_items/1' failed: Empty values
insert for 'content://com.android.contacts/data' failed: raw_contact_id is required
insert for 'content://com.android.contacts/settings' failed: Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE; URI: content://com.android.contacts/settings?account_type=1, calling user: com.android.providers.contacts.tests2, calling package:com.android.providers.contacts.tests2
insert for 'content://com.android.contacts/status_updates' failed: PROTOCOL and IM_HANDLE are required
insert for 'content://com.android.contacts/profile' failed: The profile contact is created automatically
insert for 'content://com.android.contacts/profile/data' failed: raw_contact_id is required
insert for 'content://com.android.contacts/profile/raw_contacts/1/data' failed: mimetype is required
insert for 'content://com.android.contacts/profile/status_updates' failed: PROTOCOL and IM_HANDLE are required
openInputStream for 'content://com.android.contacts/contacts/as_multi_vcard/XXX' failed: Caught Exception: Invalid lookup id: XXX
openInputStream for 'content://com.android.contacts/directory_file_enterprise/XXX?directory=0' failed: Caught Exception: java.lang.IllegalArgumentException: Directory is not a remote directory: content://com.android.contacts/directory_file_enterprise/XXX?directory=0
openOutputStream for 'content://com.android.contacts/directory_file_enterprise/XXX?directory=0' failed: Caught Exception: java.lang.IllegalArgumentException: Directory is not a remote directory: content://com.android.contacts/directory_file_enterprise/XXX?directory=0
*/
/**
* TODO Add test for delete/update/insert too.
* TODO Copy it to CTS
*/
@LargeTest
public class AllUriTest extends AndroidTestCase {
private static final String TAG = "AllUrlTest";
// "-" : Query not supported.
// "!" : Can't query because it requires the cross-user permission.
// The following markers are planned, but not implemented and the definition below is not all
// correct yet.
// "d" : supports delete.
// "u" : supports update.
// "i" : supports insert.
// "r" : supports read.
// "w" : supports write.
// "s" : has x_times_contacted and x_last_time_contacted.
// "t" : has x_times_used and x_last_time_used.
private static final String[][] URIs = {
{"content://com.android.contacts/contacts", "sud"},
{"content://com.android.contacts/contacts/1", "sud"},
{"content://com.android.contacts/contacts/1/data", "t"},
{"content://com.android.contacts/contacts/1/entities", "t"},
{"content://com.android.contacts/contacts/1/suggestions"},
{"content://com.android.contacts/contacts/1/suggestions/XXX"},
{"content://com.android.contacts/contacts/1/photo", "r"},
{"content://com.android.contacts/contacts/1/display_photo", "-r"},
{"content://com.android.contacts/contacts_corp/1/photo", "-r"},
{"content://com.android.contacts/contacts_corp/1/display_photo", "-r"},
{"content://com.android.contacts/contacts/filter", "s"},
{"content://com.android.contacts/contacts/filter/XXX", "s"},
{"content://com.android.contacts/contacts/lookup/nlookup", "sud"},
{"content://com.android.contacts/contacts/lookup/nlookup/data", "t"},
{"content://com.android.contacts/contacts/lookup/nlookup/photo", "tr"},
{"content://com.android.contacts/contacts/lookup/nlookup/1", "sud"},
{"content://com.android.contacts/contacts/lookup/nlookup/1/data"},
{"content://com.android.contacts/contacts/lookup/nlookup/1/photo", "r"},
{"content://com.android.contacts/contacts/lookup/nlookup/display_photo", "-r"},
{"content://com.android.contacts/contacts/lookup/nlookup/1/display_photo", "-r"},
{"content://com.android.contacts/contacts/lookup/nlookup/entities"},
{"content://com.android.contacts/contacts/lookup/nlookup/1/entities"},
{"content://com.android.contacts/contacts/as_vcard/nlookup", "r"},
{"content://com.android.contacts/contacts/as_multi_vcard/XXX"},
{"content://com.android.contacts/contacts/strequent/", "s"},
{"content://com.android.contacts/contacts/strequent/filter/XXX", "s"},
{"content://com.android.contacts/contacts/group/XXX"},
{"content://com.android.contacts/contacts/frequent", "s"},
{"content://com.android.contacts/contacts/delete_usage", "-d"},
{"content://com.android.contacts/contacts/filter_enterprise?directory=0", "s"},
{"content://com.android.contacts/contacts/filter_enterprise/XXX?directory=0", "s"},
{"content://com.android.contacts/raw_contacts", "siud"},
{"content://com.android.contacts/raw_contacts/1", "sud"},
{"content://com.android.contacts/raw_contacts/1/data", "tu"},
{"content://com.android.contacts/raw_contacts/1/display_photo", "-rw"},
{"content://com.android.contacts/raw_contacts/1/entity"},
{"content://com.android.contacts/raw_contact_entities"},
{"content://com.android.contacts/raw_contact_entities_corp", "!"},
{"content://com.android.contacts/data", "tud"},
{"content://com.android.contacts/data/1", "tudr"},
{"content://com.android.contacts/data/phones", "t"},
{"content://com.android.contacts/data_enterprise/phones", "!"},
{"content://com.android.contacts/data/phones/1", "tud"},
{"content://com.android.contacts/data/phones/filter", "t"},
{"content://com.android.contacts/data/phones/filter/XXX", "t"},
{"content://com.android.contacts/data/phones/filter_enterprise?directory=0", "t"},
{"content://com.android.contacts/data/phones/filter_enterprise/XXX?directory=0", "t"},
{"content://com.android.contacts/data/emails", "t"},
{"content://com.android.contacts/data/emails/1", "tud"},
{"content://com.android.contacts/data/emails/lookup", "t"},
{"content://com.android.contacts/data/emails/lookup/XXX", "t"},
{"content://com.android.contacts/data/emails/filter", "t"},
{"content://com.android.contacts/data/emails/filter/XXX", "t"},
{"content://com.android.contacts/data/emails/filter_enterprise?directory=0", "t"},
{"content://com.android.contacts/data/emails/filter_enterprise/XXX?directory=0", "t"},
{"content://com.android.contacts/data/emails/lookup_enterprise", "t"},
{"content://com.android.contacts/data/emails/lookup_enterprise/XXX", "t"},
{"content://com.android.contacts/data/postals", "t"},
{"content://com.android.contacts/data/postals/1", "tud"},
{"content://com.android.contacts/data/usagefeedback/1,2,3", "-u"},
{"content://com.android.contacts/data/callables/", "t"},
{"content://com.android.contacts/data/callables/1", "tud"},
{"content://com.android.contacts/data/callables/filter", "t"},
{"content://com.android.contacts/data/callables/filter/XXX", "t"},
{"content://com.android.contacts/data/callables/filter_enterprise?directory=0", "t"},
{"content://com.android.contacts/data/callables/filter_enterprise/XXX?directory=0",
"t"},
{"content://com.android.contacts/data/contactables/", "t"},
{"content://com.android.contacts/data/contactables/filter", "t"},
{"content://com.android.contacts/data/contactables/filter/XXX", "t"},
{"content://com.android.contacts/groups", "iud"},
{"content://com.android.contacts/groups/1", "ud"},
{"content://com.android.contacts/groups_summary"},
{"content://com.android.contacts/syncstate", "iud"},
{"content://com.android.contacts/syncstate/1", "-ud"},
{"content://com.android.contacts/profile/syncstate", "iud"},
{"content://com.android.contacts/phone_lookup/XXX"},
{"content://com.android.contacts/phone_lookup_enterprise/XXX"},
{"content://com.android.contacts/aggregation_exceptions", "u"},
{"content://com.android.contacts/settings", "ud"},
{"content://com.android.contacts/status_updates", "ud"},
{"content://com.android.contacts/status_updates/1"},
{"content://com.android.contacts/search_suggest_query"},
{"content://com.android.contacts/search_suggest_query/XXX"},
{"content://com.android.contacts/search_suggest_shortcut/XXX"},
{"content://com.android.contacts/provider_status"},
{"content://com.android.contacts/directories", "u"},
{"content://com.android.contacts/directories/1"},
{"content://com.android.contacts/directories_enterprise"},
{"content://com.android.contacts/directories_enterprise/1"},
{"content://com.android.contacts/complete_name"},
{"content://com.android.contacts/profile", "su"},
{"content://com.android.contacts/profile/entities", "s"},
{"content://com.android.contacts/profile/data", "tud"},
{"content://com.android.contacts/profile/data/1", "td"},
{"content://com.android.contacts/profile/photo", "t"},
{"content://com.android.contacts/profile/display_photo", "-r"},
{"content://com.android.contacts/profile/as_vcard", "r"},
{"content://com.android.contacts/profile/raw_contacts", "siud"},
// Note this should have supported update... Too late to add.
{"content://com.android.contacts/profile/raw_contacts/1", "sd"},
{"content://com.android.contacts/profile/raw_contacts/1/data", "tu"},
{"content://com.android.contacts/profile/raw_contacts/1/entity"},
{"content://com.android.contacts/profile/status_updates", "ud"},
{"content://com.android.contacts/profile/raw_contact_entities"},
{"content://com.android.contacts/display_photo/1", "-r"},
{"content://com.android.contacts/photo_dimensions"},
{"content://com.android.contacts/deleted_contacts"},
{"content://com.android.contacts/deleted_contacts/1"},
{"content://com.android.contacts/directory_file_enterprise/XXX?directory=0", "-"},
};
private static final String[] ARG1 = {"-1"};
private ContentResolver mResolver;
private ArrayList<String> mFailures;
@Override
protected void setUp() throws Exception {
super.setUp();
mFailures = new ArrayList<>();
mResolver = getContext().getContentResolver();
}
@Override
protected void tearDown() throws Exception {
if (mFailures != null) {
fail("mFailures is not null. Did you forget to call failIfFailed()?");
}
super.tearDown();
}
private void addFailure(String message, Throwable th) {
Log.e(TAG, "Failed: " + message, th);
final int MAX = 100;
if (mFailures.size() == MAX) {
mFailures.add("Too many failures.");
} else if (mFailures.size() > MAX) {
// Too many failures already...
} else {
mFailures.add(message);
}
}
private void failIfFailed() {
if (mFailures == null) {
fail("mFailures is null. Maybe called failIfFailed() twice?");
}
if (mFailures.size() > 0) {
StringBuilder message = new StringBuilder();
if (mFailures.size() > 0) {
Log.e(TAG, "Something went wrong:");
for (String s : mFailures) {
Log.e(TAG, s);
if (message.length() > 0) {
message.append("\n");
}
message.append(s);
}
}
mFailures = null;
fail("Following test(s) failed:\n" + message);
}
mFailures = null;
}
private static Uri getUri(String[] path) {
return Uri.parse(path[0]);
}
private static boolean supportsQuery(String[] path) {
if (path.length == 1) {
return true; // supports query by default.
}
return !(path[1].contains("-") || path[1].contains("!"));
}
private static boolean supportsInsert(String[] path) {
return (path.length) >= 2 && path[1].contains("i");
}
private static boolean supportsUpdate(String[] path) {
return (path.length) >= 2 && path[1].contains("u");
}
private static boolean supportsDelete(String[] path) {
return (path.length) >= 2 && path[1].contains("d");
}
private static boolean supportsRead(String[] path) {
return (path.length) >= 2 && path[1].contains("r");
}
private static boolean supportsWrite(String[] path) {
return (path.length) >= 2 && path[1].contains("w");
}
private String[] getColumns(Uri uri) {
try (Cursor c = mResolver.query(uri,
null, // projection
"1=2", // selection
null, // selection args
null // sort order
)) {
return c.getColumnNames();
}
}
private void checkQueryExecutable(Uri uri,
String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
try {
try (Cursor c = mResolver.query(uri, projection, selection,
selectionArgs, sortOrder)) {
c.moveToFirst();
}
} catch (Throwable th) {
addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th);
}
try {
// With CancellationSignal.
try (Cursor c = mResolver.query(uri, projection, selection,
selectionArgs, sortOrder, new CancellationSignal())) {
c.moveToFirst();
}
} catch (Throwable th) {
addFailure("Query with cancel failed: URI=" + uri + " Message=" + th.getMessage(), th);
}
try {
// With limit.
try (Cursor c = mResolver.query(
uri.buildUpon().appendQueryParameter(
ContactsContract.LIMIT_PARAM_KEY, "0").build(),
projection, selection, selectionArgs, sortOrder)) {
c.moveToFirst();
}
} catch (Throwable th) {
addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage(), th);
}
try {
// With account.
try (Cursor c = mResolver.query(
uri.buildUpon()
.appendQueryParameter(RawContacts.ACCOUNT_NAME, "a")
.appendQueryParameter(RawContacts.ACCOUNT_TYPE, "b")
.appendQueryParameter(RawContacts.DATA_SET, "c")
.build(),
projection, selection, selectionArgs, sortOrder)) {
c.moveToFirst();
}
} catch (Throwable th) {
addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage(), th);
}
}
private void checkQueryNotExecutable(Uri uri,
String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
try {
try (Cursor c = mResolver.query(uri, projection, selection,
selectionArgs, sortOrder)) {
c.moveToFirst();
}
} catch (Throwable th) {
// pass.
return;
}
addFailure("Query on " + uri + " expected to fail, but succeeded.", null);
}
/**
* Make sure all URLs are accessible with all arguments = null.
*/
public void testSelect() {
for (String[] path : URIs) {
if (!supportsQuery(path)) continue;
try {
final Uri uri = getUri(path);
checkQueryExecutable(uri, // uri
null, // projection
null, // selection
null, // selection args
null // sort order
);
} catch (Throwable th) {
addFailure("Failed: URI=" + path[0] + " Message=" + th.getMessage(), th);
}
}
failIfFailed();
}
public void testNoHiddenColumns() {
for (String[] path : URIs) {
if (!supportsQuery(path)) continue;
try {
final Uri uri = getUri(path);
for (String column : getColumns(uri)) {
if (column.toLowerCase().startsWith(ContactsContract.HIDDEN_COLUMN_PREFIX)) {
addFailure("Uri " + uri + " returned hidden column " + column, null);
}
}
} catch (Throwable th) {
addFailure("Failed: URI=" + path[0] + " Message=" + th.getMessage(), th);
}
}
failIfFailed();
}
// Temporarily disabled due to taking too much time.
// /**
// * Make sure all URLs are accessible with a projection.
// */
// public void testSelectWithProjection() {
// for (String[] path : URIs) {
// if (!supportsQuery(path)) continue;
// final Uri uri = getUri(path);
//
// for (String column : getColumns(uri)) {
// // Some columns are not selectable alone due to bugs, and we don't want to fix them
// // in order to avoid expanding the differences between versions, so here're some
// // hacks to make it work...
//
// String[] projection = {column};
//
// final String u = path[0];
// if ((u.startsWith("content://com.android.contacts/status_updates")
// || u.startsWith("content://com.android.contacts/profile/status_updates"))
// && ("im_handle".equals(column)
// || "im_account".equals(column)
// || "protocol".equals(column)
// || "custom_protocol".equals(column)
// || "presence_raw_contact_id".equals(column)
// )) {
// // These columns only show up when the projection contains certain columns.
//
// projection = new String[]{"mode", column};
// } else if ((u.startsWith("content://com.android.contacts/search_suggest_query")
// || u.startsWith("content://contacts/search_suggest_query"))
// && "suggest_intent_action".equals(column)) {
// // Can't be included in the projection due to a bug in GlobalSearchSupport.
// continue;
// } else if (RawContacts.BACKUP_ID.equals(column)) {
// // Some URIs don't support a projection with BAKCUP_ID only.
// projection = new String[]{RawContacts.BACKUP_ID, RawContacts.SOURCE_ID};
// }
//
// checkQueryExecutable(uri,
// projection, // projection
// null, // selection
// null, // selection args
// null // sort order
// );
// }
// }
// failIfFailed();
// }
/**
* Make sure all URLs are accessible with a selection.
*/
public void testSelectWithSelection() {
for (String[] path : URIs) {
if (!supportsQuery(path)) continue;
final Uri uri = getUri(path);
checkQueryExecutable(uri,
null, // projection
"1=?", // selection
ARG1, // , // selection args
null // sort order
);
}
failIfFailed();
}
// /**
// * Make sure all URLs are accessible with a selection.
// */
// public void testSelectWithSelectionUsingColumns() {
// for (String[] path : URIs) {
// if (!supportsQuery(path)) continue;
// final Uri uri = getUri(path);
//
// for (String column : getColumns(uri)) {
// checkQueryExecutable(uri,
// null, // projection
// column + "=?", // selection
// ARG1, // , // selection args
// null // sort order
// );
// }
// }
// failIfFailed();
// }
// Temporarily disabled due to taking too much time.
// /**
// * Make sure all URLs are accessible with an order-by.
// */
// public void testSelectWithSortOrder() {
// for (String[] path : URIs) {
// if (!supportsQuery(path)) continue;
// final Uri uri = getUri(path);
//
// for (String column : getColumns(uri)) {
// checkQueryExecutable(uri,
// null, // projection
// "1=2", // selection
// null, // , // selection args
// column // sort order
// );
// }
// }
// failIfFailed();
// }
/**
* Make sure all URLs are accessible with all arguments.
*/
public void testSelectWithAllArgs() {
for (String[] path : URIs) {
if (!supportsQuery(path)) continue;
final Uri uri = getUri(path);
final String[] projection = {getColumns(uri)[0]};
checkQueryExecutable(uri,
projection, // projection
"1=?", // selection
ARG1, // , // selection args
getColumns(uri)[0] // sort order
);
}
failIfFailed();
}
public void testNonSelect() {
for (String[] path : URIs) {
if (supportsQuery(path)) continue;
final Uri uri = getUri(path);
checkQueryNotExecutable(uri, // uri
null, // projection
null, // selection
null, // selection args
null // sort order
);
}
failIfFailed();
}
private static boolean supportsTimesContacted(String[] path) {
return path.length > 1 && path[1].contains("s");
}
private static boolean supportsTimesUsed(String[] path) {
return path.length > 1 && path[1].contains("t");
}
private void checkColumnAccessible(Uri uri, String column) {
try {
try (Cursor c = mResolver.query(
uri, new String[]{column}, column + "=0", null, column
)) {
c.moveToFirst();
}
} catch (Throwable th) {
addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th);
}
}
/** Test for {@link #checkColumnAccessible} */
public void testCheckColumnAccessible() {
checkColumnAccessible(Contacts.CONTENT_URI, "x_times_contacted");
try {
failIfFailed();
} catch (AssertionFailedError expected) {
return; // expected.
}
fail("Failed to detect issue.");
}
private void checkColumnNotAccessibleInner(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
try {
try (Cursor c = mResolver.query(uri, projection, selection,
selectionArgs, sortOrder)) {
c.moveToFirst();
}
} catch (IllegalArgumentException th) {
// pass.
return;
}
addFailure("Query on " + uri +
" expected to throw IllegalArgumentException, but succeeded.", null);
}
private void checkColumnNotAccessible(Uri uri, String column) {
checkColumnNotAccessibleInner(uri, new String[] {column}, null, null, null);
checkColumnNotAccessibleInner(uri, null, column + "=1", null, null);
checkColumnNotAccessibleInner(uri, null, null, null, /* order by */ column);
}
/** Test for {@link #checkColumnNotAccessible} */
public void testCheckColumnNotAccessible() {
checkColumnNotAccessible(Contacts.CONTENT_URI, "times_contacted");
try {
failIfFailed();
} catch (AssertionFailedError expected) {
return; // expected.
}
fail("Failed to detect issue.");
}
/**
* Make sure the x_ columns are not accessible.
*/
public void testProhibitedColumns() {
for (String[] path : URIs) {
final Uri uri = getUri(path);
if (supportsTimesContacted(path)) {
checkColumnAccessible(uri, "times_contacted");
checkColumnAccessible(uri, "last_time_contacted");
checkColumnNotAccessible(uri, "X_times_contacted");
checkColumnNotAccessible(uri, "X_slast_time_contacted");
}
if (supportsTimesUsed(path)) {
checkColumnAccessible(uri, "times_used");
checkColumnAccessible(uri, "last_time_used");
checkColumnNotAccessible(uri, "X_times_used");
checkColumnNotAccessible(uri, "X_last_time_used");
}
}
failIfFailed();
}
private void checkExecutable(String operation, Uri uri, boolean shouldWork, Runnable r) {
if (shouldWork) {
try {
r.run();
} catch (Exception e) {
addFailure(operation + " for '" + uri + "' failed: " + e.getMessage(), e);
}
} else {
try {
r.run();
addFailure(operation + " for '" + uri + "' NOT failed.", null);
} catch (Exception expected) {
}
}
}
public void testAllOperations() {
final ContentValues cv = new ContentValues();
for (String[] path : URIs) {
final Uri uri = getUri(path);
try {
cv.clear();
if (supportsQuery(path)) {
cv.put(getColumns(uri)[0], 1);
} else {
cv.put("_id", 1);
}
if (uri.toString().contains("syncstate")) {
cv.put(SyncState.ACCOUNT_NAME, "abc");
cv.put(SyncState.ACCOUNT_TYPE, "def");
}
checkExecutable("insert", uri, supportsInsert(path), () -> {
final Uri newUri = mResolver.insert(uri, cv);
if (newUri == null) {
addFailure("Insert for '" + uri + "' returned null.", null);
} else {
// "profile/raw_contacts/#" is missing update support. too late to add, so
// just skip.
if (!newUri.toString().startsWith(
"content://com.android.contacts/profile/raw_contacts/")) {
checkExecutable("insert -> update", newUri, true, () -> {
mResolver.update(newUri, cv, null, null);
});
}
checkExecutable("insert -> delete", newUri, true, () -> {
mResolver.delete(newUri, null, null);
});
}
});
checkExecutable("update", uri, supportsUpdate(path), () -> {
mResolver.update(uri, cv, "1=2", null);
});
checkExecutable("delete", uri, supportsDelete(path), () -> {
mResolver.delete(uri, "1=2", null);
});
} catch (Throwable th) {
addFailure("Failed: URI=" + uri + " Message=" + th.getMessage(), th);
}
}
failIfFailed();
}
public void testAllFileOperations() {
for (String[] path : URIs) {
final Uri uri = getUri(path);
checkExecutable("openInputStream", uri, supportsRead(path), () -> {
try (InputStream st = mResolver.openInputStream(uri)) {
} catch (FileNotFoundException e) {
// TODO This happens because we try to read nonexistent photos. Ideally
// we should actually check it's readable.
if (e.getMessage().contains("Stream I/O not supported")) {
throw new RuntimeException("Caught Exception: " + e.toString(), e);
}
} catch (Exception e) {
throw new RuntimeException("Caught Exception: " + e.toString(), e);
}
});
checkExecutable("openOutputStream", uri, supportsWrite(path), () -> {
try (OutputStream st = mResolver.openOutputStream(uri)) {
} catch (Exception e) {
throw new RuntimeException("Caught Exception: " + e.toString(), e);
}
});
}
failIfFailed();
}
}