Test new READ/WRITE_EXTERNAL_STORAGE behavior.

In KLP, the platform now makes package-specific directories on
external storage available to apps without requiring they hold any
storage permissions.  This change uses three different packages to
verify behavior of apps with no permissions, with READ_EXTERNAL,
and with WRITE_EXTERNAL.  All three packages run a set of common
tests to verify that package-specific directories are always
writable.

Also verifies behavior of new APIs that provide access to secondary
external storage devices.  Specifically, apps only get write access
to their package-specific directories.  Apps holding WRITE_EXTERNAL
can only write outside their package-specific directories on primary
external storage.

Bug: 10577541
Change-Id: I467221c358fdc69f88d0df81d7ee50ad3c5ab5d5
diff --git a/CtsTestCaseList.mk b/CtsTestCaseList.mk
index 755cb45..5d23eb5 100644
--- a/CtsTestCaseList.mk
+++ b/CtsTestCaseList.mk
@@ -19,6 +19,7 @@
 	CtsInstrumentationAppDiffCert \
 	CtsPermissionDeclareApp \
 	CtsPermissionDeclareAppCompat \
+	CtsReadExternalStorageApp \
 	CtsSharedUidInstall \
 	CtsSharedUidInstallDiffCert \
 	CtsSimpleAppInstall \
diff --git a/hostsidetests/appsecurity/src/com/android/cts/appsecurity/AppSecurityTests.java b/hostsidetests/appsecurity/src/com/android/cts/appsecurity/AppSecurityTests.java
index 3779db9..88b05fb 100644
--- a/hostsidetests/appsecurity/src/com/android/cts/appsecurity/AppSecurityTests.java
+++ b/hostsidetests/appsecurity/src/com/android/cts/appsecurity/AppSecurityTests.java
@@ -17,7 +17,6 @@
 package com.android.cts.appsecurity;
 
 import com.android.cts.tradefed.build.CtsBuildHelper;
-import com.android.ddmlib.IDevice;
 import com.android.ddmlib.Log;
 import com.android.ddmlib.testrunner.InstrumentationResultParser;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
@@ -66,15 +65,16 @@
     private static final String APP_ACCESS_DATA_PKG = "com.android.cts.appaccessdata";
 
     // External storage constants
+    private static final String COMMON_EXTERNAL_STORAGE_APP_CLASS = "com.android.cts.externalstorageapp.CommonExternalStorageTest";
     private static final String EXTERNAL_STORAGE_APP_APK = "CtsExternalStorageApp.apk";
     private static final String EXTERNAL_STORAGE_APP_PKG = "com.android.cts.externalstorageapp";
-    private static final String EXTERNAL_STORAGE_APP_CLASS = EXTERNAL_STORAGE_APP_PKG
-            + ".ExternalStorageTest";
+    private static final String EXTERNAL_STORAGE_APP_CLASS = EXTERNAL_STORAGE_APP_PKG + ".ExternalStorageTest";
+    private static final String READ_EXTERNAL_STORAGE_APP_APK = "CtsReadExternalStorageApp.apk";
+    private static final String READ_EXTERNAL_STORAGE_APP_PKG = "com.android.cts.readexternalstorageapp";
+    private static final String READ_EXTERNAL_STORAGE_APP_CLASS = READ_EXTERNAL_STORAGE_APP_PKG + ".ReadExternalStorageTest";
     private static final String WRITE_EXTERNAL_STORAGE_APP_APK = "CtsWriteExternalStorageApp.apk";
