blob: 5c1248437729fd0788d6e9384ee17ccf946b74c2 [file] [log] [blame]
/*
* 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.android.externalstorage;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentColumns;
import android.provider.DocumentsContract.RootColumns;
import android.util.Log;
import android.webkit.MimeTypeMap;
import com.google.android.collect.Maps;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
public class ExternalStorageProvider extends ContentProvider {
private static final String TAG = "ExternalStorage";
private static final String AUTHORITY = "com.android.externalstorage";
// TODO: support multiple storage devices
private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int URI_ROOTS = 1;
private static final int URI_ROOTS_ID = 2;
private static final int URI_DOCS_ID = 3;
private static final int URI_DOCS_ID_CONTENTS = 4;
private static final int URI_DOCS_ID_SEARCH = 5;
private HashMap<String, Root> mRoots = Maps.newHashMap();
private static class Root {
public int rootType;
public String name;
public int icon = 0;
public String title = null;
public String summary = null;
public File path;
}
static {
sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
}
@Override
public boolean onCreate() {
mRoots.clear();
final Root root = new Root();
root.rootType = DocumentsContract.ROOT_TYPE_DEVICE_ADVANCED;
root.name = "primary";
root.title = getContext().getString(R.string.root_internal_storage);
root.path = Environment.getExternalStorageDirectory();
mRoots.put(root.name, root);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
// TODO: support custom projections
final String[] rootsProjection = new String[] {
BaseColumns._ID, RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON,
RootColumns.TITLE, RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES };
final String[] docsProjection = new String[] {
BaseColumns._ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
DocumentColumns.DOC_ID, DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED,
DocumentColumns.FLAGS };
switch (sMatcher.match(uri)) {
case URI_ROOTS: {
final MatrixCursor cursor = new MatrixCursor(rootsProjection);
for (Root root : mRoots.values()) {
includeRoot(cursor, root);
}
return cursor;
}
case URI_ROOTS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final MatrixCursor cursor = new MatrixCursor(rootsProjection);
includeRoot(cursor, root);
return cursor;
}
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final MatrixCursor cursor = new MatrixCursor(docsProjection);
final File file = docIdToFile(root, docId);
includeFile(cursor, root, file);
return cursor;
}
case URI_DOCS_ID_CONTENTS: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final MatrixCursor cursor = new MatrixCursor(docsProjection);
final File parent = docIdToFile(root, docId);
for (File file : parent.listFiles()) {
includeFile(cursor, root, file);
}
return cursor;
}
case URI_DOCS_ID_SEARCH: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final String query = DocumentsContract.getSearchQuery(uri).toLowerCase();
final MatrixCursor cursor = new MatrixCursor(docsProjection);
final File parent = docIdToFile(root, docId);
final LinkedList<File> pending = new LinkedList<File>();
pending.add(parent);
while (!pending.isEmpty() && cursor.getCount() < 20) {
final File file = pending.removeFirst();
if (file.isDirectory()) {
for (File child : file.listFiles()) {
pending.add(child);
}
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(cursor, root, file);
}
}
}
return cursor;
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
private String fileToDocId(Root root, File file) {
String rootPath = root.path.getAbsolutePath();
final String path = file.getAbsolutePath();
if (path.equals(rootPath)) {
return DocumentsContract.ROOT_DOC_ID;
}
if (!rootPath.endsWith("/")) {
rootPath += "/";
}
if (!path.startsWith(rootPath)) {
throw new IllegalArgumentException("File " + path + " outside root " + root.path);
} else {
return path.substring(rootPath.length());
}
}
private File docIdToFile(Root root, String docId) {
if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
return root.path;
} else {
return new File(root.path, docId);
}
}
private void includeRoot(MatrixCursor cursor, Root root) {
cursor.addRow(new Object[] {
root.name.hashCode(), root.name, root.rootType, root.icon, root.title, root.summary,
root.path.getFreeSpace() });
}
private void includeFile(MatrixCursor cursor, Root root, File file) {
int flags = 0;
if (file.isDirectory()) {
flags |= DocumentsContract.FLAG_SUPPORTS_SEARCH;
}
if (file.isDirectory() && file.canWrite()) {
flags |= DocumentsContract.FLAG_SUPPORTS_CREATE;
}
if (file.canWrite()) {
flags |= DocumentsContract.FLAG_SUPPORTS_RENAME;
flags |= DocumentsContract.FLAG_SUPPORTS_DELETE;
}
final String mimeType = getTypeForFile(file);
if (mimeType.startsWith("image/")) {
flags |= DocumentsContract.FLAG_SUPPORTS_THUMBNAIL;
}
final String docId = fileToDocId(root, file);
final long id = docId.hashCode();
final String displayName;
if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
displayName = root.title;
} else {
displayName = file.getName();
}
cursor.addRow(new Object[] {
id, displayName, file.length(), docId, mimeType, file.lastModified(), flags });
}
@Override
public String getType(Uri uri) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
return getTypeForFile(docIdToFile(root, docId));
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
private String getTypeForFile(File file) {
if (file.isDirectory()) {
return DocumentsContract.MIME_TYPE_DIRECTORY;
} else {
return getTypeForName(file.getName());
}
}
private String getTypeForName(String name) {
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) {
return mime;
}
}
return "application/octet-stream";
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
// TODO: offer as thumbnail
final File file = docIdToFile(root, docId);
return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final File parent = docIdToFile(root, docId);
final String mimeType = values.getAsString(DocumentColumns.MIME_TYPE);
final String name = validateDisplayName(
values.getAsString(DocumentColumns.DISPLAY_NAME), mimeType);
final File file = new File(parent, name);
if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
if (!file.mkdir()) {
return null;
}
} else {
try {
if (!file.createNewFile()) {
return null;
}
} catch (IOException e) {
Log.w(TAG, "Failed to create file", e);
return null;
}
}
final String newDocId = fileToDocId(root, file);
return DocumentsContract.buildDocumentUri(AUTHORITY, root.name, newDocId);
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final File file = docIdToFile(root, docId);
final File newFile = new File(
file.getParentFile(), values.getAsString(DocumentColumns.DISPLAY_NAME));
return file.renameTo(newFile) ? 1 : 0;
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final File file = docIdToFile(root, docId);
return file.delete() ? 1 : 0;
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
private String validateDisplayName(String displayName, String mimeType) {
if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
return displayName;
} else {
// Try appending meaningful extension if needed
if (!mimeType.equals(getTypeForName(displayName))) {
final String extension = MimeTypeMap.getSingleton()
.getExtensionFromMimeType(mimeType);
if (extension != null) {
displayName += "." + extension;
}
}
return displayName;
}
}
}