ExternalStorageProvider.java revision 92d7e697a864a3e18bef4ef256bb3eb339a66b4e
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.externalstorage;
18
19import android.content.ContentProvider;
20import android.content.ContentResolver;
21import android.content.ContentValues;
22import android.content.UriMatcher;
23import android.database.Cursor;
24import android.database.MatrixCursor;
25import android.net.Uri;
26import android.os.Environment;
27import android.os.ParcelFileDescriptor;
28import android.provider.BaseColumns;
29import android.provider.DocumentsContract;
30import android.provider.DocumentsContract.DocumentColumns;
31import android.provider.DocumentsContract.RootColumns;
32import android.util.Log;
33import android.webkit.MimeTypeMap;
34
35import com.google.android.collect.Maps;
36
37import java.io.File;
38import java.io.FileNotFoundException;
39import java.io.IOException;
40import java.util.HashMap;
41import java.util.LinkedList;
42
43public class ExternalStorageProvider extends ContentProvider {
44    private static final String TAG = "ExternalStorage";
45
46    private static final String AUTHORITY = "com.android.externalstorage";
47
48    // TODO: support multiple storage devices
49
50    private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
51
52    private static final int URI_ROOTS = 1;
53    private static final int URI_ROOTS_ID = 2;
54    private static final int URI_DOCS_ID = 3;
55    private static final int URI_DOCS_ID_CONTENTS = 4;
56    private static final int URI_DOCS_ID_SEARCH = 5;
57
58    private HashMap<String, Root> mRoots = Maps.newHashMap();
59
60    private static class Root {
61        public int rootType;
62        public String name;
63        public int icon = 0;
64        public String title = null;
65        public String summary = null;
66        public File path;
67    }
68
69    static {
70        sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
71        sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
72        sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
73        sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
74        sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
75    }
76
77    @Override
78    public boolean onCreate() {
79        mRoots.clear();
80
81        final Root root = new Root();
82        root.rootType = DocumentsContract.ROOT_TYPE_DEVICE_ADVANCED;
83        root.name = "primary";
84        root.title = getContext().getString(R.string.root_internal_storage);
85        root.path = Environment.getExternalStorageDirectory();
86        mRoots.put(root.name, root);
87
88        return true;
89    }
90
91    @Override
92    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
93            String sortOrder) {
94
95        // TODO: support custom projections
96        final String[] rootsProjection = new String[] {
97                BaseColumns._ID, RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON,
98                RootColumns.TITLE, RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES };
99        final String[] docsProjection = new String[] {
100                BaseColumns._ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
101                DocumentColumns.DOC_ID, DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED,
102                DocumentColumns.FLAGS };
103
104        switch (sMatcher.match(uri)) {
105            case URI_ROOTS: {
106                final MatrixCursor cursor = new MatrixCursor(rootsProjection);
107                for (Root root : mRoots.values()) {
108                    includeRoot(cursor, root);
109                }
110                return cursor;
111            }
112            case URI_ROOTS_ID: {
113                final String root = uri.getPathSegments().get(1);
114
115                final MatrixCursor cursor = new MatrixCursor(rootsProjection);
116                includeRoot(cursor, mRoots.get(root));
117                return cursor;
118            }
119            case URI_DOCS_ID: {
120                final Root root = mRoots.get(uri.getPathSegments().get(1));
121                final String docId = uri.getPathSegments().get(3);
122
123                final MatrixCursor cursor = new MatrixCursor(docsProjection);
124                final File file = docIdToFile(root, docId);
125                includeFile(cursor, root, file);
126                return cursor;
127            }
128            case URI_DOCS_ID_CONTENTS: {
129                final Root root = mRoots.get(uri.getPathSegments().get(1));
130                final String docId = uri.getPathSegments().get(3);
131
132                final MatrixCursor cursor = new MatrixCursor(docsProjection);
133                final File parent = docIdToFile(root, docId);
134                for (File file : parent.listFiles()) {
135                    includeFile(cursor, root, file);
136                }
137                return cursor;
138            }
139            case URI_DOCS_ID_SEARCH: {
140                final Root root = mRoots.get(uri.getPathSegments().get(1));
141                final String docId = uri.getPathSegments().get(3);
142                final String query = uri.getQueryParameter(DocumentsContract.PARAM_QUERY).toLowerCase();
143
144                final MatrixCursor cursor = new MatrixCursor(docsProjection);
145                final File parent = docIdToFile(root, docId);
146
147                final LinkedList<File> pending = new LinkedList<File>();
148                pending.add(parent);
149                while (!pending.isEmpty() && cursor.getCount() < 20) {
150                    final File file = pending.removeFirst();
151                    if (file.isDirectory()) {
152                        for (File child : file.listFiles()) {
153                            pending.add(child);
154                        }
155                    } else {
156                        if (file.getName().toLowerCase().contains(query)) {
157                            includeFile(cursor, root, file);
158                        }
159                    }
160                }
161                return cursor;
162            }
163            default: {
164                throw new UnsupportedOperationException("Unsupported Uri " + uri);
165            }
166        }
167    }
168
169    private String fileToDocId(Root root, File file) {
170        String rootPath = root.path.getAbsolutePath();
171        final String path = file.getAbsolutePath();
172        if (path.equals(rootPath)) {
173            return DocumentsContract.ROOT_DOC_ID;
174        }
175
176        if (!rootPath.endsWith("/")) {
177            rootPath += "/";
178        }
179        if (!path.startsWith(rootPath)) {
180            throw new IllegalArgumentException("File " + path + " outside root " + root.path);
181        } else {
182            return path.substring(rootPath.length());
183        }
184    }
185
186    private File docIdToFile(Root root, String docId) {
187        if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
188            return root.path;
189        } else {
190            return new File(root.path, docId);
191        }
192    }
193
194    private void includeRoot(MatrixCursor cursor, Root root) {
195        cursor.addRow(new Object[] {
196                root.name.hashCode(), root.name, root.rootType, root.icon, root.title, root.summary,
197                root.path.getFreeSpace() });
198    }
199
200    private void includeFile(MatrixCursor cursor, Root root, File file) {
201        int flags = 0;
202
203        if (file.isDirectory()) {
204            flags |= DocumentsContract.FLAG_SUPPORTS_SEARCH;
205        }
206        if (file.isDirectory() && file.canWrite()) {
207            flags |= DocumentsContract.FLAG_SUPPORTS_CREATE;
208        }
209        if (file.canWrite()) {
210            flags |= DocumentsContract.FLAG_SUPPORTS_RENAME;
211            flags |= DocumentsContract.FLAG_SUPPORTS_DELETE;
212        }
213
214        final String mimeType = getTypeForFile(file);
215        if (mimeType.startsWith("image/")) {
216            flags |= DocumentsContract.FLAG_SUPPORTS_THUMBNAIL;
217        }
218
219        final String docId = fileToDocId(root, file);
220        final long id = docId.hashCode();
221        cursor.addRow(new Object[] {
222                id, file.getName(), file.length(), docId, mimeType, file.lastModified(), flags });
223    }
224
225    @Override
226    public String getType(Uri uri) {
227        switch (sMatcher.match(uri)) {
228            case URI_DOCS_ID: {
229                final Root root = mRoots.get(uri.getPathSegments().get(1));
230                final String docId = uri.getPathSegments().get(3);
231                return getTypeForFile(docIdToFile(root, docId));
232            }
233            default: {
234                throw new UnsupportedOperationException("Unsupported Uri " + uri);
235            }
236        }
237    }
238
239    private String getTypeForFile(File file) {
240        if (file.isDirectory()) {
241            return DocumentsContract.MIME_TYPE_DIRECTORY;
242        } else {
243            return getTypeForName(file.getName());
244        }
245    }
246
247    private String getTypeForName(String name) {
248        final int lastDot = name.lastIndexOf('.');
249        if (lastDot >= 0) {
250            final String extension = name.substring(lastDot + 1);
251            final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
252            if (mime != null) {
253                return mime;
254            }
255        }
256
257        return "application/octet-stream";
258    }
259
260    @Override
261    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
262        switch (sMatcher.match(uri)) {
263            case URI_DOCS_ID: {
264                final Root root = mRoots.get(uri.getPathSegments().get(1));
265                final String docId = uri.getPathSegments().get(3);
266
267                // TODO: offer as thumbnail
268                final File file = docIdToFile(root, docId);
269                return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
270            }
271            default: {
272                throw new UnsupportedOperationException("Unsupported Uri " + uri);
273            }
274        }
275    }
276
277    @Override
278    public Uri insert(Uri uri, ContentValues values) {
279        switch (sMatcher.match(uri)) {
280            case URI_DOCS_ID: {
281                final Root root = mRoots.get(uri.getPathSegments().get(1));
282                final String docId = uri.getPathSegments().get(3);
283
284                final File parent = docIdToFile(root, docId);
285
286                final String mimeType = values.getAsString(DocumentColumns.MIME_TYPE);
287                final String name = validateDisplayName(
288                        values.getAsString(DocumentColumns.DISPLAY_NAME), mimeType);
289
290                final File file = new File(parent, name);
291                if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
292                    if (!file.mkdir()) {
293                        return null;
294                    }
295
296                } else {
297                    try {
298                        if (!file.createNewFile()) {
299                            return null;
300                        }
301                    } catch (IOException e) {
302                        Log.w(TAG, "Failed to create file", e);
303                        return null;
304                    }
305                }
306
307                final String newDocId = fileToDocId(root, file);
308                return DocumentsContract.buildDocumentUri(AUTHORITY, root.name, newDocId);
309            }
310            default: {
311                throw new UnsupportedOperationException("Unsupported Uri " + uri);
312            }
313        }
314    }
315
316    @Override
317    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
318        switch (sMatcher.match(uri)) {
319            case URI_DOCS_ID: {
320                final Root root = mRoots.get(uri.getPathSegments().get(1));
321                final String docId = uri.getPathSegments().get(3);
322
323                final File file = docIdToFile(root, docId);
324                final File newFile = new File(
325                        file.getParentFile(), values.getAsString(DocumentColumns.DISPLAY_NAME));
326                return file.renameTo(newFile) ? 1 : 0;
327            }
328            default: {
329                throw new UnsupportedOperationException("Unsupported Uri " + uri);
330            }
331        }
332    }
333
334    @Override
335    public int delete(Uri uri, String selection, String[] selectionArgs) {
336        switch (sMatcher.match(uri)) {
337            case URI_DOCS_ID: {
338                final Root root = mRoots.get(uri.getPathSegments().get(1));
339                final String docId = uri.getPathSegments().get(3);
340
341                final File file = docIdToFile(root, docId);
342                return file.delete() ? 1 : 0;
343            }
344            default: {
345                throw new UnsupportedOperationException("Unsupported Uri " + uri);
346            }
347        }
348    }
349
350    private String validateDisplayName(String displayName, String mimeType) {
351        if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
352            return displayName;
353        } else {
354            // Try appending meaningful extension if needed
355            if (!mimeType.equals(getTypeForName(displayName))) {
356                final String extension = MimeTypeMap.getSingleton()
357                        .getExtensionFromMimeType(mimeType);
358                if (extension != null) {
359                    displayName += "." + extension;
360                }
361            }
362
363            return displayName;
364        }
365    }
366}
367