-    private static final String
-            WRITE_EXTERNAL_STORAGE_APP_PKG = "com.android.cts.writeexternalstorageapp";
-    private static final String WRITE_EXTERNAL_STORAGE_APP_CLASS = WRITE_EXTERNAL_STORAGE_APP_PKG
-            + ".WriteExternalStorageTest";
+    private static final String WRITE_EXTERNAL_STORAGE_APP_PKG = "com.android.cts.writeexternalstorageapp";
+    private static final String WRITE_EXTERNAL_STORAGE_APP_CLASS = WRITE_EXTERNAL_STORAGE_APP_PKG + ".WriteExternalStorageTest";
 
     // testInstrumentationDiffCert constants
     private static final String TARGET_INSTRUMENT_APK = "CtsTargetInstrumentationApp.apk";
@@ -207,20 +207,89 @@
     }
 
     /**
-     * Verify that legacy filesystem paths continue working, and that they all
-     * point to same location.
+     * Verify that app with no external storage permissions works correctly.
      */
-    public void testExternalStorageLegacyPaths() throws Exception {
+    public void testExternalStorageNone() throws Exception {
         try {
+            wipePrimaryExternalStorage(getDevice());
+
+            getDevice().uninstallPackage(EXTERNAL_STORAGE_APP_PKG);
+            assertNull(getDevice()
+                    .installPackage(getTestAppFile(EXTERNAL_STORAGE_APP_APK), false));
+            assertTrue("Failed external storage with no permissions",
+                    runDeviceTests(EXTERNAL_STORAGE_APP_PKG));
+        } finally {
+            getDevice().uninstallPackage(EXTERNAL_STORAGE_APP_PKG);
+        }
+    }
+
+    /**
+     * Verify that app with
+     * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} works
+     * correctly.
+     */
+    public void testExternalStorageRead() throws Exception {
+        try {
+            wipePrimaryExternalStorage(getDevice());
+
+            getDevice().uninstallPackage(READ_EXTERNAL_STORAGE_APP_PKG);
+            assertNull(getDevice()
+                    .installPackage(getTestAppFile(READ_EXTERNAL_STORAGE_APP_APK), false));
+            assertTrue("Failed external storage with read permissions",
+                    runDeviceTests(READ_EXTERNAL_STORAGE_APP_PKG));
+        } finally {
+            getDevice().uninstallPackage(READ_EXTERNAL_STORAGE_APP_PKG);
+        }
+    }
+
+    /**
+     * Verify that app with
+     * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} works
+     * correctly.
+     */
+    public void testExternalStorageWrite() throws Exception {
+        try {
+            wipePrimaryExternalStorage(getDevice());
+
             getDevice().uninstallPackage(WRITE_EXTERNAL_STORAGE_APP_PKG);
             assertNull(getDevice()
                     .installPackage(getTestAppFile(WRITE_EXTERNAL_STORAGE_APP_APK), false));
+            assertTrue("Failed external storage with write permissions",
+                    runDeviceTests(WRITE_EXTERNAL_STORAGE_APP_PKG));
+        } finally {
+            getDevice().uninstallPackage(WRITE_EXTERNAL_STORAGE_APP_PKG);
+        }
+    }
 
-            assertTrue("Failed to verify legacy filesystem paths", runDeviceTests(
-                    WRITE_EXTERNAL_STORAGE_APP_PKG, WRITE_EXTERNAL_STORAGE_APP_CLASS,
-                    "testLegacyPaths"));
+    /**
+     * Verify that app with WRITE_EXTERNAL can leave gifts in external storage
+     * directories belonging to other apps, and those apps can read.
+     */
+    public void testExternalStorageGifts() throws Exception {
+        try {
+            wipePrimaryExternalStorage(getDevice());
+
+            getDevice().uninstallPackage(EXTERNAL_STORAGE_APP_PKG);
+            getDevice().uninstallPackage(READ_EXTERNAL_STORAGE_APP_PKG);
+            getDevice().uninstallPackage(WRITE_EXTERNAL_STORAGE_APP_PKG);
+            assertNull(getDevice()
+                    .installPackage(getTestAppFile(EXTERNAL_STORAGE_APP_APK), false));
+            assertNull(getDevice()
+                    .installPackage(getTestAppFile(READ_EXTERNAL_STORAGE_APP_APK), false));
+            assertNull(getDevice()
+                    .installPackage(getTestAppFile(WRITE_EXTERNAL_STORAGE_APP_APK), false));
+
+            assertTrue("Failed to write gifts", runDeviceTests(WRITE_EXTERNAL_STORAGE_APP_PKG,
+                    WRITE_EXTERNAL_STORAGE_APP_CLASS, "doWriteGifts"));
+
+            assertTrue("Read failed to verify gifts", runDeviceTests(READ_EXTERNAL_STORAGE_APP_PKG,
+                    READ_EXTERNAL_STORAGE_APP_CLASS, "doVerifyGifts"));
+            assertTrue("None failed to verify gifts", runDeviceTests(EXTERNAL_STORAGE_APP_PKG,
+                    EXTERNAL_STORAGE_APP_CLASS, "doVerifyGifts"));
 
         } finally {
+            getDevice().uninstallPackage(EXTERNAL_STORAGE_APP_PKG);
+            getDevice().uninstallPackage(READ_EXTERNAL_STORAGE_APP_PKG);
             getDevice().uninstallPackage(WRITE_EXTERNAL_STORAGE_APP_PKG);
         }
     }
@@ -500,4 +569,9 @@
         getDevice().executeShellCommand(cmd, parser);
         return listener.getCurrentRunResults();
     }
