623 lines
23 KiB
Java
623 lines
23 KiB
Java
/*
|
|
* 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.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
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
|
|
| Root.FLAG_SUPPORTS_IS_CHILD);
|
|
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_RENAME;
|
|
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 boolean isChildDocument(String parentDocumentId, String documentId) {
|
|
if (TextUtils.equals(parentDocumentId, documentId)) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
final long parentDocId = Long.parseLong(parentDocumentId);
|
|
final EncryptedDocument parentDoc = getDocument(parentDocId);
|
|
|
|
// Recursively search any children
|
|
// TODO: consider building an index to optimize this check
|
|
final JSONObject meta = parentDoc.readMetadata();
|
|
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 String childDocumentId = children.getString(i);
|
|
if (isChildDocument(childDocumentId, documentId)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
throw new IllegalStateException(e);
|
|
} catch (GeneralSecurityException e) {
|
|
throw new IllegalStateException(e);
|
|
} catch (JSONException e) {
|
|
throw new IllegalStateException(e);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@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 String renameDocument(String documentId, String displayName)
|
|
throws FileNotFoundException {
|
|
final long docId = Long.parseLong(documentId);
|
|
|
|
try {
|
|
final EncryptedDocument doc = getDocument(docId);
|
|
final JSONObject meta = doc.readMetadata();
|
|
|
|
meta.put(Document.COLUMN_DISPLAY_NAME, displayName);
|
|
doc.writeMetadataAndContent(meta, null);
|
|
|
|
return null;
|
|
|
|
} catch (IOException e) {
|
|
throw new IllegalStateException(e);
|
|
} catch (GeneralSecurityException e) {
|
|
throw new IllegalStateException(e);
|
|
} catch (JSONException e) {
|
|
throw new IllegalStateException(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;
|
|
}
|
|
}
|
|
}
|