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         String extra = "";
134         File file = getFileForUri(uri);
135         if (!file.exists()) {
136             Log.e(TAG, String.format("Insert - File from uri: '%s' does not exists.", uri));
137             return null;
138         }
139         if (mFileTracker.get(uri) != null) {
140             Log.e(
141                     TAG,
142                     String.format("Insert - File from uri: '%s' already exists, ignoring.", uri));
143             return null;
144         }
145         mFileTracker.put(uri, contentValues);
146         return uri;
147     }
148 
149     @Override
delete(Uri uri, String selection, String[] selectionArgs)150     public int delete(Uri uri, String selection, String[] selectionArgs) {
151         // Stop Tracking the File of directory if it was tracked and delete it from the disk
152         mFileTracker.remove(uri);
153         File file = getFileForUri(uri);
154         int num = recursiveDelete(file);
155         return num;
156     }
157 
158     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)159     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
160         File file = getFileForUri(uri);
161         if (!file.exists()) {
162             Log.e(TAG, String.format("Update - File from uri: '%s' does not exists.", uri));
163             return 0;
164         }
165         if (mFileTracker.get(uri) == null) {
166             Log.e(
167                     TAG,
168                     String.format(
169                             "Update - File from uri: '%s' is not tracked yet, use insert.", uri));
170             return 0;
171         }
172         mFileTracker.put(uri, values);
173         return 1;
174     }
175 
176     @Override
openFile(Uri uri, String mode)177     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
178         final File file = getFileForUri(uri);
179         final int fileMode = modeToMode(mode);
180 
181         if ((fileMode & ParcelFileDescriptor.MODE_CREATE) == ParcelFileDescriptor.MODE_CREATE) {
182             // If the file is being created, create all its parent directories that don't already
183             // exist.
184             file.getParentFile().mkdirs();
185             if (!mFileTracker.containsKey(uri)) {
186                 // Track the file, if not already tracked.
187                 mFileTracker.put(uri, new ContentValues());
188             }
189         }
190         return ParcelFileDescriptor.open(file, fileMode);
191     }
192 
getRow(String[] columns, File file, String metadata)193     private Object[] getRow(String[] columns, File file, String metadata) {
194         Object[] values = new Object[columns.length];
195         for (int i = 0; i < columns.length; i++) {
196             values[i] = getColumnValue(columns[i], file, metadata);
197         }
198         return values;
199     }
200 
getColumnValue(String columnName, File file, String metadata)201     private Object getColumnValue(String columnName, File file, String metadata) {
202         Object value = null;
203         if (COLUMN_NAME.equals(columnName)) {
204             value = file.getName();
205         } else if (COLUMN_ABSOLUTE_PATH.equals(columnName)) {
206             value = file.getAbsolutePath();
207         } else if (COLUMN_DIRECTORY.equals(columnName)) {
208             value = file.isDirectory();
209         } else if (COLUMN_METADATA.equals(columnName)) {
210             value = metadata;
211         } else if (COLUMN_MIME_TYPE.equals(columnName)) {
212             value = file.isDirectory() ? null : getType(file);
213         }
214         return value;
215     }
216 
getType(File file)217     private String getType(File file) {
218         final int lastDot = file.getName().lastIndexOf('.');
219         if (lastDot >= 0) {
220             final String extension = file.getName().substring(lastDot + 1);
221             final String mime = sMimeMap.getMimeTypeFromExtension(extension);
222             if (mime != null) {
223                 return mime;
224             }
225         }
226 
227         return "application/octet-stream";
228     }
229 
230     @SuppressLint("SdCardPath")
getFileForUri(Uri uri)231     private File getFileForUri(Uri uri) {
232         // TODO: apply the /sdcard resolution to query() too.
233         String uriPath = uri.getPath();
234         try {
235             uriPath = URLDecoder.decode(uriPath, "UTF-8");
236         } catch (UnsupportedEncodingException e) {
237             throw new RuntimeException(e);
238         }
239         if (uriPath.startsWith("/sdcard/")) {
240             uriPath =
241                     uriPath.replaceAll(
242                             "/sdcard", Environment.getExternalStorageDirectory().getAbsolutePath());
243         }
244         return new File(uriPath);
245     }
246 
247     /** Copied from FileProvider.java. */
modeToMode(String mode)248     private static int modeToMode(String mode) {
249         int modeBits;
250         if ("r".equals(mode)) {
251             modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
252         } else if ("w".equals(mode) || "wt".equals(mode)) {
253             modeBits =
254                     ParcelFileDescriptor.MODE_WRITE_ONLY
255                             | ParcelFileDescriptor.MODE_CREATE
256                             | ParcelFileDescriptor.MODE_TRUNCATE;
257         } else if ("wa".equals(mode)) {
258             modeBits =
259                     ParcelFileDescriptor.MODE_WRITE_ONLY
260                             | ParcelFileDescriptor.MODE_CREATE
261                             | ParcelFileDescriptor.MODE_APPEND;
262         } else if ("rw".equals(mode)) {
263             modeBits = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE;
264         } else if ("rwt".equals(mode)) {
265             modeBits =
266                     ParcelFileDescriptor.MODE_READ_WRITE
267                             | ParcelFileDescriptor.MODE_CREATE
268                             | ParcelFileDescriptor.MODE_TRUNCATE;
269         } else {
270             throw new IllegalArgumentException("Invalid mode: " + mode);
271         }
272         return modeBits;
273     }
274 
275     /**
276      * Recursively delete given file or directory and all its contents.
277      *
278      * @param rootDir the directory or file to be deleted; can be null
279      * @return The number of deleted files.
280      */
recursiveDelete(File rootDir)281     private int recursiveDelete(File rootDir) {
282         int count = 0;
283         if (rootDir != null) {
284             if (rootDir.isDirectory()) {
285                 File[] childFiles = rootDir.listFiles();
286                 if (childFiles != null) {
287                     for (File child : childFiles) {
288                         count += recursiveDelete(child);
289                     }
290                 }
291             }
292             rootDir.delete();
293             count++;
294         }
295         return count;
296     }
297 
sortFilesByAbsolutePath(File[] files)298     private void sortFilesByAbsolutePath(File[] files) {
299         Arrays.sort(
300                 files,
301                 new Comparator<File>() {
302                     @Override
303                     public int compare(File f1, File f2) {
304                         return f1.getAbsolutePath().compareTo(f2.getAbsolutePath());
305                     }
306                 });
307     }
308 }
309