+
+    private static void wipePrimaryExternalStorage(ITestDevice device)
+            throws DeviceNotAvailableException {
+        device.executeShellCommand("rm -rf /sdcard/*");
+    }
 }
diff --git a/hostsidetests/appsecurity/test-apps/ExternalStorageApp/Android.mk b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/Android.mk
index 91d6ccf..bc99560 100644
--- a/hostsidetests/appsecurity/test-apps/ExternalStorageApp/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/Android.mk
@@ -17,7 +17,7 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE_TAGS := tests
-LOCAL_SDK_VERSION := 10
+LOCAL_SDK_VERSION := current
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_PACKAGE_NAME := CtsExternalStorageApp
diff --git a/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/CommonExternalStorageTest.java b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/CommonExternalStorageTest.java
new file mode 100644
index 0000000..340ebed
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/CommonExternalStorageTest.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.externalstorageapp;
+
+import android.content.Context;
+import android.os.Environment;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Tests common functionality that should be supported regardless of external
+ * storage status.
+ */
+public class CommonExternalStorageTest extends AndroidTestCase {
+    private static final String TAG = "CommonExternalStorageTest";
+
+    public static final String PACKAGE_NONE = "com.android.cts.externalstorageapp";
+    public static final String PACKAGE_READ = "com.android.cts.readexternalstorageapp";
+    public static final String PACKAGE_WRITE = "com.android.cts.writeexternalstorageapp";
+
+    /**
+     * Primary storage must always be mounted.
+     */
+    public void testExternalStorageMounted() {
+        assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
+    }
+
+    /**
+     * Verify that single path is always first item in multiple.
+     */
+    public void testMultipleCacheDirs() throws Exception {
+        final File single = getContext().getExternalCacheDir();
+        assertNotNull("Primary storage must always be available", single);
+        final File firstMultiple = getContext().getExternalCacheDirs()[0];
+        assertEquals(single, firstMultiple);
+    }
+
+    /**
+     * Verify that single path is always first item in multiple.
+     */
+    public void testMultipleFilesDirs() throws Exception {
+        final File single = getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+        assertNotNull("Primary storage must always be available", single);
+        final File firstMultiple = getContext()
+                .getExternalFilesDirs(Environment.DIRECTORY_PICTURES)[0];
+        assertEquals(single, firstMultiple);
+    }
+
+    /**
+     * Verify that single path is always first item in multiple.
+     */
+    public void testMultipleObbDirs() throws Exception {
+        final File single = getContext().getObbDir();
+        assertNotNull("Primary storage must always be available", single);
+        final File firstMultiple = getContext().getObbDirs()[0];
+        assertEquals(single, firstMultiple);
+    }
+
+    /**
+     * Verify we can write to our own package dirs.
+     */
+    public void testPackageDirs() throws Exception {
+        final List<File> paths = getAllPackageSpecificPaths(getContext());
+        for (File path : paths) {
+            if (path == null) continue;
+
+            assertEquals(Environment.MEDIA_MOUNTED, Environment.getStorageState(path));
+            assertDirReadWriteAccess(path);
+
+            final File directChild = new File(path, "directChild");
+            final File subdir = new File(path, "subdir");
+            final File subdirChild = new File(path, "subdirChild");
+
+            writeInt(directChild, 32);
+            subdir.mkdirs();
+            assertDirReadWriteAccess(subdir);
+            writeInt(subdirChild, 64);
+
+            assertEquals(32, readInt(directChild));
+            assertEquals(64, readInt(subdirChild));
+        }
+
+        for (File path : paths) {
+            deleteContents(path);
+        }
+    }
+
+    /**
+     * Return a set of several package-specific external storage paths.
+     */
+    public static List<File> getAllPackageSpecificPaths(Context context) {
+        final List<File> paths = new ArrayList<File>();
+        Collections.addAll(paths, context.getExternalCacheDirs());
+        Collections.addAll(paths, context.getExternalFilesDirs(null));
+        Collections.addAll(paths, context.getExternalFilesDirs(Environment.DIRECTORY_PICTURES));
+        Collections.addAll(paths, context.getObbDirs());
+        return paths;
+    }
+
+    public static List<File> getPrimaryPackageSpecificPaths(Context context) {
+        final List<File> paths = new ArrayList<File>();
+        Collections.addAll(paths, context.getExternalCacheDir());
+        Collections.addAll(paths, context.getExternalFilesDir(null));
+        Collections.addAll(paths, context.getExternalFilesDir(Environment.DIRECTORY_PICTURES));
+        Collections.addAll(paths, context.getObbDir());
+        return paths;
+    }
+
+    public static List<File> getSecondaryPackageSpecificPaths(Context context) {
+        final List<File> paths = new ArrayList<File>();
+        Collections.addAll(paths, dropFirst(context.getExternalCacheDirs()));
+        Collections.addAll(paths, dropFirst(context.getExternalFilesDirs(null)));
+        Collections.addAll(
+                paths, dropFirst(context.getExternalFilesDirs(Environment.DIRECTORY_PICTURES)));
+        Collections.addAll(paths, dropFirst(context.getObbDirs()));
+        return paths;
+    }
+
+    private static File[] dropFirst(File[] before) {
+        final File[] after = new File[before.length - 1];
+        System.arraycopy(before, 1, after, 0, after.length);
+        return after;
+    }
+
+    public static File buildGiftForPackage(Context context, String packageName) {
+        final File myCache = context.getExternalCacheDir();
+        return new File(myCache.getAbsolutePath().replace(context.getPackageName(), packageName),
+                packageName + ".gift");
+    }
+
+    public static void assertDirReadOnlyAccess(File path) {
+        Log.d(TAG, "Asserting read-only access to " + path);
+
+        assertTrue("exists", path.exists());
+        assertTrue("read", path.canRead());
+        assertTrue("execute", path.canExecute());
+        assertNotNull("list", path.list());
+
+        try {
+            final File probe = new File(path, ".probe");
+            assertTrue(!probe.exists());
+            probe.createNewFile();
+            probe.delete();
+            fail("able to create probe!");
+        } catch (IOException e) {
+            // expected
+        }
+    }
+
+    public static void assertDirReadWriteAccess(File path) {
+        Log.d(TAG, "Asserting read/write access to " + path);
+
+        assertTrue("exists", path.exists());
+        assertTrue("read", path.canRead());
+        assertTrue("execute", path.canExecute());
+        assertNotNull("list", path.list());
+
+        try {
+            final File probe = new File(path, ".probe");
+            assertTrue(!probe.exists());
+            probe.createNewFile();
+            probe.delete();
+        } catch (IOException e) {
+            fail("failed to create probe!");
+        }
+    }
+
+    public static void assertDirNoAccess(File path) {
+        Log.d(TAG, "Asserting no access to " + path);
+
+        assertFalse("read", path.canRead());
+        assertNull("list", path.list());
+
+        try {
+            final File probe = new File(path, ".probe");
+            assertTrue(!probe.exists());
+            probe.createNewFile();
+            probe.delete();
+            fail("able to create probe!");
+        } catch (IOException e) {
+            // expected
+        }
+    }
+
+    public static void assertFileReadOnlyAccess(File path) {
+        try {
+            new FileInputStream(path).close();
+        } catch (IOException e) {
+            fail("failed to read!");
+        }
+
+        try {
+            new FileOutputStream(path, true).close();
+            fail("able to write!");
+        } catch (IOException e) {
+            // expected
+        }
+    }
+
+    public static void assertFileReadWriteAccess(File path) {
+        try {
+            new FileInputStream(path).close();
+        } catch (IOException e) {
+            fail("failed to read!");
+        }
+
+        try {
+            new FileOutputStream(path, true).close();
+        } catch (IOException e) {
+            fail("failed to write!");
+        }
+    }
+
+    public static void assertFileNoAccess(File path) {
+        try {
+            new FileInputStream(path).close();
+            fail("able to read!");
+        } catch (IOException e) {
+            // expected
+        }
+
+        try {
+            new FileOutputStream(path, true).close();
+            fail("able to write!");
+        } catch (IOException e) {
+            // expected
+        }
+    }
+
+    public static void deleteContents(File dir) throws IOException {
+        File[] files = dir.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                if (file.isDirectory()) {
+                    deleteContents(file);
+                }
+                assertTrue(file.delete());
+            }
+            assertEquals(0, dir.listFiles().length);
+        }
+    }
+
+    public static void writeInt(File file, int value) throws IOException {
+        final DataOutputStream os = new DataOutputStream(new FileOutputStream(file));
+        try {
+            os.writeInt(value);
+        } finally {
+            os.close();
+        }
+    }
+
+    public static int readInt(File file) throws IOException {
+        final DataInputStream is = new DataInputStream(new FileInputStream(file));
+        try {
+            return is.readInt();
+        } finally {
+            is.close();
+        }
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/ExternalStorageTest.java b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/ExternalStorageTest.java
index a3fcf4a..9d24f98 100644
--- a/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/ExternalStorageTest.java
+++ b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/ExternalStorageTest.java
@@ -16,43 +16,69 @@
 
 package com.android.cts.externalstorageapp;
 
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.PACKAGE_NONE;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.PACKAGE_READ;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.PACKAGE_WRITE;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertDirNoAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertDirReadWriteAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertFileNoAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertFileReadWriteAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.buildGiftForPackage;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.getAllPackageSpecificPaths;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.readInt;
+
 import android.os.Environment;
 import android.test.AndroidTestCase;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
+import java.util.List;
 
 /**
- * Test if {@link Environment#getExternalStorageDirectory()} is readable.
+ * Test external storage from an application that has no external storage
+ * permissions.
  */
 public class ExternalStorageTest extends AndroidTestCase {
 
-    private static final String TEST_FILE = "meow";
-
-    private void assertExternalStorageMounted() {
-        assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
+    public void testPrimaryNoAccess() throws Exception {
+        assertDirNoAccess(Environment.getExternalStorageDirectory());
     }
 
-    private void readExternalStorage() throws IOException {
-        final File file = new File(Environment.getExternalStorageDirectory(), TEST_FILE);
-        final InputStream is = new FileInputStream(file);
-        try {
-            is.read();
-        } finally {
-            is.close();
+    /**
+     * Verify that above our package directories we always have no access.
+     */
+    public void testAllWalkingUpTreeNoAccess() throws Exception {
+        final List<File> paths = getAllPackageSpecificPaths(getContext());
+        final String packageName = getContext().getPackageName();
+
+        for (File path : paths) {
+            assertTrue(path.getAbsolutePath().contains(packageName));
+
+            // Walk up until we drop our package
+            while (path.getAbsolutePath().contains(packageName)) {
+                assertDirReadWriteAccess(path);
+                path = path.getParentFile();
+            }
+
+            // Keep walking up until we leave device
+            while (Environment.MEDIA_MOUNTED.equals(Environment.getStorageState(path))) {
+                assertDirNoAccess(path);
+                path = path.getParentFile();
+            }
         }
     }
 
-    public void testFailReadExternalStorage() throws Exception {
-        assertExternalStorageMounted();
-        try {
-            readExternalStorage();
-            fail("able read external file");
-        } catch (IOException e) {
-            // expected
-            e.printStackTrace();
-        }
+    /**
+     * Verify we can read only our gifts.
+     */
+    public void doVerifyGifts() throws Exception {
+        final File none = buildGiftForPackage(getContext(), PACKAGE_NONE);
+        assertFileReadWriteAccess(none);
+        assertEquals(100, readInt(none));
+
+        final File read = buildGiftForPackage(getContext(), PACKAGE_READ);
+        assertFileNoAccess(read);
+
+        final File write = buildGiftForPackage(getContext(), PACKAGE_WRITE);
+        assertFileNoAccess(write);
     }
 }
diff --git a/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/Android.mk b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/Android.mk
new file mode 100644
index 0000000..44e4bef
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/Android.mk
@@ -0,0 +1,29 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src) \
+    ../ExternalStorageApp/src/com/android/cts/externalstorageapp/CommonExternalStorageTest.java
+
+LOCAL_PACKAGE_NAME := CtsReadExternalStorageApp
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/AndroidManifest.xml
new file mode 100644
index 0000000..f6582b9
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+       package="com.android.cts.readexternalstorageapp">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="com.android.cts.readexternalstorageapp" />
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/src/com/android/cts/readexternalstorageapp/ReadExternalStorageTest.java b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/src/com/android/cts/readexternalstorageapp/ReadExternalStorageTest.java
new file mode 100644
index 0000000..d315651
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/src/com/android/cts/readexternalstorageapp/ReadExternalStorageTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.readexternalstorageapp;
+
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.PACKAGE_NONE;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.PACKAGE_READ;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.PACKAGE_WRITE;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertDirReadOnlyAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertDirReadWriteAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertFileReadOnlyAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertFileReadWriteAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.buildGiftForPackage;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.getAllPackageSpecificPaths;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.readInt;
+
+import android.os.Environment;
+import android.test.AndroidTestCase;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Test external storage from an application that has
+ * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}.
+ */
+public class ReadExternalStorageTest extends AndroidTestCase {
+
+    public void testPrimaryReadOnly() throws Exception {
+        assertDirReadOnlyAccess(Environment.getExternalStorageDirectory());
+    }
+
+    /**
+     * Verify that above our package directories we always have read only
+     * access.
+     */
+    public void testAllWalkingUpTreeReadOnly() throws Exception {
+        final List<File> paths = getAllPackageSpecificPaths(getContext());
+        final String packageName = getContext().getPackageName();
+
+        for (File path : paths) {
+            assertTrue(path.getAbsolutePath().contains(packageName));
+
+            // Walk up until we drop our package
+            while (path.getAbsolutePath().contains(packageName)) {
+                assertDirReadWriteAccess(path);
+                path = path.getParentFile();
+            }
+
+            // Keep walking up until we leave device
+            while (Environment.MEDIA_MOUNTED.equals(Environment.getStorageState(path))) {
+                assertDirReadOnlyAccess(path);
+                path = path.getParentFile();
+            }
+        }
+    }
+
+    /**
+     * Verify we can read all gifts.
+     */
+    public void doVerifyGifts() throws Exception {
+        final File none = buildGiftForPackage(getContext(), PACKAGE_NONE);
+        assertFileReadOnlyAccess(none);
+        assertEquals(100, readInt(none));
+
+        final File read = buildGiftForPackage(getContext(), PACKAGE_READ);
+        assertFileReadWriteAccess(read);
+        assertEquals(101, readInt(read));
+
+        final File write = buildGiftForPackage(getContext(), PACKAGE_WRITE);
+        assertFileReadOnlyAccess(write);
+        assertEquals(102, readInt(write));
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/Android.mk b/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/Android.mk
index 9e056a9..4352bfb 100644
--- a/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/Android.mk
@@ -17,9 +17,11 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE_TAGS := tests
-LOCAL_SDK_VERSION := 10
+LOCAL_SDK_VERSION := current
 
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES := $(call all-java-files-under, src) \
+    ../ExternalStorageApp/src/com/android/cts/externalstorageapp/CommonExternalStorageTest.java
+
 LOCAL_PACKAGE_NAME := CtsWriteExternalStorageApp
 
 LOCAL_DEX_PREOPT := false
diff --git a/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/src/com/android/cts/writeexternalstorageapp/WriteExternalStorageTest.java b/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/src/com/android/cts/writeexternalstorageapp/WriteExternalStorageTest.java
index 3f103b6..ff2f1b7 100644
--- a/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/src/com/android/cts/writeexternalstorageapp/WriteExternalStorageTest.java
+++ b/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/src/com/android/cts/writeexternalstorageapp/WriteExternalStorageTest.java
@@ -16,19 +16,30 @@
 
 package com.android.cts.writeexternalstorageapp;
 
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.PACKAGE_NONE;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.PACKAGE_READ;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.PACKAGE_WRITE;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertDirReadOnlyAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertDirReadWriteAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertFileReadWriteAccess;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.buildGiftForPackage;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.deleteContents;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.getAllPackageSpecificPaths;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.getPrimaryPackageSpecificPaths;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.getSecondaryPackageSpecificPaths;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.readInt;
+import static com.android.cts.externalstorageapp.CommonExternalStorageTest.writeInt;
+
 import android.os.Environment;
 import android.test.AndroidTestCase;
 
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
+import java.util.List;
 import java.util.Random;
 
 /**
- * Test if {@link Environment#getExternalStorageDirectory()} is writable.
+ * Test external storage from an application that has
+ * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE}.
  */
 public class WriteExternalStorageTest extends AndroidTestCase {
 
@@ -57,6 +68,10 @@
         }
     }
 
+    private void assertExternalStorageMounted() {
+        assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
+    }
+
     public void testReadExternalStorage() throws Exception {
         assertExternalStorageMounted();
         Environment.getExternalStorageDirectory().list();
@@ -93,25 +108,124 @@
         }
     }
 
-    private static void assertExternalStorageMounted() {
-        assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
+    public void testPrimaryReadWrite() throws Exception {
+        assertDirReadWriteAccess(Environment.getExternalStorageDirectory());
     }
 
-    private static void writeInt(File file, int value) throws IOException {
-        final DataOutputStream os = new DataOutputStream(new FileOutputStream(file));
-        try {
-            os.writeInt(value);
-        } finally {
-            os.close();
+    /**
+     * Verify that above our package directories (on primary storage) we always
+     * have write access.
+     */
+    public void testPrimaryWalkingUpTreeReadWrite() throws Exception {
+        final List<File> paths = getPrimaryPackageSpecificPaths(getContext());
+        final String packageName = getContext().getPackageName();
+
+        for (File path : paths) {
+            assertTrue(path.getAbsolutePath().contains(packageName));
+
+            // Walk until we leave device, writing the whole way
+            while (Environment.MEDIA_MOUNTED.equals(Environment.getStorageState(path))) {
+                assertDirReadWriteAccess(path);
+                path = path.getParentFile();
+            }
         }
     }
 
-    private static int readInt(File file) throws IOException {
-        final DataInputStream is = new DataInputStream(new FileInputStream(file));
-        try {
-            return is.readInt();
-        } finally {
-            is.close();
+    /**
+     * Verify that we have write access in other packages on primary external
+     * storage.
+     */
+    public void testPrimaryOtherPackageWriteAccess() throws Exception {
+        deleteContents(Environment.getExternalStorageDirectory());
+
+        final File ourCache = getContext().getExternalCacheDir();
+        final File otherCache = new File(ourCache.getAbsolutePath()
+                .replace(getContext().getPackageName(), PACKAGE_NONE));
+
+        assertTrue(otherCache.mkdirs());
+        assertDirReadWriteAccess(otherCache);
+    }
+
+    /**
+     * Verify that we have write access in our package-specific directories on
+     * secondary storage devices, but it becomes read-only access above them.
+     */
+    public void testSecondaryWalkingUpTreeReadOnly() throws Exception {
+        final List<File> paths = getSecondaryPackageSpecificPaths(getContext());
+        final String packageName = getContext().getPackageName();
+
+        for (File path : paths) {
+            assertTrue(path.getAbsolutePath().contains(packageName));
+
+            // Walk up until we drop our package
+            while (path.getAbsolutePath().contains(packageName)) {
+                assertDirReadWriteAccess(path);
+                path = path.getParentFile();
+            }
+
+            // Keep walking up until we leave device
+            while (Environment.MEDIA_MOUNTED.equals(Environment.getStorageState(path))) {
+                assertDirReadOnlyAccess(path);
+                path = path.getParentFile();
+            }
         }
     }
+
+    /**
+     * Verify that .nomedia is created correctly.
+     */
+    public void testVerifyNoMediaCreated() throws Exception {
+        deleteContents(Environment.getExternalStorageDirectory());
+
+        final List<File> paths = getAllPackageSpecificPaths(getContext());
+
+        // Require that .nomedia was created somewhere above each dir
+        for (File path : paths) {
+            final File start = path;
+
+            boolean found = false;
+            while (Environment.MEDIA_MOUNTED.equals(Environment.getStorageState(path))) {
+                final File test = new File(path, ".nomedia");
+                if (test.exists()) {
+                    found = true;
+                    break;
+                }
+                path = path.getParentFile();
+            }
+
+            if (!found) {
+                fail("Missing .nomedia file above package-specific directory " + start
+                        + "; gave up at " + path);
+            }
+        }
+    }
+
+    /**
+     * Leave gifts for other packages in their primary external cache dirs.
+     */
+    public void doWriteGifts() throws Exception {
+        final File none = buildGiftForPackage(getContext(), PACKAGE_NONE);
+        none.getParentFile().mkdirs();
+        none.createNewFile();
+        assertFileReadWriteAccess(none);
+
+        writeInt(none, 100);
+        assertEquals(100, readInt(none));
+
+        final File read = buildGiftForPackage(getContext(), PACKAGE_READ);
+        read.getParentFile().mkdirs();
+        read.createNewFile();
+        assertFileReadWriteAccess(read);
+
+        writeInt(read, 101);
+        assertEquals(101, readInt(read));
+
+        final File write = buildGiftForPackage(getContext(), PACKAGE_WRITE);
+        write.getParentFile().mkdirs();
+        write.createNewFile();
+        assertFileReadWriteAccess(write);
+
+        writeInt(write, 102);
+        assertEquals(102, readInt(write));
+    }
 }