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 com.android.cts.externalstorageapp;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.net.Uri;
23 import android.os.Environment;
24 import android.provider.MediaStore;
25 import android.provider.MediaStore.Images;
26 import android.test.AndroidTestCase;
27 import android.util.Log;
28 
29 import java.io.BufferedReader;
30 import java.io.ByteArrayOutputStream;
31 import java.io.DataInputStream;
32 import java.io.DataOutputStream;
33 import java.io.File;
34 import java.io.FileInputStream;
35 import java.io.FileOutputStream;
36 import java.io.FileReader;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.OutputStream;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Collections;
43 import java.util.List;
44 
45 /**
46  * Tests common functionality that should be supported regardless of external
47  * storage status.
48  */
49 public class CommonExternalStorageTest extends AndroidTestCase {
50     public static final String TAG = "CommonExternalStorageTest";
51 
52     public static final String PACKAGE_NONE = "com.android.cts.externalstorageapp";
53     public static final String PACKAGE_READ = "com.android.cts.readexternalstorageapp";
54     public static final String PACKAGE_WRITE = "com.android.cts.writeexternalstorageapp";
55 
56     /**
57      * Dump helpful debugging details.
58      */
testDumpDebug()59     public void testDumpDebug() throws Exception {
60         logCommand("/system/bin/id");
61         logCommand("/system/bin/cat", "/proc/self/mountinfo");
62     }
63 
64     /**
65      * Primary storage must always be mounted.
66      */
testExternalStorageMounted()67     public void testExternalStorageMounted() {
68         assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
69     }
70 
71     /**
72      * Verify that single path is always first item in multiple.
73      */
testMultipleCacheDirs()74     public void testMultipleCacheDirs() throws Exception {
75         final File single = getContext().getExternalCacheDir();
76         assertNotNull("Primary storage must always be available", single);
77         final File firstMultiple = getContext().getExternalCacheDirs()[0];
78         assertEquals(single, firstMultiple);
79     }
80 
81     /**
82      * Verify that single path is always first item in multiple.
83      */
testMultipleFilesDirs()84     public void testMultipleFilesDirs() throws Exception {
85         final File single = getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
86         assertNotNull("Primary storage must always be available", single);
87         final File firstMultiple = getContext()
88                 .getExternalFilesDirs(Environment.DIRECTORY_PICTURES)[0];
89         assertEquals(single, firstMultiple);
90     }
91 
92     /**
93      * Verify that single path is always first item in multiple.
94      */
testMultipleObbDirs()95     public void testMultipleObbDirs() throws Exception {
96         final File single = getContext().getObbDir();
97         assertNotNull("Primary storage must always be available", single);
98         final File firstMultiple = getContext().getObbDirs()[0];
99         assertEquals(single, firstMultiple);
100     }
101 
102     /**
103      * Verify we can write to our own package dirs.
104      */
testAllPackageDirsWritable()105     public void testAllPackageDirsWritable() throws Exception {
106         final long testValue = 1234500000000L;
107         final List<File> paths = getAllPackageSpecificPaths(getContext());
108         for (File path : paths) {
109             assertNotNull("Valid media must be inserted during CTS", path);
110             assertEquals("Valid media must be inserted during CTS", Environment.MEDIA_MOUNTED,
111                     Environment.getExternalStorageState(path));
112 
113             assertDirReadWriteAccess(path);
114 
115             final File directChild = new File(path, "directChild");
116             final File subdir = new File(path, "subdir");
117             final File subdirChild = new File(path, "subdirChild");
118 
119             writeInt(directChild, 32);
120             subdir.mkdirs();
121             assertDirReadWriteAccess(subdir);
122             writeInt(subdirChild, 64);
123 
124             assertEquals(32, readInt(directChild));
125             assertEquals(64, readInt(subdirChild));
126 
127             assertTrue("Must be able to set last modified", directChild.setLastModified(testValue));
128             assertTrue("Must be able to set last modified", subdirChild.setLastModified(testValue));
129 
130             assertEquals(testValue, directChild.lastModified());
131             assertEquals(testValue, subdirChild.lastModified());
132         }
133 
134         for (File path : paths) {
135             deleteContents(path);
136         }
137     }
138 
139     /**
140      * Return a set of several package-specific external storage paths.
141      */
getAllPackageSpecificPaths(Context context)142     public static List<File> getAllPackageSpecificPaths(Context context) {
143         final List<File> paths = new ArrayList<File>();
144         Collections.addAll(paths, context.getExternalCacheDirs());
145         Collections.addAll(paths, context.getExternalFilesDirs(null));
146         Collections.addAll(paths, context.getExternalFilesDirs(Environment.DIRECTORY_PICTURES));
147         Collections.addAll(paths, context.getExternalMediaDirs());
148         Collections.addAll(paths, context.getObbDirs());
149         return paths;
150     }
151 
getAllPackageSpecificPathsExceptMedia(Context context)152     public static List<File> getAllPackageSpecificPathsExceptMedia(Context context) {
153         final List<File> paths = new ArrayList<File>();
154         Collections.addAll(paths, context.getExternalCacheDirs());
155         Collections.addAll(paths, context.getExternalFilesDirs(null));
156         Collections.addAll(paths, context.getExternalFilesDirs(Environment.DIRECTORY_PICTURES));
157         Collections.addAll(paths, context.getObbDirs());
158         return paths;
159     }
160 
getAllPackageSpecificPathsExceptObb(Context context)161     public static List<File> getAllPackageSpecificPathsExceptObb(Context context) {
162         final List<File> paths = new ArrayList<File>();
163         Collections.addAll(paths, context.getExternalCacheDirs());
164         Collections.addAll(paths, context.getExternalFilesDirs(null));
165         Collections.addAll(paths, context.getExternalFilesDirs(Environment.DIRECTORY_PICTURES));
166         Collections.addAll(paths, context.getExternalMediaDirs());
167         return paths;
168     }
169 
getAllPackageSpecificObbGiftPaths(Context context, String targetPackageName)170     public static List<File> getAllPackageSpecificObbGiftPaths(Context context,
171             String targetPackageName) {
172         final List<File> targetFiles = new ArrayList<>();
173         final File obbDir = context.getObbDir();
174         final File targetObbDir = new File(
175                 obbDir.getAbsolutePath().replace(context.getPackageName(), targetPackageName));
176         targetFiles.add(new File(targetObbDir, targetPackageName + ".gift"));
177         return targetFiles;
178     }
179 
180     /**
181      * Return a set of several package-specific external storage paths pointing
182      * at "gift" files designed to be exchanged with the target package in Q.
183      * These directories can't be used to exchange "gift" files in R.
184      */
getAllPackageSpecificNoGiftPaths(Context context, String targetPackageName)185     public static List<File> getAllPackageSpecificNoGiftPaths(Context context,
186             String targetPackageName) {
187         final List<File> files = getPrimaryPackageSpecificPathsExceptMedia(context);
188         final List<File> targetFiles = new ArrayList<>();
189         for (File file : files) {
190             final File targetFile = new File(
191                     file.getAbsolutePath().replace(context.getPackageName(), targetPackageName));
192             targetFiles.add(new File(targetFile, targetPackageName + ".gift"));
193         }
194         return targetFiles;
195     }
196 
getPrimaryPackageSpecificPathsExceptMedia(Context context)197     public static List<File> getPrimaryPackageSpecificPathsExceptMedia(Context context) {
198         final List<File> paths = new ArrayList<File>();
199         Collections.addAll(paths, context.getExternalCacheDir());
200         Collections.addAll(paths, context.getExternalFilesDir(null));
201         Collections.addAll(paths, context.getObbDir());
202         return paths;
203     }
204 
getPrimaryPackageSpecificPaths(Context context)205     public static List<File> getPrimaryPackageSpecificPaths(Context context) {
206         final List<File> paths = new ArrayList<File>();
207         Collections.addAll(paths, context.getExternalCacheDir());
208         Collections.addAll(paths, context.getExternalFilesDir(null));
209         Collections.addAll(paths, context.getExternalFilesDir(Environment.DIRECTORY_PICTURES));
210         Collections.addAll(paths, context.getObbDir());
211         return paths;
212     }
213 
getSecondaryPackageSpecificPaths(Context context)214     public static List<File> getSecondaryPackageSpecificPaths(Context context) {
215         final List<File> paths = new ArrayList<File>();
216         Collections.addAll(paths, dropFirst(context.getExternalCacheDirs()));
217         Collections.addAll(paths, dropFirst(context.getExternalFilesDirs(null)));
218         Collections.addAll(
219                 paths, dropFirst(context.getExternalFilesDirs(Environment.DIRECTORY_PICTURES)));
220         Collections.addAll(paths, dropFirst(context.getObbDirs()));
221         return paths;
222     }
223 
getMountPaths()224     public static List<File> getMountPaths() throws IOException {
225         final List<File> paths = new ArrayList<>();
226         final BufferedReader br = new BufferedReader(new FileReader("/proc/self/mounts"));
227         try {
228             String line;
229             while ((line = br.readLine()) != null) {
230                 final String[] fields = line.split(" ");
231                 paths.add(new File(fields[1]));
232             }
233         } finally {
234             br.close();
235         }
236         return paths;
237     }
238 
dropFirst(File[] before)239     private static File[] dropFirst(File[] before) {
240         final File[] after = new File[before.length - 1];
241         System.arraycopy(before, 1, after, 0, after.length);
242         return after;
243     }
244 
buildProbeFile(File dir)245     public static File buildProbeFile(File dir) {
246         return new File(dir, ".probe_" + System.nanoTime());
247     }
248 
buildCommonChildDirs(File dir)249     public static File[] buildCommonChildDirs(File dir) {
250         return new File[] {
251                 new File(dir, Environment.DIRECTORY_MUSIC),
252                 new File(dir, Environment.DIRECTORY_PODCASTS),
253                 new File(dir, Environment.DIRECTORY_ALARMS),
254                 new File(dir, Environment.DIRECTORY_RINGTONES),
255                 new File(dir, Environment.DIRECTORY_NOTIFICATIONS),
256                 new File(dir, Environment.DIRECTORY_PICTURES),
257                 new File(dir, Environment.DIRECTORY_MOVIES),
258                 new File(dir, Environment.DIRECTORY_DOWNLOADS),
259                 new File(dir, Environment.DIRECTORY_DCIM),
260                 new File(dir, Environment.DIRECTORY_DOCUMENTS),
261         };
262     }
263 
assertDirReadOnlyAccess(File path)264     public static void assertDirReadOnlyAccess(File path) {
265         Log.d(TAG, "Asserting read-only access to " + path);
266 
267         assertTrue("exists", path.exists());
268         assertTrue("execute", path.canExecute());
269 
270         try {
271             final File probe = buildProbeFile(path);
272             assertFalse(probe.createNewFile());
273             assertFalse(probe.exists());
274             assertFalse(probe.delete());
275             fail("able to create probe!");
276         } catch (IOException e) {
277             // expected
278         }
279     }
280 
assertDirReadWriteAccess(File[] paths)281     public static void assertDirReadWriteAccess(File[] paths) {
282         for (File path : paths) {
283             assertDirReadWriteAccess(path);
284         }
285     }
286 
assertDirReadWriteAccess(File path)287     public static void assertDirReadWriteAccess(File path) {
288         Log.d(TAG, "Asserting read/write access to " + path);
289 
290         assertTrue("exists", path.exists());
291         assertTrue("read", path.canRead());
292         assertTrue("execute", path.canExecute());
293         assertNotNull("list", path.list());
294 
295         try {
296             final File probe = buildProbeFile(path);
297             assertTrue(probe.createNewFile());
298             assertTrue(probe.exists());
299             assertTrue(probe.delete());
300             assertFalse(probe.exists());
301         } catch (IOException e) {
302             fail("failed to create probe!");
303         }
304     }
305 
assertDirNoAccess(File path)306     public static void assertDirNoAccess(File path) {
307         Log.d(TAG, "Asserting no access to " + path);
308 
309         try {
310             final File probe = buildProbeFile(path);
311             assertFalse(probe.createNewFile());
312             assertFalse(probe.exists());
313             assertFalse(probe.delete());
314             fail("able to create probe!");
315         } catch (IOException e) {
316             // expected
317         }
318     }
319 
assertDirNoWriteAccess(File[] paths)320     public static void assertDirNoWriteAccess(File[] paths) {
321         for (File path : paths) {
322             assertDirNoWriteAccess(path);
323         }
324     }
325 
assertDirNoWriteAccess(File path)326     public static void assertDirNoWriteAccess(File path) {
327         Log.d(TAG, "Asserting no write access to " + path);
328 
329         try {
330             final File probe = buildProbeFile(path);
331             assertFalse(probe.createNewFile());
332             assertFalse(probe.exists());
333             assertFalse(probe.delete());
334             fail("able to create probe!");
335         } catch (IOException e) {
336             // expected
337         }
338     }
339 
assertFileReadWriteAccess(File path)340     public static void assertFileReadWriteAccess(File path) {
341         try {
342             new FileInputStream(path).close();
343         } catch (IOException e) {
344             fail("failed to read!");
345         }
346 
347         try {
348             new FileOutputStream(path, true).close();
349         } catch (IOException e) {
350             fail("failed to write!");
351         }
352     }
353 
assertFileNoAccess(File path)354     public static void assertFileNoAccess(File path) {
355         try {
356             new FileInputStream(path).close();
357             fail("able to read!");
358         } catch (IOException e) {
359             // expected
360         }
361 
362         try {
363             new FileOutputStream(path, true).close();
364             fail("able to write!");
365         } catch (IOException e) {
366             // expected
367         }
368     }
369 
assertFileNotPresent(File path)370     public static void assertFileNotPresent(File path) {
371         assertFalse(path + " exists!", path.exists());
372     }
373 
assertMediaNoAccess(ContentResolver resolver, boolean legacyApp)374     public static void assertMediaNoAccess(ContentResolver resolver, boolean legacyApp)
375             throws Exception {
376         final ContentValues values = new ContentValues();
377         values.put(Images.Media.MIME_TYPE, "image/jpeg");
378         values.put(Images.Media.DATA,
379                 buildProbeFile(Environment.getExternalStorageDirectory()).getAbsolutePath());
380 
381         try {
382             Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
383             if (legacyApp) {
384                 // For legacy apps we do not crash - just make the operation do nothing
385                 assertEquals(MediaStore.Images.Media.EXTERNAL_CONTENT_URI
386                         .buildUpon().appendPath("0").build().toString(), uri.toString());
387             } else {
388                 fail("Expected access to be blocked");
389             }
390         } catch (Exception expected) {
391         }
392     }
393 
assertMediaReadWriteAccess(ContentResolver resolver)394     public static void assertMediaReadWriteAccess(ContentResolver resolver) throws Exception {
395         final ContentValues values = new ContentValues();
396         values.put(Images.Media.MIME_TYPE, "image/jpeg");
397         values.put(Images.Media.DATA,
398                 buildProbeFile(Environment.getExternalStorageDirectory()).getAbsolutePath());
399 
400         final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
401         try {
402             resolver.openFileDescriptor(uri, "rw").close();
403             resolver.openFileDescriptor(uri, "w").close();
404             resolver.openFileDescriptor(uri, "r").close();
405         } finally {
406             resolver.delete(uri, null, null);
407         }
408     }
409 
isAllowList(File file)410     private static boolean isAllowList(File file) {
411         final String[] allowLists = {
412                 "autorun.inf", ".android_secure", "android_secure"
413         };
414         if (file.getParentFile().getAbsolutePath().equals(
415                 Environment.getExternalStorageDirectory().getAbsolutePath())) {
416             for (String allowList : allowLists) {
417                 if (file.getName().equalsIgnoreCase(allowList)) {
418                     return true;
419                 }
420             }
421         }
422         return false;
423     }
424 
removeAllowList(File[] files)425     private static File[] removeAllowList(File[] files) {
426         List<File> fileList = new ArrayList<File>();
427         if (files == null) {
428             return null;
429         }
430 
431         for (File file : files) {
432             if (!isAllowList(file)) {
433                 fileList.add(file);
434             }
435         }
436         return fileList.toArray(new File[fileList.size()]);
437     }
438 
deleteContents(File dir)439     public static void deleteContents(File dir) throws IOException {
440         File[] files = dir.listFiles();
441         files = removeAllowList(files);
442         if (files != null) {
443             for (File file : files) {
444                 if (file.isDirectory()) {
445                     deleteContents(file);
446                 }
447                 assertTrue(file.delete());
448             }
449 
450             File[] dirs = removeAllowList(dir.listFiles());
451             if (dirs.length != 0) {
452                 fail("Expected wiped storage but found: " + Arrays.toString(dirs));
453             }
454         }
455     }
456 
writeInt(File file, int value)457     public static void writeInt(File file, int value) throws IOException {
458         final DataOutputStream os = new DataOutputStream(new FileOutputStream(file));
459         try {
460             os.writeInt(value);
461         } finally {
462             os.close();
463         }
464     }
465 
readInt(File file)466     public static int readInt(File file) throws IOException {
467         final DataInputStream is = new DataInputStream(new FileInputStream(file));
468         try {
469             return is.readInt();
470         } finally {
471             is.close();
472         }
473     }
474 
logCommand(String... cmd)475     public static void logCommand(String... cmd) throws Exception {
476         final Process proc = new ProcessBuilder(cmd).redirectErrorStream(true).start();
477 
478         final ByteArrayOutputStream buf = new ByteArrayOutputStream();
479         copy(proc.getInputStream(), buf);
480         final int res = proc.waitFor();
481 
482         Log.d(TAG, Arrays.toString(cmd) + " result " + res + ":");
483         Log.d(TAG, buf.toString());
484     }
485 
486     /** Shamelessly lifted from libcore.io.Streams */
copy(InputStream in, OutputStream out)487     public static int copy(InputStream in, OutputStream out) throws IOException {
488         int total = 0;
489         byte[] buffer = new byte[8192];
490         int c;
491         while ((c = in.read(buffer)) != -1) {
492             total += c;
493             out.write(buffer, 0, c);
494         }
495         return total;
496     }
497 }
498