• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 package android.tradefed.contentprovider;
17 
18 import android.annotation.SuppressLint;
19 import android.content.ContentProvider;
20 import android.content.ContentValues;
21 import android.database.Cursor;
22 import android.database.MatrixCursor;
23 import android.net.Uri;
24 import android.os.Environment;
25 import android.os.ParcelFileDescriptor;
26 import android.util.Log;
27 import android.webkit.MimeTypeMap;
28 
29 import java.io.File;
30 import java.io.FileNotFoundException;
31 import java.io.UnsupportedEncodingException;
32 import java.net.URLDecoder;
33 import java.util.Arrays;
34 import java.util.Comparator;
35 import java.util.HashMap;
36 import java.util.Map;
37 
38 /**
39  * Content Provider implementation to hide sd card details away from host/device interactions, and
40  * that allows to abstract the host/device interactions more by allowing device and host to
41  * communicate files through the provider.
42  *
43  * <p>This implementation aims to be standard and work in all situations.
44  */
45 public class ManagedFileContentProvider extends ContentProvider {
46     public static final String COLUMN_NAME = "name";
47     public static final String COLUMN_ABSOLUTE_PATH = "absolute_path";
48     public static final String COLUMN_DIRECTORY = "is_directory";
49     public static final String COLUMN_MIME_TYPE = "mime_type";
50     public static final String COLUMN_METADATA = "metadata";
51 
52     // TODO: Complete the list of columns
53     public static final String[] COLUMNS =
54             new String[] {
55                 COLUMN_NAME,
56                 COLUMN_ABSOLUTE_PATH,
57                 COLUMN_DIRECTORY,
58                 COLUMN_MIME_TYPE,
59                 COLUMN_METADATA
60             };
61 
62     private static final String TAG = "TradefedContentProvider";
63     private static MimeTypeMap sMimeMap = MimeTypeMap.getSingleton();
64 
65     private Map<Uri, ContentValues> mFileTracker = new HashMap<>();
66 
67     @Override
onCreate()68     public boolean onCreate() {
69         mFileTracker = new HashMap<>();
70         return true;
71     }
72 
73     /**
74      * Use a content URI with absolute device path embedded to get information about a file or a
75      * directory on the device.
76      *
77      * @param uri A content uri that contains the path to the desired file/directory.
78      * @param projection - not supported.
79      * @param selection - not supported.
80      * @param selectionArgs - not supported.
81      * @param sortOrder - not supported.
82      * @return A {@link Cursor} containing the results of the query. Cursor contains a single row
83      *     for files and for directories it returns one row for each {@link File} returned by {@link
84      *     File#listFiles()}.
85      */
86     @Override
query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)87     public Cursor query(
88             Uri uri,
89             String[] projection,
90             String selection,
91             String[] selectionArgs,
92             String sortOrder) {
93         File file = getFileForUri(uri);
94         if ("/".equals(file.getAbsolutePath())) {
95             // Querying the root will list all the known file (inserted)
96             final MatrixCursor cursor = new MatrixCursor(COLUMNS, mFileTracker.size());
97             for (Map.Entry<Uri, ContentValues> path : mFileTracker.entrySet()) {
98                 String metadata = path.getValue().getAsString(COLUMN_METADATA);
99                 cursor.addRow(getRow(COLUMNS, getFileForUri(path.getKey()), metadata));
100             }
101             return cursor;
102         }
103 
104         if (!file.exists()) {
105             Log.e(TAG, String.format("Query - File from uri: '%s' does not exists.", uri));
106             return null;
107         }
108 
109         if (!file.isDirectory()) {
110             // Just return the information about the file itself.
111             final MatrixCursor cursor = new MatrixCursor(COLUMNS, 1);
112             cursor.addRow(getRow(COLUMNS, file, /* metadata= */ null));
113             return cursor;
114         }
115 
116         // Otherwise return the content of the directory - similar to doing ls command.
117         File[] files = file.listFiles();
118         sortFilesByAbsolutePath(files);
119         final MatrixCursor cursor = new MatrixCursor(COLUMNS, files.length + 1);
120         for (File child : files) {
121             cursor.addRow(getRow(COLUMNS, child, /* metadata= */ null));
122         }
123         return cursor;
124     }
125 
126     @Override
getType(Uri uri)127     public String getType(Uri uri) {
128         return getType(getFileForUri(uri));
129     }
130 
131     @Override
insert(Uri uri, ContentValues contentValues)132     public Uri insert(Uri uri, ContentValues contentValues) {
133         File file = getFileForUri(uri);
134         if (!file.exists()) {
135             Log.e(TAG, String.format("Insert - File from uri: '%s' does not exists.", uri));
136             return null;
137         }
138         if (mFileTracker.get(uri) != null) {
139             Log.e(
140                     TAG,
141                     String.format("Insert - File from uri: '%s' already exists, ignoring.", uri));
142             return null;
143         }
144         mFileTracker.put(uri, contentValues);
145         return uri;
146     }
147 
148     @Override
delete(Uri uri, String selection, String[] selectionArgs)149     public int delete(Uri uri, String selection, String[] selectionArgs) {
150         // Stop Tracking the File of directory if it was tracked and delete it from the disk
151         mFileTracker.remove(uri);
152         File file = getFileForUri(uri);
153         int num = recursiveDelete(file);
154         return num;
155     }
156 
157     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)158     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
159         File file = getFileForUri(uri);
160         if (!file.exists()) {
161             Log.e(TAG, String.format("Update - File from uri: '%s' does not exists.", uri));
162             return 0;
163         }
164         if (mFileTracker.get(uri) == null) {
165             Log.e(
166                     TAG,
167                     String.format(
168                             "Update - File from uri: '%s' is not tracked yet, use insert.", uri));
169             return 0;
170         }
171         mFileTracker.put(uri, values);
172         return 1;
173     }
174 
175     @Override
openFile(Uri uri, String mode)176     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
177         final File file = getFileForUri(uri);
178         final int fileMode = modeToMode(mode);
179 
180         if ((fileMode & ParcelFileDescriptor.MODE_CREATE) == ParcelFileDescriptor.MODE_CREATE) {
181             // If the file is being created, create all its parent directories that don't already
182             // exist.
183             file.getParentFile().mkdirs();
184             if (!mFileTracker.containsKey(uri)) {
185                 // Track the file, if not already tracked.
186                 mFileTracker.put(uri, new ContentValues());
187             }
188         }
189         return ParcelFileDescriptor.open(file, fileMode);
190     }
191 
getRow(String[] columns, File file, String metadata)192     private Object[] getRow(String[] columns, File file, String metadata) {
193         Object[] values = new Object[columns.length];
194         for (int i = 0; i < columns.length; i++) {
195             values[i] = getColumnValue(columns[i], file, metadata);
196         }
197         return values;
198     }
199 
getColumnValue(String columnName, File file, String metadata)200     private Object getColumnValue(String columnName, File file, String metadata) {
201         Object value = null;
202         if (COLUMN_NAME.equals(columnName)) {
203             value = file.getName();
204         } else if (COLUMN_ABSOLUTE_PATH.equals(columnName)) {
205             value = file.getAbsolutePath();
206         } else if (COLUMN_DIRECTORY.equals(columnName)) {
207             value = file.isDirectory();
208         } else if (COLUMN_METADATA.equals(columnName)) {
209             value = metadata;
210         } else if (COLUMN_MIME_TYPE.equals(columnName)) {
211             value = file.isDirectory() ? null : getType(file);
212         }
213         return value;
214     }
215 
getType(File file)216     private String getType(File file) {
217         final int lastDot = file.getName().lastIndexOf('.');
218         if (lastDot >= 0) {
219             final String extension = file.getName().substring(lastDot + 1);
220             final String mime = sMimeMap.getMimeTypeFromExtension(extension);
221             if (mime != null) {
222                 return mime;
223             }
224         }
225 
226         return "application/octet-stream";
227     }
228 
229     @SuppressLint("SdCardPath")
getFileForUri(Uri uri)230     private File getFileForUri(Uri uri) {
231         // TODO: apply the /sdcard resolution to query() too.
232         String uriPath = uri.getPath();
233         try {
234             uriPath = URLDecoder.decode(uriPath, "UTF-8");
235         } catch (UnsupportedEncodingException e) {
236             throw new RuntimeException(e);
237         }
238         if (uriPath.startsWith("/sdcard/")) {
239             uriPath =
240                     uriPath.replaceAll(
241                             "/sdcard", Environment.getExternalStorageDirectory().getAbsolutePath());
242         }
243         return new File(uriPath);
244     }
245 
246     /** Copied from FileProvider.java. */
modeToMode(String mode)247     private static int modeToMode(String mode) {
248         int modeBits;
249         if ("r".equals(mode)) {
250             modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
251         } else if ("w".equals(mode) || "wt".equals(mode)) {
252             modeBits =
253                     ParcelFileDescriptor.MODE_WRITE_ONLY
254                             | ParcelFileDescriptor.MODE_CREATE
255                             | ParcelFileDescriptor.MODE_TRUNCATE;
256         } else if ("wa".equals(mode)) {
257             modeBits =
258                     ParcelFileDescriptor.MODE_WRITE_ONLY
259                             | ParcelFileDescriptor.MODE_CREATE
260                             | ParcelFileDescriptor.MODE_APPEND;
261         } else if ("rw".equals(mode)) {
262             modeBits = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE;
263         } else if ("rwt".equals(mode)) {
264             modeBits =
265                     ParcelFileDescriptor.MODE_READ_WRITE
266                             | ParcelFileDescriptor.MODE_CREATE
267                             | ParcelFileDescriptor.MODE_TRUNCATE;
268         } else {
269             throw new IllegalArgumentException("Invalid mode: " + mode);
270         }
271         return modeBits;
272     }
273 
274     /**
275      * Recursively delete given file or directory and all its contents.
276      *
277      * @param rootDir the directory or file to be deleted; can be null
278      * @return The number of deleted files.
279      */
recursiveDelete(File rootDir)280     private int recursiveDelete(File rootDir) {
281         int count = 0;
282         if (rootDir != null) {
283             if (rootDir.isDirectory()) {
284                 File[] childFiles = rootDir.listFiles();
285                 if (childFiles != null) {
286                     for (File child : childFiles) {
287                         count += recursiveDelete(child);
288                     }
289                 }
290             }
291             rootDir.delete();
292             count++;
293         }
294         return count;
295     }
296 
sortFilesByAbsolutePath(File[] files)297     private void sortFilesByAbsolutePath(File[] files) {
298         Arrays.sort(
299                 files,
300                 new Comparator<File>() {
301                     @Override
302                     public int compare(File f1, File f2) {
303                         return f1.getAbsolutePath().compareTo(f2.getAbsolutePath());
304                     }
305                 });
306     }
307 }
308