// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.util;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.support.v4.content.FileProvider;
import java.io.FileNotFoundException;
import java.util.Arrays;
/**
* A file provider class that can share a potentially non-existent file and blocks the client
* application from accessing the file till it is written.
*
* This class serves as the default file provider, but also lets us share blocked files.
* For blocked files, it generates an unique identifier for the file to be shared and the embedder
* must write the file and notify that the file is ready using the unique uri generated. The client
* application is blocked from accessing the file till the file is ready. This provider allows only
* one blocked file to be shared at a given time.
*/
public class ChromeFileProvider extends FileProvider {
private static final String AUTHORITY_SUFFIX = ".FileProvider";
private static final String BLOCKED_FILE_PREFIX = "BlockedFile_";
// All these static objects must be accesseed in a synchronized block:
private static Object sLock = new Object();
private static boolean sIsFileReady;
private static Uri sCurrentBlockingUri;
private static Uri sFileUri;
/**
* Returns an unique uri to identify the file to be shared and block access to it till
* notifyFileReady is called.
*
* This function clobbers any uri that was previously created and the client application
* accessing those uri will get a null file descriptor.
* @param context Activity context that is used to access package manager.
*/
public static Uri generateUriAndBlockAccess(final Context context) {
String authority = context.getPackageName() + AUTHORITY_SUFFIX;
String fileName = BLOCKED_FILE_PREFIX + String.valueOf(System.nanoTime());
Uri blockingUri =
new Uri.Builder().scheme("content").authority(authority).path(fileName).build();
synchronized (sLock) {
sCurrentBlockingUri = blockingUri;
sFileUri = null;
sIsFileReady = false;
// In case the previous file never got ready.
sLock.notify();
}
return blockingUri;
}
/**
* Notify that the file is ready to be accessed by the client application.
*
* @param blockingUri The unique uri that was generated by generateUriAndBlockAccess.
* @param fileUri The Uri for actual file given by FileProvider.
*/
public static void notifyFileReady(Uri blockingUri, Uri fileUri) {
synchronized (sLock) {
sFileUri = fileUri;
// Ready is set only if the current file is ready.
sIsFileReady = doesMatchCurrentBlockingUri(blockingUri);
sLock.notify();
}
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
Uri fileUri = getFileUriWhenReady(uri);
return fileUri != null ? super.openFile(fileUri, mode) : null;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Uri fileUri = getFileUriWhenReady(uri);
if (fileUri == null) return null;
// Workaround for a bad assumption that particular MediaStore columns exist by certain third
// party applications.
// http://crbug.com/467423.
Cursor source = super.query(fileUri, projection, selection, selectionArgs, sortOrder);
String[] columnNames = source.getColumnNames();
String[] newColumnNames = columnNamesWithData(columnNames);
if (columnNames == newColumnNames) return source;
MatrixCursor cursor = new MatrixCursor(newColumnNames, source.getCount());
source.moveToPosition(-1);
while (source.moveToNext()) {
MatrixCursor.RowBuilder row = cursor.newRow();
for (int i = 0; i < columnNames.length; i++) {
switch (source.getType(i)) {
case Cursor.FIELD_TYPE_INTEGER:
row.add(source.getInt(i));
break;
case Cursor.FIELD_TYPE_FLOAT:
row.add(source.getFloat(i));
break;
case Cursor.FIELD_TYPE_STRING:
row.add(source.getString(i));
break;
case Cursor.FIELD_TYPE_BLOB:
row.add(source.getBlob(i));
break;
case Cursor.FIELD_TYPE_NULL:
default:
row.add(null);
break;
}
}
}
source.close();
return cursor;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (uri != null && uri.getPath().contains(BLOCKED_FILE_PREFIX)) {
synchronized (sLock) {
if (!doesMatchCurrentBlockingUri(uri)) return 0;
sFileUri = null;
sIsFileReady = false;
sCurrentBlockingUri = null;
}
}
return super.delete(uri, selection, selectionArgs);
}
/**
* Waits and returns file uri iff the file is ready to be accessed, or returns null if file is
* replaced.
*/
protected static Uri getFileUriWhenReady(Uri uri) {
// If the uri passed is not a blocked file, then the given uri can be directly used.
if (uri == null || !uri.getPath().contains(BLOCKED_FILE_PREFIX)) return uri;
synchronized (sLock) {
// Wait only if the file is not ready and the current file has not changed.
while (!sIsFileReady && doesMatchCurrentBlockingUri(uri)) {
try {
sLock.wait();
} catch (InterruptedException e) {
break;
}
}
// If the current file has changed while waiting, return null.
if (doesMatchCurrentBlockingUri(uri)) return sFileUri;
}
return null;
}
private static boolean doesMatchCurrentBlockingUri(Uri uri) {
return uri != null && sCurrentBlockingUri != null && sCurrentBlockingUri.equals(uri);
}
private String[] columnNamesWithData(String[] columnNames) {
for (String columnName : columnNames) {
if (MediaStore.MediaColumns.DATA.equals(columnName)) return columnNames;
}
String[] newColumnNames = Arrays.copyOf(columnNames, columnNames.length + 1);
newColumnNames[columnNames.length] = MediaStore.MediaColumns.DATA;
return newColumnNames;
}
}