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