1 /*
2  * Copyright (C) 2012 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 
17 package android.provider.cts;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.net.Uri;
25 import android.os.Environment;
26 import android.os.ParcelFileDescriptor;
27 import android.os.SystemClock;
28 import android.provider.MediaStore;
29 import android.provider.MediaStore.MediaColumns;
30 import android.provider.MediaStore.Files.FileColumns;
31 import android.test.AndroidTestCase;
32 
33 import com.android.compatibility.common.util.FileCopyHelper;
34 
35 import java.io.File;
36 import java.io.FileNotFoundException;
37 import java.io.FileOutputStream;
38 import java.io.FilenameFilter;
39 import java.io.IOException;
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.List;
43 
44 public class MediaStore_FilesTest extends AndroidTestCase {
45 
46     private ContentResolver mResolver;
47 
48     @Override
setUp()49     protected void setUp() throws Exception {
50         super.setUp();
51         mResolver = mContext.getContentResolver();
52         cleanup();
53     }
54 
55     @Override
tearDown()56     protected void tearDown() throws Exception {
57         super.tearDown();
58         cleanup();
59     }
60 
cleanup()61     void cleanup() {
62         final String testName = getClass().getCanonicalName();
63         mResolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
64                 "_data LIKE ?1", new String[] {"%" + testName + "%"});
65 
66         mResolver.delete(MediaStore.Files.getContentUri("external"),
67                 "_data LIKE ?1", new String[] {"%" + testName + "%"});
68 
69         File ext = Environment.getExternalStorageDirectory();
70         File[] junk = ext.listFiles(new FilenameFilter() {
71 
72             @Override
73             public boolean accept(File dir, String filename) {
74                 return filename.contains(testName);
75             }
76         });
77         for (File f: junk) {
78             deleteAll(f);
79         }
80     }
81 
deleteAll(File f)82     void deleteAll(File f) {
83         if (f.isDirectory()) {
84             File [] sub = f.listFiles();
85             for (File s: sub) {
86                 deleteAll(s);
87             }
88         }
89         f.delete();
90     }
91 
testGetContentUri()92     public void testGetContentUri() {
93         String volumeName = MediaStoreAudioTestHelper.EXTERNAL_VOLUME_NAME;
94         Uri allFilesUri = MediaStore.Files.getContentUri(volumeName);
95 
96         // Get the current file count. We will check if this increases after
97         // adding a file to the provider.
98         int fileCount = getFileCount(allFilesUri);
99 
100         // Check that inserting empty values causes an exception.
101         ContentValues values = new ContentValues();
102         try {
103             mResolver.insert(allFilesUri, values);
104             fail("Should throw an exception");
105         } catch (IllegalArgumentException e) {
106             // Expecting an exception
107         }
108 
109         // Add a path for a file and check that the returned uri appends a
110         // path properly.
111         String dataPath = "does_not_really_exist.txt";
112         values.put(MediaColumns.DATA, dataPath);
113         Uri fileUri = mResolver.insert(allFilesUri, values);
114         long fileId = ContentUris.parseId(fileUri);
115         assertEquals(fileUri, ContentUris.withAppendedId(allFilesUri, fileId));
116 
117         // Check that getContentUri with the file id produces the same url
118         Uri rowUri = MediaStore.Files.getContentUri(volumeName, fileId);
119         assertEquals(fileUri, rowUri);
120 
121         // Check that the file count has increased.
122         int newFileCount = getFileCount(allFilesUri);
123         assertEquals(fileCount + 1, newFileCount);
124 
125         // Check that the path we inserted was stored properly.
126         assertStringColumn(fileUri, MediaColumns.DATA, dataPath);
127 
128         // Update the path and check that the database changed.
129         String updatedPath = "still_does_not_exist.txt";
130         values.put(MediaColumns.DATA, updatedPath);
131         assertEquals(1, mResolver.update(fileUri, values, null, null));
132         assertStringColumn(fileUri, MediaColumns.DATA, updatedPath);
133 
134         // check that inserting a duplicate entry fails
135         Uri foo = mResolver.insert(allFilesUri, values);
136         assertNull(foo);
137 
138         // Delete the file and observe that the file count decreased.
139         assertEquals(1, mResolver.delete(fileUri, null, null));
140         assertEquals(fileCount, getFileCount(allFilesUri));
141 
142         // Make sure the deleted file is not returned by the cursor.
143         Cursor cursor = mResolver.query(fileUri, null, null, null, null);
144         try {
145             assertFalse(cursor.moveToNext());
146         } finally {
147             cursor.close();
148         }
149 
150         // insert file and check its parent
151         values.clear();
152         try {
153             String b = mContext.getExternalFilesDir(Environment.DIRECTORY_MUSIC).getCanonicalPath();
154             values.put(MediaColumns.DATA, b + "/testing");
155             fileUri = mResolver.insert(allFilesUri, values);
156             cursor = mResolver.query(fileUri, new String[] { MediaStore.Files.FileColumns.PARENT },
157                     null, null, null);
158             assertEquals(1, cursor.getCount());
159             cursor.moveToFirst();
160             long parentid = cursor.getLong(0);
161             assertTrue("got 0 parent for non root file", parentid != 0);
162 
163             cursor.close();
164             cursor = mResolver.query(ContentUris.withAppendedId(allFilesUri, parentid),
165                     new String[] { MediaColumns.DATA }, null, null, null);
166             assertEquals(1, cursor.getCount());
167             cursor.moveToFirst();
168             String parentPath = cursor.getString(0);
169             assertEquals(b, parentPath);
170 
171             mResolver.delete(fileUri, null, null);
172         } catch (IOException e) {
173             fail(e.getMessage());
174         } finally {
175             cursor.close();
176         }
177     }
178 
testCaseSensitivity()179     public void testCaseSensitivity() throws IOException {
180         String fileDir = Environment.getExternalStorageDirectory() +
181                 "/" + getClass().getCanonicalName();
182         String fileName = fileDir + "/Test.Mp3";
183         writeFile(R.raw.testmp3, fileName);
184 
185         String volumeName = MediaStoreAudioTestHelper.EXTERNAL_VOLUME_NAME;
186         Uri allFilesUri = MediaStore.Files.getContentUri(volumeName);
187         ContentValues values = new ContentValues();
188         values.put(MediaColumns.DATA, fileDir + "/test.mp3");
189         Uri fileUri = mResolver.insert(allFilesUri, values);
190         try {
191             ParcelFileDescriptor pfd = mResolver.openFileDescriptor(fileUri, "r");
192             pfd.close();
193         } finally {
194             mResolver.delete(fileUri, null, null);
195             new File(fileName).delete();
196             new File(fileDir).delete();
197         }
198     }
199 
realPathFor(ParcelFileDescriptor pfd)200     String realPathFor(ParcelFileDescriptor pfd) {
201         File real = new File("/proc/self/fd/" + pfd.getFd());
202         try {
203             return real.getCanonicalPath();
204         } catch (IOException e) {
205             return null;
206         }
207     }
208 
testAccess()209     public void testAccess() throws IOException {
210         // clean up from previous run
211         mResolver.delete(MediaStore.Images.Media.INTERNAL_CONTENT_URI,
212                 "_data NOT LIKE ?", new String[] { "/system/%" } );
213 
214         // insert some dummy starter data into the provider
215         ContentValues values = new ContentValues();
216         values.put(MediaStore.Images.Media.DISPLAY_NAME, "My Bitmap");
217         values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
218         values.put(MediaStore.Images.Media.DATA, "/foo/bar/dummy.jpg");
219         Uri uri = mResolver.insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
220 
221         // point _data at directory and try to get an fd for it
222         values = new ContentValues();
223         values.put("_data", "/data/media");
224         mResolver.update(uri, values, null, null);
225         ParcelFileDescriptor pfd = null;
226         try {
227             pfd = mResolver.openFileDescriptor(uri, "r");
228             pfd.close();
229             fail("shouldn't be here");
230         } catch (FileNotFoundException e) {
231             // expected
232         }
233 
234         // try to create a file in a place we don't have access to
235         values = new ContentValues();
236         values.put("_data", "/data/media/test.dat");
237         mResolver.update(uri, values, null, null);
238         try {
239             pfd = mResolver.openFileDescriptor(uri, "w");
240             pfd.close();
241             fail("shouldn't be here");
242         } catch (FileNotFoundException e) {
243             // expected
244         }
245         // read file back
246         try {
247             pfd = mResolver.openFileDescriptor(uri, "r");
248             pfd.close();
249             fail("shouldn't be here");
250         } catch (FileNotFoundException e) {
251             // expected
252         }
253 
254         // point _data at media database and read it
255         values = new ContentValues();
256         values.put("_data", "/data/data/com.android.providers.media/databases/internal.db");
257         mResolver.update(uri, values, null, null);
258         try {
259             pfd = mResolver.openFileDescriptor(uri, "r");
260             pfd.close();
261             fail("shouldn't be here");
262         } catch (FileNotFoundException e) {
263             // expected
264         }
265 
266         // Insert a private file into the database. Since it's private, the media provider won't
267         // be able to open it
268         FileOutputStream fos = mContext.openFileOutput("dummy.dat", Context.MODE_PRIVATE);
269         fos.write(0);
270         fos.close();
271         File path = mContext.getFileStreamPath("dummy.dat");
272         values = new ContentValues();
273         values.put("_data", path.getAbsolutePath());
274 
275         mResolver.update(uri, values, null, null);
276         try {
277             pfd = mResolver.openFileDescriptor(uri, "r");
278             pfd.close();
279             fail("shouldn't be here");
280         } catch (FileNotFoundException e) {
281             // expected
282         }
283         path.delete();
284 
285         File sdfile = null;
286         if (Environment.isExternalStorageEmulated()) {
287             // create file on sdcard and check access via real path
288             String fileDir = Environment.getExternalStorageDirectory() +
289                     "/" + getClass().getCanonicalName() + "/test.mp3";
290             sdfile = new File(fileDir);
291             writeFile(R.raw.testmp3, sdfile.getCanonicalPath());
292             assertTrue(sdfile.exists());
293             values = new ContentValues();
294             values.put("_data", sdfile.getCanonicalPath());
295             mResolver.update(uri, values, null, null);
296             try {
297                 pfd = mResolver.openFileDescriptor(uri, "r");
298 
299                 // get the real path from the file descriptor (this relies on the media provider
300                 // having opened the path via the real path instead of the emulated path).
301                 String realPath = realPathFor(pfd);
302                 pfd.close();
303                 if (realPath.equals(sdfile.getCanonicalPath())) {
304                     // provider did not use real/translated path
305                     sdfile = null;
306                 } else {
307                     values = new ContentValues();
308                     values.put("_data", realPath);
309                     mResolver.update(uri, values, null, null);
310 
311                     // we shouldn't be able to access this
312                     try {
313                         pfd = mResolver.openFileDescriptor(uri, "r");
314                         fail("shouldn't have fd for " + realPath);
315                     } catch (FileNotFoundException e) {
316                         // expected
317                     } finally {
318                         pfd.close();
319                     }
320                 }
321             } catch (FileNotFoundException e) {
322                 fail("couldn't open file");
323             }
324         }
325 
326         // clean up
327         assertEquals(1, mResolver.delete(uri, null, null));
328         if (sdfile != null) {
329             assertEquals("couldn't delete " + sdfile.getCanonicalPath(), true, sdfile.delete());
330         }
331 
332         // test secondary storage if present
333         List<File> allpaths = getSecondaryPackageSpecificPaths(mContext);
334         List<String> trimmedPaths = new ArrayList<String>();
335 
336         for (File extpath: allpaths) {
337             assertNotNull("Valid media must be inserted during CTS", extpath);
338             assertEquals("Valid media must be inserted for " + extpath
339                     + " during CTS", Environment.MEDIA_MOUNTED,
340                     Environment.getStorageState(extpath));
341 
342             File child = extpath;
343             while (true) {
344                 File parent = child.getParentFile();
345                 if (parent == null) {
346                     fail("didn't expect to be here");
347                 }
348                 if (!Environment.MEDIA_MOUNTED.equals(Environment.getStorageState(parent))) {
349                     // we went past the root
350                     String abspath = child.getAbsolutePath();
351                     if (!trimmedPaths.contains(abspath)) {
352                         trimmedPaths.add(abspath);
353                     }
354                     break;
355                 }
356                 child = parent;
357             }
358         }
359 
360         String fileDir = Environment.getExternalStorageDirectory() +
361                 "/" + getClass().getCanonicalName() + "-" + SystemClock.elapsedRealtime();
362         String fileName = fileDir + "/TestSecondary.Mp3";
363         writeFile(R.raw.testmp3_2, fileName); // file without album art
364 
365 
366         // insert temp file
367         values = new ContentValues();
368         values.put(MediaStore.Audio.Media.DATA, fileName);
369         values.put(MediaStore.Audio.Media.ARTIST, "Artist-" + SystemClock.elapsedRealtime());
370         values.put(MediaStore.Audio.Media.ALBUM, "Album-" + SystemClock.elapsedRealtime());
371         values.put(MediaStore.Audio.Media.MIME_TYPE, "audio/mp3");
372         Uri fileUri = mResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
373         // give media provider some time to realize there's no album art
374         SystemClock.sleep(1000);
375         // get its album id
376         Cursor c = mResolver.query(fileUri, new String[] { MediaStore.Audio.Media.ALBUM_ID},
377                 null, null, null);
378         assertTrue(c.moveToFirst());
379         int albumid = c.getInt(0);
380         Uri albumArtUriBase = Uri.parse("content://media/external/audio/albumart");
381         Uri albumArtUri = ContentUris.withAppendedId(albumArtUriBase, albumid);
382         try {
383             pfd = mResolver.openFileDescriptor(albumArtUri, "r");
384             fail("no album art, shouldn't be here. Got: " + realPathFor(pfd));
385         } catch (Exception e) {
386             // expected
387         }
388 
389         // replace file with one that has album art
390         writeFile(R.raw.testmp3, fileName); // file with album art
391 
392         for (String s: trimmedPaths) {
393             File dir = new File(s + "/foobardir-" + SystemClock.elapsedRealtime());
394             assertFalse("please remove " + dir.getAbsolutePath()
395                     + " before running", dir.exists());
396             File file = new File(dir, "foobar");
397             values = new ContentValues();
398             values.put(MediaStore.Audio.Media.ALBUM_ID, albumid);
399             values.put(MediaStore.Audio.Media.DATA, file.getAbsolutePath());
400             mResolver.insert(albumArtUriBase, values);
401             try {
402                 pfd = mResolver.openFileDescriptor(albumArtUri, "r");
403                 fail("shouldn't have fd for album " + albumid + ", got " + realPathFor(pfd));
404             } catch (Exception e) {
405                 // expected
406             } finally {
407                 pfd.close();
408             }
409             assertFalse(dir.getAbsolutePath() + " was created", dir.exists());
410         }
411         mResolver.delete(fileUri, null, null);
412         new File(fileName).delete();
413 
414         // try creating files in root
415         for (String s: trimmedPaths) {
416             File dir = new File(s);
417             File file = new File(dir, "foobar.jpg");
418 
419             values = new ContentValues();
420             values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
421             fileUri = mResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
422             assertNotNull(fileUri);
423 
424             // check that adding the file doesn't cause it to be created
425             assertFalse(file.exists());
426 
427             // check if opening the file for write works
428             try {
429                 mResolver.openOutputStream(fileUri).close();
430                 fail("shouldn't have been able to create output stream");
431             } catch (SecurityException e) {
432                 // expected
433             }
434             // check that deleting the file doesn't cause it to be created
435             mResolver.delete(fileUri, null, null);
436             assertFalse(file.exists());
437         }
438 
439         // try creating files in new subdir
440         for (String s: trimmedPaths) {
441             File dir = new File(s + "/foobardir");
442             File file = new File(dir, "foobar.jpg");
443 
444             values = new ContentValues();
445             values.put(MediaStore.Files.FileColumns.DATA, dir.getAbsolutePath());
446 
447             Uri dirUri = mResolver.insert(MediaStore.Files.getContentUri("external"), values);
448             assertNotNull(dirUri);
449 
450             values = new ContentValues();
451             values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
452             fileUri = mResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
453             assertNotNull(fileUri);
454 
455             // check that adding the file or its folder didn't cause either one to be created
456             assertFalse(dir.exists());
457             assertFalse(file.exists());
458 
459             // check if opening the file for write works
460             try {
461                 mResolver.openOutputStream(fileUri).close();
462                 fail("shouldn't have been able to create output stream");
463             } catch (SecurityException e) {
464                 // expected
465             }
466             // check that deleting the file or its folder doesn't cause either one to be created
467             mResolver.delete(fileUri, null, null);
468             assertFalse(dir.exists());
469             assertFalse(file.exists());
470             mResolver.delete(dirUri, null, null);
471             assertFalse(dir.exists());
472             assertFalse(file.exists());
473         }
474     }
475 
getSecondaryPackageSpecificPaths(Context context)476     public static List<File> getSecondaryPackageSpecificPaths(Context context) {
477         final List<File> paths = new ArrayList<File>();
478         Collections.addAll(paths, dropFirst(context.getExternalCacheDirs()));
479         Collections.addAll(paths, dropFirst(context.getExternalFilesDirs(null)));
480         Collections.addAll(
481                 paths, dropFirst(context.getExternalFilesDirs(Environment.DIRECTORY_PICTURES)));
482         Collections.addAll(paths, dropFirst(context.getObbDirs()));
483         return paths;
484     }
485 
testUpdateMediaType()486     public void testUpdateMediaType() throws Exception {
487         String fileDir = Environment.getExternalStorageDirectory() +
488                 "/" + getClass().getCanonicalName();
489         String fileName = fileDir + "/test.mp3";
490         writeFile(R.raw.testmp3, fileName);
491 
492         String volumeName = MediaStoreAudioTestHelper.EXTERNAL_VOLUME_NAME;
493         Uri allFilesUri = MediaStore.Files.getContentUri(volumeName);
494         ContentValues values = new ContentValues();
495         values.put(MediaColumns.DATA, fileName);
496         values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
497         Uri fileUri = mResolver.insert(allFilesUri, values);
498 
499 
500         // There is special logic in MediaProvider#update() to update paths when a folder was moved
501         // or renamed. It only checks whether newValues only has one column but assumes the provided
502         // column is _data. We need to guard the case where there is only one column in newValues
503         // and it's not _data.
504         ContentValues newValues = new ContentValues(1);
505         newValues.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
506         mResolver.update(fileUri, newValues, null, null);
507 
508         try (Cursor c = mResolver.query(
509                 fileUri, new String[] { FileColumns.MEDIA_TYPE }, null, null, null)) {
510             c.moveToNext();
511             assertEquals(FileColumns.MEDIA_TYPE_NONE,
512                     c.getInt(c.getColumnIndex(FileColumns.MEDIA_TYPE)));
513         }
514     }
515 
dropFirst(File[] before)516     private static File[] dropFirst(File[] before) {
517         final File[] after = new File[before.length - 1];
518         System.arraycopy(before, 1, after, 0, after.length);
519         return after;
520     }
521 
writeFile(int resid, String path)522     private void writeFile(int resid, String path) throws IOException {
523         File out = new File(path);
524         File dir = out.getParentFile();
525         dir.mkdirs();
526         FileCopyHelper copier = new FileCopyHelper(mContext);
527         copier.copyToExternalStorage(resid, out);
528     }
529 
getFileCount(Uri uri)530     private int getFileCount(Uri uri) {
531         Cursor cursor = mResolver.query(uri, null, null, null, null);
532         try {
533             return cursor.getCount();
534         } finally {
535             cursor.close();
536         }
537     }
538 
assertStringColumn(Uri fileUri, String columnName, String expectedValue)539     private void assertStringColumn(Uri fileUri, String columnName, String expectedValue) {
540         Cursor cursor = mResolver.query(fileUri, null, null, null, null);
541         try {
542             assertTrue(cursor.moveToNext());
543             int index = cursor.getColumnIndexOrThrow(columnName);
544             assertEquals(expectedValue, cursor.getString(index));
545         } finally {
546             cursor.close();
547         }
548     }
549 }
550