Vault example documents provider.
Example provider that encrypts both metadata and contents of documents stored inside. It shows advanced usage of new storage access APIs and hardware-backed key chain. Change-Id: I2cdf4e949be8471c3d8b4f45ec0681c9248ea09c
This commit is contained in:
565
samples/Vault/src/com/example/android/vault/VaultProvider.java
Normal file
565
samples/Vault/src/com/example/android/vault/VaultProvider.java
Normal file
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.example.android.vault;
|
||||
|
||||
import static com.example.android.vault.EncryptedDocument.DATA_KEY_LENGTH;
|
||||
import static com.example.android.vault.EncryptedDocument.MAC_KEY_LENGTH;
|
||||
import static com.example.android.vault.Utils.closeQuietly;
|
||||
import static com.example.android.vault.Utils.closeWithErrorQuietly;
|
||||
import static com.example.android.vault.Utils.readFully;
|
||||
import static com.example.android.vault.Utils.writeFully;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MatrixCursor.RowBuilder;
|
||||
import android.os.Bundle;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.security.KeyChain;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.vault.R;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Provider that encrypts both metadata and contents of documents stored inside.
|
||||
* Each document is stored as described by {@link EncryptedDocument} with
|
||||
* separate metadata and content sections. Directories are just
|
||||
* {@link EncryptedDocument} instances without a content section, and a list of
|
||||
* child documents included in the metadata section.
|
||||
* <p>
|
||||
* All content is encrypted/decrypted on demand through pipes, using
|
||||
* {@link ParcelFileDescriptor#createReliablePipe()} to detect and recover from
|
||||
* remote crashes and errors.
|
||||
* <p>
|
||||
* Our symmetric encryption key is stored on disk only after using
|
||||
* {@link SecretKeyWrapper} to "wrap" it using another public/private key pair
|
||||
* stored in the platform {@link KeyStore}. This allows us to protect our
|
||||
* symmetric key with hardware-backed keys, if supported. Devices without
|
||||
* hardware support still encrypt their keys while at rest, and the platform
|
||||
* always requires a user to present a PIN, password, or pattern to unlock the
|
||||
* KeyStore before use.
|
||||
*/
|
||||
public class VaultProvider extends DocumentsProvider {
|
||||
public static final String TAG = "Vault";
|
||||
|
||||
static final String AUTHORITY = "com.example.android.vault.provider";
|
||||
|
||||
static final String DEFAULT_ROOT_ID = "vault";
|
||||
static final String DEFAULT_DOCUMENT_ID = "0";
|
||||
|
||||
/** JSON key storing array of all children documents in a directory. */
|
||||
private static final String KEY_CHILDREN = "vault:children";
|
||||
|
||||
/** Key pointing to next available document ID. */
|
||||
private static final String PREF_NEXT_ID = "next_id";
|
||||
|
||||
/** Blob used to derive {@link #mDataKey} from our secret key. */
|
||||
private static final byte[] BLOB_DATA = "DATA".getBytes(StandardCharsets.UTF_8);
|
||||
/** Blob used to derive {@link #mMacKey} from our secret key. */
|
||||
private static final byte[] BLOB_MAC = "MAC".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
|
||||
Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
|
||||
Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, Root.COLUMN_SUMMARY
|
||||
};
|
||||
|
||||
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
|
||||
Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
|
||||
};
|
||||
|
||||
private static String[] resolveRootProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
|
||||
}
|
||||
|
||||
private static String[] resolveDocumentProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
|
||||
}
|
||||
|
||||
private final Object mIdLock = new Object();
|
||||
|
||||
/**
|
||||
* Flag indicating that the {@link SecretKeyWrapper} public/private key is
|
||||
* hardware-backed. A software keystore is more vulnerable to offline
|
||||
* attacks if the device is compromised.
|
||||
*/
|
||||
private boolean mHardwareBacked;
|
||||
|
||||
/** File where wrapped symmetric key is stored. */
|
||||
private File mKeyFile;
|
||||
/** Directory where all encrypted documents are stored. */
|
||||
private File mDocumentsDir;
|
||||
|
||||
private SecretKey mDataKey;
|
||||
private SecretKey mMacKey;
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
mHardwareBacked = KeyChain.isBoundKeyAlgorithm("RSA");
|
||||
|
||||
mKeyFile = new File(getContext().getFilesDir(), "vault.key");
|
||||
mDocumentsDir = new File(getContext().getFilesDir(), "documents");
|
||||
mDocumentsDir.mkdirs();
|
||||
|
||||
try {
|
||||
// Load secret key and ensure our root document is ready.
|
||||
loadOrGenerateKeys(getContext(), mKeyFile);
|
||||
initDocument(Long.parseLong(DEFAULT_DOCUMENT_ID), Document.MIME_TYPE_DIR, null);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for testing.
|
||||
*/
|
||||
void wipeAllContents() throws IOException, GeneralSecurityException {
|
||||
for (File f : mDocumentsDir.listFiles()) {
|
||||
f.delete();
|
||||
}
|
||||
|
||||
initDocument(Long.parseLong(DEFAULT_DOCUMENT_ID), Document.MIME_TYPE_DIR, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load our symmetric secret key and use it to derive two different data and
|
||||
* MAC keys. The symmetric secret key is stored securely on disk by wrapping
|
||||
* it with a public/private key pair, possibly backed by hardware.
|
||||
*/
|
||||
private void loadOrGenerateKeys(Context context, File keyFile)
|
||||
throws GeneralSecurityException, IOException {
|
||||
final SecretKeyWrapper wrapper = new SecretKeyWrapper(context, TAG);
|
||||
|
||||
// Generate secret key if none exists
|
||||
if (!keyFile.exists()) {
|
||||
final byte[] raw = new byte[DATA_KEY_LENGTH];
|
||||
new SecureRandom().nextBytes(raw);
|
||||
|
||||
final SecretKey key = new SecretKeySpec(raw, "AES");
|
||||
final byte[] wrapped = wrapper.wrap(key);
|
||||
|
||||
writeFully(keyFile, wrapped);
|
||||
}
|
||||
|
||||
// Even if we just generated the key, always read it back to ensure we
|
||||
// can read it successfully.
|
||||
final byte[] wrapped = readFully(keyFile);
|
||||
final SecretKey key = wrapper.unwrap(wrapped);
|
||||
|
||||
final Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(key);
|
||||
|
||||
// Derive two different keys for encryption and authentication.
|
||||
final byte[] rawDataKey = new byte[DATA_KEY_LENGTH];
|
||||
final byte[] rawMacKey = new byte[MAC_KEY_LENGTH];
|
||||
|
||||
System.arraycopy(mac.doFinal(BLOB_DATA), 0, rawDataKey, 0, rawDataKey.length);
|
||||
System.arraycopy(mac.doFinal(BLOB_MAC), 0, rawMacKey, 0, rawMacKey.length);
|
||||
|
||||
mDataKey = new SecretKeySpec(rawDataKey, "AES");
|
||||
mMacKey = new SecretKeySpec(rawMacKey, "HmacSHA256");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
|
||||
final RowBuilder row = result.newRow();
|
||||
row.add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID);
|
||||
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY);
|
||||
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_label));
|
||||
row.add(Root.COLUMN_DOCUMENT_ID, DEFAULT_DOCUMENT_ID);
|
||||
row.add(Root.COLUMN_ICON, R.drawable.ic_lock_lock);
|
||||
|
||||
// Notify user in storage UI when key isn't hardware-backed
|
||||
if (!mHardwareBacked) {
|
||||
row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.info_software));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private EncryptedDocument getDocument(long docId) throws GeneralSecurityException {
|
||||
final File file = new File(mDocumentsDir, String.valueOf(docId));
|
||||
return new EncryptedDocument(docId, file, mDataKey, mMacKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Include metadata for a document in the given result cursor.
|
||||
*/
|
||||
private void includeDocument(MatrixCursor result, long docId)
|
||||
throws IOException, GeneralSecurityException {
|
||||
final EncryptedDocument doc = getDocument(docId);
|
||||
if (!doc.getFile().exists()) {
|
||||
throw new FileNotFoundException("Missing document " + docId);
|
||||
}
|
||||
|
||||
final JSONObject meta = doc.readMetadata();
|
||||
|
||||
int flags = 0;
|
||||
|
||||
final String mimeType = meta.optString(Document.COLUMN_MIME_TYPE);
|
||||
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
} else {
|
||||
flags |= Document.FLAG_SUPPORTS_WRITE;
|
||||
}
|
||||
flags |= Document.FLAG_SUPPORTS_DELETE;
|
||||
|
||||
final RowBuilder row = result.newRow();
|
||||
row.add(Document.COLUMN_DOCUMENT_ID, meta.optLong(Document.COLUMN_DOCUMENT_ID));
|
||||
row.add(Document.COLUMN_DISPLAY_NAME, meta.optString(Document.COLUMN_DISPLAY_NAME));
|
||||
row.add(Document.COLUMN_SIZE, meta.optLong(Document.COLUMN_SIZE));
|
||||
row.add(Document.COLUMN_MIME_TYPE, mimeType);
|
||||
row.add(Document.COLUMN_FLAGS, flags);
|
||||
row.add(Document.COLUMN_LAST_MODIFIED, meta.optLong(Document.COLUMN_LAST_MODIFIED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createDocument(String parentDocumentId, String mimeType, String displayName)
|
||||
throws FileNotFoundException {
|
||||
final long parentDocId = Long.parseLong(parentDocumentId);
|
||||
|
||||
// Allocate the next available ID
|
||||
final long childDocId;
|
||||
synchronized (mIdLock) {
|
||||
final SharedPreferences prefs = getContext()
|
||||
.getSharedPreferences(PREF_NEXT_ID, Context.MODE_PRIVATE);
|
||||
childDocId = prefs.getLong(PREF_NEXT_ID, 1);
|
||||
if (!prefs.edit().putLong(PREF_NEXT_ID, childDocId + 1).commit()) {
|
||||
throw new IllegalStateException("Failed to allocate document ID");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
initDocument(childDocId, mimeType, displayName);
|
||||
|
||||
// Update parent to reference new child
|
||||
final EncryptedDocument parentDoc = getDocument(parentDocId);
|
||||
final JSONObject parentMeta = parentDoc.readMetadata();
|
||||
parentMeta.accumulate(KEY_CHILDREN, childDocId);
|
||||
parentDoc.writeMetadataAndContent(parentMeta, null);
|
||||
|
||||
return String.valueOf(childDocId);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (JSONException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create document on disk, writing an initial metadata section. Someone
|
||||
* might come back later to write contents.
|
||||
*/
|
||||
private void initDocument(long docId, String mimeType, String displayName)
|
||||
throws IOException, GeneralSecurityException {
|
||||
final EncryptedDocument doc = getDocument(docId);
|
||||
if (doc.getFile().exists()) return;
|
||||
|
||||
try {
|
||||
final JSONObject meta = new JSONObject();
|
||||
meta.put(Document.COLUMN_DOCUMENT_ID, docId);
|
||||
meta.put(Document.COLUMN_MIME_TYPE, mimeType);
|
||||
meta.put(Document.COLUMN_DISPLAY_NAME, displayName);
|
||||
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||
meta.put(KEY_CHILDREN, new JSONArray());
|
||||
}
|
||||
|
||||
doc.writeMetadataAndContent(meta, null);
|
||||
} catch (JSONException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDocument(String documentId) throws FileNotFoundException {
|
||||
final long docId = Long.parseLong(documentId);
|
||||
|
||||
try {
|
||||
// Delete given document, any children documents under it, and any
|
||||
// references to it from parents.
|
||||
deleteDocumentTree(docId);
|
||||
deleteDocumentReferences(docId);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete the given document and any children under it.
|
||||
*/
|
||||
private void deleteDocumentTree(long docId) throws IOException, GeneralSecurityException {
|
||||
final EncryptedDocument doc = getDocument(docId);
|
||||
final JSONObject meta = doc.readMetadata();
|
||||
try {
|
||||
if (Document.MIME_TYPE_DIR.equals(meta.getString(Document.COLUMN_MIME_TYPE))) {
|
||||
final JSONArray children = meta.getJSONArray(KEY_CHILDREN);
|
||||
for (int i = 0; i < children.length(); i++) {
|
||||
final long childDocId = children.getLong(i);
|
||||
deleteDocumentTree(childDocId);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
if (!doc.getFile().delete()) {
|
||||
throw new IOException("Failed to delete " + docId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any references to the given document, usually when included as a
|
||||
* child of another directory.
|
||||
*/
|
||||
private void deleteDocumentReferences(long docId) {
|
||||
for (String name : mDocumentsDir.list()) {
|
||||
try {
|
||||
final long parentDocId = Long.parseLong(name);
|
||||
final EncryptedDocument parentDoc = getDocument(parentDocId);
|
||||
final JSONObject meta = parentDoc.readMetadata();
|
||||
|
||||
if (Document.MIME_TYPE_DIR.equals(meta.getString(Document.COLUMN_MIME_TYPE))) {
|
||||
final JSONArray children = meta.getJSONArray(KEY_CHILDREN);
|
||||
if (maybeRemove(children, docId)) {
|
||||
Log.d(TAG, "Removed " + docId + " reference from " + name);
|
||||
parentDoc.writeMetadataAndContent(meta, null);
|
||||
|
||||
getContext().getContentResolver().notifyChange(
|
||||
DocumentsContract.buildChildDocumentsUri(AUTHORITY, name), null,
|
||||
false);
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to examine " + name, e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w(TAG, "Failed to examine " + name, e);
|
||||
} catch (JSONException e) {
|
||||
Log.w(TAG, "Failed to examine " + name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryDocument(String documentId, String[] projection)
|
||||
throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
|
||||
try {
|
||||
includeDocument(result, Long.parseLong(documentId));
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryChildDocuments(
|
||||
String parentDocumentId, String[] projection, String sortOrder)
|
||||
throws FileNotFoundException {
|
||||
final ExtrasMatrixCursor result = new ExtrasMatrixCursor(
|
||||
resolveDocumentProjection(projection));
|
||||
result.setNotificationUri(getContext().getContentResolver(),
|
||||
DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId));
|
||||
|
||||
// Notify user in storage UI when key isn't hardware-backed
|
||||
if (!mHardwareBacked) {
|
||||
result.putString(DocumentsContract.EXTRA_INFO,
|
||||
getContext().getString(R.string.info_software_detail));
|
||||
}
|
||||
|
||||
try {
|
||||
final EncryptedDocument doc = getDocument(Long.parseLong(parentDocumentId));
|
||||
final JSONObject meta = doc.readMetadata();
|
||||
final JSONArray children = meta.getJSONArray(KEY_CHILDREN);
|
||||
for (int i = 0; i < children.length(); i++) {
|
||||
final long docId = children.getLong(i);
|
||||
includeDocument(result, docId);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (JSONException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openDocument(
|
||||
String documentId, String mode, CancellationSignal signal)
|
||||
throws FileNotFoundException {
|
||||
final long docId = Long.parseLong(documentId);
|
||||
|
||||
try {
|
||||
final EncryptedDocument doc = getDocument(docId);
|
||||
if ("r".equals(mode)) {
|
||||
return startRead(doc);
|
||||
} else if ("w".equals(mode) || "wt".equals(mode)) {
|
||||
return startWrite(doc);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported mode: " + mode);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off a thread to handle a read request for the given document.
|
||||
* Internally creates a pipe and returns the read end for returning to a
|
||||
* remote process.
|
||||
*/
|
||||
private ParcelFileDescriptor startRead(final EncryptedDocument doc) throws IOException {
|
||||
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
|
||||
final ParcelFileDescriptor readEnd = pipe[0];
|
||||
final ParcelFileDescriptor writeEnd = pipe[1];
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
doc.readContent(writeEnd);
|
||||
Log.d(TAG, "Success reading " + doc);
|
||||
closeQuietly(writeEnd);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed reading " + doc, e);
|
||||
closeWithErrorQuietly(writeEnd, e.toString());
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w(TAG, "Failed reading " + doc, e);
|
||||
closeWithErrorQuietly(writeEnd, e.toString());
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
return readEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off a thread to handle a write request for the given document.
|
||||
* Internally creates a pipe and returns the write end for returning to a
|
||||
* remote process.
|
||||
*/
|
||||
private ParcelFileDescriptor startWrite(final EncryptedDocument doc) throws IOException {
|
||||
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
|
||||
final ParcelFileDescriptor readEnd = pipe[0];
|
||||
final ParcelFileDescriptor writeEnd = pipe[1];
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final JSONObject meta = doc.readMetadata();
|
||||
doc.writeMetadataAndContent(meta, readEnd);
|
||||
Log.d(TAG, "Success writing " + doc);
|
||||
closeQuietly(readEnd);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed writing " + doc, e);
|
||||
closeWithErrorQuietly(readEnd, e.toString());
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w(TAG, "Failed writing " + doc, e);
|
||||
closeWithErrorQuietly(readEnd, e.toString());
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
return writeEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe remove the given value from a {@link JSONArray}.
|
||||
*
|
||||
* @return if the array was mutated.
|
||||
*/
|
||||
private static boolean maybeRemove(JSONArray array, long value) throws JSONException {
|
||||
boolean mutated = false;
|
||||
int i = 0;
|
||||
while (i < array.length()) {
|
||||
if (value == array.getLong(i)) {
|
||||
array.remove(i);
|
||||
mutated = true;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return mutated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple extension of {@link MatrixCursor} that makes it easy to provide a
|
||||
* {@link Bundle} of extras.
|
||||
*/
|
||||
private static class ExtrasMatrixCursor extends MatrixCursor {
|
||||
private Bundle mExtras;
|
||||
|
||||
public ExtrasMatrixCursor(String[] columnNames) {
|
||||
super(columnNames);
|
||||
}
|
||||
|
||||
public void putString(String key, String value) {
|
||||
if (mExtras == null) {
|
||||
mExtras = new Bundle();
|
||||
}
|
||||
mExtras.putString(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getExtras() {
|
||||
return mExtras;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user