Guide user towards adoption when card is "empty".

When a newly inserted SD card is empty, we'd like to guide the user
towards adopting it.  Similarly, if the card contains personal media
like photos, we'd like to guide the user towards using it as portable
storage.

Do this by quickly hunting around on the card for files under various
well-known directories.  Special logic to ignore bundled "helper"
apps included from the SD card factory.

Test: bit FrameworksCoreTests:android.os.EnvironmentTest
Bug: 69128181
Change-Id: I10e43d2e76379fac5137eb3810742c33f5f57d80
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index f977c1d..ce82062 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -24,6 +24,7 @@
 import android.util.Log;
 
 import java.io.File;
+import java.util.LinkedList;
 
 /**
  * Provides access to environment variables.
@@ -608,6 +609,79 @@
         return false;
     }
 
+    /** {@hide} */ public static final int HAS_MUSIC = 1 << 0;
+    /** {@hide} */ public static final int HAS_PODCASTS = 1 << 1;
+    /** {@hide} */ public static final int HAS_RINGTONES = 1 << 2;
+    /** {@hide} */ public static final int HAS_ALARMS = 1 << 3;
+    /** {@hide} */ public static final int HAS_NOTIFICATIONS = 1 << 4;
+    /** {@hide} */ public static final int HAS_PICTURES = 1 << 5;
+    /** {@hide} */ public static final int HAS_MOVIES = 1 << 6;
+    /** {@hide} */ public static final int HAS_DOWNLOADS = 1 << 7;
+    /** {@hide} */ public static final int HAS_DCIM = 1 << 8;
+    /** {@hide} */ public static final int HAS_DOCUMENTS = 1 << 9;
+
+    /** {@hide} */ public static final int HAS_ANDROID = 1 << 16;
+    /** {@hide} */ public static final int HAS_OTHER = 1 << 17;
+
+    /**
+     * Classify the content types present on the given external storage device.
+     * <p>
+     * This is typically useful for deciding if an inserted SD card is empty, or
+     * if it contains content like photos that should be preserved.
+     *
+     * @hide
+     */
+    public static int classifyExternalStorageDirectory(File dir) {
+        int res = 0;
+        for (File f : FileUtils.listFilesOrEmpty(dir)) {
+            if (f.isFile() && isInterestingFile(f)) {
+                res |= HAS_OTHER;
+            } else if (f.isDirectory() && hasInterestingFiles(f)) {
+                final String name = f.getName();
+                if (DIRECTORY_MUSIC.equals(name)) res |= HAS_MUSIC;
+                else if (DIRECTORY_PODCASTS.equals(name)) res |= HAS_PODCASTS;
+                else if (DIRECTORY_RINGTONES.equals(name)) res |= HAS_RINGTONES;
+                else if (DIRECTORY_ALARMS.equals(name)) res |= HAS_ALARMS;
+                else if (DIRECTORY_NOTIFICATIONS.equals(name)) res |= HAS_NOTIFICATIONS;
+                else if (DIRECTORY_PICTURES.equals(name)) res |= HAS_PICTURES;
+                else if (DIRECTORY_MOVIES.equals(name)) res |= HAS_MOVIES;
+                else if (DIRECTORY_DOWNLOADS.equals(name)) res |= HAS_DOWNLOADS;
+                else if (DIRECTORY_DCIM.equals(name)) res |= HAS_DCIM;
+                else if (DIRECTORY_DOCUMENTS.equals(name)) res |= HAS_DOCUMENTS;
+                else if (DIRECTORY_ANDROID.equals(name)) res |= HAS_ANDROID;
+                else res |= HAS_OTHER;
+            }
+        }
+        return res;
+    }
+
+    private static boolean hasInterestingFiles(File dir) {
+        final LinkedList<File> explore = new LinkedList<>();
+        explore.add(dir);
+        while (!explore.isEmpty()) {
+            dir = explore.pop();
+            for (File f : FileUtils.listFilesOrEmpty(dir)) {
+                if (isInterestingFile(f)) return true;
+                if (f.isDirectory()) explore.add(f);
+            }
+        }
+        return false;
+    }
+
+    private static boolean isInterestingFile(File file) {
+        if (file.isFile()) {
+            final String name = file.getName().toLowerCase();
+            if (name.endsWith(".exe") || name.equals("autorun.inf")
+                    || name.equals("launchpad.zip") || name.equals(".nomedia")) {
+                return false;
+            } else {
+                return true;
+            }
+        } else {
+            return false;
+        }
+    }
+
     /**
      * Get a top-level shared/external storage directory for placing files of a
      * particular type. This is where the user will typically place and manage
diff --git a/core/tests/coretests/src/android/os/EnvironmentTest.java b/core/tests/coretests/src/android/os/EnvironmentTest.java
new file mode 100644
index 0000000..5189df5
--- /dev/null
+++ b/core/tests/coretests/src/android/os/EnvironmentTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017 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 android.os;
+
+import static android.os.Environment.HAS_ANDROID;
+import static android.os.Environment.HAS_DCIM;
+import static android.os.Environment.HAS_DOWNLOADS;
+import static android.os.Environment.HAS_OTHER;
+import static android.os.Environment.classifyExternalStorageDirectory;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+public class EnvironmentTest {
+    private File dir;
+
+    private Context getContext() {
+        return InstrumentationRegistry.getContext();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        dir = getContext().getDir("testing", Context.MODE_PRIVATE);
+        FileUtils.deleteContents(dir);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        FileUtils.deleteContents(dir);
+    }
+
+    @Test
+    public void testClassify_empty() {
+        assertEquals(0, classifyExternalStorageDirectory(dir));
+    }
+
+    @Test
+    public void testClassify_emptyDirs() {
+        Environment.buildPath(dir, "DCIM").mkdirs();
+        Environment.buildPath(dir, "DCIM", "January").mkdirs();
+        Environment.buildPath(dir, "Downloads").mkdirs();
+        Environment.buildPath(dir, "LOST.DIR").mkdirs();
+        assertEquals(0, classifyExternalStorageDirectory(dir));
+    }
+
+    @Test
+    public void testClassify_emptyFactory() throws Exception {
+        Environment.buildPath(dir, "autorun.inf").createNewFile();
+        Environment.buildPath(dir, "LaunchU3.exe").createNewFile();
+        Environment.buildPath(dir, "LaunchPad.zip").createNewFile();
+        assertEquals(0, classifyExternalStorageDirectory(dir));
+    }
+
+    @Test
+    public void testClassify_photos() throws Exception {
+        Environment.buildPath(dir, "DCIM").mkdirs();
+        Environment.buildPath(dir, "DCIM", "IMG_1024.JPG").createNewFile();
+        Environment.buildPath(dir, "Download").mkdirs();
+        Environment.buildPath(dir, "Download", "foobar.pdf").createNewFile();
+        assertEquals(HAS_DCIM | HAS_DOWNLOADS, classifyExternalStorageDirectory(dir));
+    }
+
+    @Test
+    public void testClassify_other() throws Exception {
+        Environment.buildPath(dir, "Android").mkdirs();
+        Environment.buildPath(dir, "Android", "com.example").mkdirs();
+        Environment.buildPath(dir, "Android", "com.example", "internal.dat").createNewFile();
+        Environment.buildPath(dir, "Linux").mkdirs();
+        Environment.buildPath(dir, "Linux", "install-amd64-minimal-20170907.iso").createNewFile();
+        assertEquals(HAS_ANDROID | HAS_OTHER, classifyExternalStorageDirectory(dir));
+    }
+
+    @Test
+    public void testClassify_otherRoot() throws Exception {
+        Environment.buildPath(dir, "Taxes.pdf").createNewFile();
+        assertEquals(HAS_OTHER, classifyExternalStorageDirectory(dir));
+    }
+}
diff --git a/core/tests/coretests/src/android/os/FileUtilsTest.java b/core/tests/coretests/src/android/os/FileUtilsTest.java
index 3a751af..cd20192 100644
--- a/core/tests/coretests/src/android/os/FileUtilsTest.java
+++ b/core/tests/coretests/src/android/os/FileUtilsTest.java
@@ -21,15 +21,24 @@
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
 import android.content.Context;
 import android.provider.DocumentsContract.Document;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
 
 import libcore.io.IoUtils;
 
 import com.google.android.collect.Sets;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileOutputStream;
@@ -37,8 +46,8 @@
 import java.util.Arrays;
 import java.util.HashSet;
 
-@MediumTest
-public class FileUtilsTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class FileUtilsTest {
     private static final String TEST_DATA =
             "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
 
@@ -47,10 +56,12 @@
     private File mCopyFile;
     private File mTarget;
 
+    private Context getContext() {
+        return InstrumentationRegistry.getContext();
+    }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @Before
+    public void setUp() throws Exception {
         mDir = getContext().getDir("testing", Context.MODE_PRIVATE);
         mTestFile = new File(mDir, "test.file");
         mCopyFile = new File(mDir, "copy.file");
@@ -59,14 +70,15 @@
         FileUtils.deleteContents(mTarget);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         IoUtils.deleteContents(mDir);
         FileUtils.deleteContents(mTarget);
     }
 
     // TODO: test setPermissions(), getPermissions()
 
+    @Test
     public void testCopyFile() throws Exception {
         stageFile(mTestFile, TEST_DATA);
         assertFalse(mCopyFile.exists());
@@ -75,6 +87,7 @@
         assertEquals(TEST_DATA, FileUtils.readTextFile(mCopyFile, 0, null));
     }
 
+    @Test
     public void testCopyToFile() throws Exception {
         final String s = "Foo Bar";
         assertFalse(mCopyFile.exists());
@@ -83,6 +96,7 @@
         assertEquals(s, FileUtils.readTextFile(mCopyFile, 0, null));
     }
 
+    @Test
     public void testIsFilenameSafe() throws Exception {
         assertTrue(FileUtils.isFilenameSafe(new File("foobar")));
         assertTrue(FileUtils.isFilenameSafe(new File("a_b-c=d.e/0,1+23")));
@@ -90,6 +104,7 @@
         assertFalse(FileUtils.isFilenameSafe(new File("foo\nbar")));
     }
 
+    @Test
     public void testReadTextFile() throws Exception {
         stageFile(mTestFile, TEST_DATA);
 
@@ -110,6 +125,7 @@
         assertEquals(TEST_DATA, FileUtils.readTextFile(mTestFile, -100, "<>"));
     }
 
+    @Test
     public void testReadTextFileWithZeroLengthFile() throws Exception {
         stageFile(mTestFile, TEST_DATA);
         new FileOutputStream(mTestFile).close();  // Zero out the file
@@ -120,6 +136,7 @@
         assertEquals("", FileUtils.readTextFile(mTestFile, -10, "<>"));
     }
 
+    @Test
     public void testContains() throws Exception {
         assertTrue(FileUtils.contains(new File("/"), new File("/moo.txt")));
         assertTrue(FileUtils.contains(new File("/"), new File("/")));
@@ -137,11 +154,13 @@
         assertFalse(FileUtils.contains(new File("/sdcard/"), new File("/sdcard.txt")));
     }
 
+    @Test
     public void testDeleteOlderEmptyDir() throws Exception {
         FileUtils.deleteOlderFiles(mDir, 10, WEEK_IN_MILLIS);
         assertDirContents();
     }
 
+    @Test
     public void testDeleteOlderTypical() throws Exception {
         touch("file1", HOUR_IN_MILLIS);
         touch("file2", 1 * DAY_IN_MILLIS + HOUR_IN_MILLIS);
@@ -152,6 +171,7 @@
         assertDirContents("file1", "file2", "file3");
     }
 
+    @Test
     public void testDeleteOlderInFuture() throws Exception {
         touch("file1", -HOUR_IN_MILLIS);
         touch("file2", HOUR_IN_MILLIS);
@@ -166,6 +186,7 @@
         assertDirContents("file1", "file2");
     }
 
+    @Test
     public void testDeleteOlderOnlyAge() throws Exception {
         touch("file1", HOUR_IN_MILLIS);
         touch("file2", 1 * DAY_IN_MILLIS + HOUR_IN_MILLIS);
@@ -177,6 +198,7 @@
         assertDirContents("file1");
     }
 
+    @Test
     public void testDeleteOlderOnlyCount() throws Exception {
         touch("file1", HOUR_IN_MILLIS);
         touch("file2", 1 * DAY_IN_MILLIS + HOUR_IN_MILLIS);
@@ -188,6 +210,7 @@
         assertDirContents("file1", "file2");
     }
 
+    @Test
     public void testValidExtFilename() throws Exception {
         assertTrue(FileUtils.isValidExtFilename("a"));
         assertTrue(FileUtils.isValidExtFilename("foo.bar"));
@@ -208,6 +231,7 @@
         assertEquals("foo.bar", FileUtils.buildValidExtFilename("foo.bar"));
     }
 
+    @Test
     public void testValidFatFilename() throws Exception {
         assertTrue(FileUtils.isValidFatFilename("a"));
         assertTrue(FileUtils.isValidFatFilename("foo bar.baz"));
@@ -233,6 +257,7 @@
         assertEquals("foo_bar__baz", FileUtils.buildValidFatFilename("foo?bar**baz"));
     }
 
+    @Test
     public void testTrimFilename() throws Exception {
         assertEquals("short.txt", FileUtils.trimFilename("short.txt", 16));
         assertEquals("extrem...eme.txt", FileUtils.trimFilename("extremelylongfilename.txt", 16));
@@ -245,6 +270,7 @@
         assertEquals("a...z", FileUtils.trimFilename(unicode, 6));
     }
 
+    @Test
     public void testBuildUniqueFile_normal() throws Exception {
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test"));
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg"));
@@ -263,6 +289,7 @@
                 FileUtils.buildUniqueFile(mTarget, "application/x-flac", "test.flac"));
     }
 
+    @Test
     public void testBuildUniqueFile_unknown() throws Exception {
         assertNameEquals("test",
                 FileUtils.buildUniqueFile(mTarget, "application/octet-stream", "test"));
@@ -275,6 +302,7 @@
         assertNameEquals("test.lolz", FileUtils.buildUniqueFile(mTarget, "lolz/lolz", "test.lolz"));
     }
 
+    @Test
     public void testBuildUniqueFile_dir() throws Exception {
         assertNameEquals("test", FileUtils.buildUniqueFile(mTarget, Document.MIME_TYPE_DIR, "test"));
         new File(mTarget, "test").mkdir();
@@ -288,6 +316,7 @@
                 FileUtils.buildUniqueFile(mTarget, Document.MIME_TYPE_DIR, "test.jpg"));
     }
 
+    @Test
     public void testBuildUniqueFile_increment() throws Exception {
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg"));
         new File(mTarget, "test.jpg").createNewFile();
@@ -298,6 +327,7 @@
                 FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg"));
     }
 
+    @Test
     public void testBuildUniqueFile_mimeless() throws Exception {
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "test.jpg"));
         new File(mTarget, "test.jpg").createNewFile();
@@ -312,6 +342,7 @@
         assertNameEquals("test.foo (1).bar", FileUtils.buildUniqueFile(mTarget, "test.foo.bar"));
     }
 
+    @Test
     public void testRoundStorageSize() throws Exception {
         final long M128 = 128000000L;
         final long M256 = 256000000L;