Modifying content provider query to return directory content.

If Uri points at a file, query returns the data about the file itself.
If Uri points at a directory, it will return the directory content -
like using "ls" command.

Adding two additional columns, is_directory and name. Adding more complex
query tests.

Bug: 123529934
Bug: 123528227
Test: atest TradefedContentProviderTest
Change-Id: I97c44d0c8b0be7077bf6baf8dfedb67c71d67aa1
Merged-In: I97c44d0c8b0be7077bf6baf8dfedb67c71d67aa1
diff --git a/res/apks/contentprovider/TradefedContentProvider.apk b/res/apks/contentprovider/TradefedContentProvider.apk
index 0f89d63..5bad779 100644
--- a/res/apks/contentprovider/TradefedContentProvider.apk
+++ b/res/apks/contentprovider/TradefedContentProvider.apk
Binary files differ
diff --git a/util-apps/ContentProvider/androidTest/src/android/tradefed/contentprovider/ManagedFileContentProviderTest.java b/util-apps/ContentProvider/androidTest/src/android/tradefed/contentprovider/ManagedFileContentProviderTest.java
index cb13496..8ba5559 100644
--- a/util-apps/ContentProvider/androidTest/src/android/tradefed/contentprovider/ManagedFileContentProviderTest.java
+++ b/util-apps/ContentProvider/androidTest/src/android/tradefed/contentprovider/ManagedFileContentProviderTest.java
@@ -46,30 +46,32 @@
 @RunWith(AndroidJUnit4.class)
 public class ManagedFileContentProviderTest {
 
-    public static final String CONTENT_PROVIDER =
-            String.format("%s://android.tradefed.contentprovider", ContentResolver.SCHEME_CONTENT);
+    public static final String CONTENT_PROVIDER_AUTHORITY = "android.tradefed.contentprovider";
     private static final String TEST_FILE = "ManagedFileContentProviderTest.txt";
+    private static final String TEST_DIRECTORY = "ManagedFileContentProviderTestDir";
+    private static final String TEST_SUBDIRECTORY = "ManagedFileContentProviderTestSubDir";
 
     private File mTestFile = null;
+    private File mTestDir = null;
+    private File mTestSubdir = null;
+
+    private Uri mTestFileUri;
+    private Uri mTestDirUri;
+    private Uri mTestSubdirUri;
+
     private Context mAppContext;
+    private ContentResolver mResolver;
     private List<Uri> mShouldBeCleaned = new ArrayList<>();
     private ContentValues mCv;
-    private Uri mTestUri;
 
     @Before
     public void setUp() throws Exception {
         mCv = new ContentValues();
-        mTestFile = new File(Environment.getExternalStorageDirectory(), TEST_FILE);
-        if (mTestFile.exists()) {
-            mTestFile.delete();
-        }
-        mTestFile.createNewFile();
+
         // Context of the app under test.
         mAppContext = InstrumentationRegistry.getTargetContext();
+        mResolver = mAppContext.getContentResolver();
         assertEquals("android.tradefed.contentprovider", mAppContext.getPackageName());
-
-        String fullUriPath = String.format("%s%s", CONTENT_PROVIDER, mTestFile.getAbsolutePath());
-        mTestUri = Uri.parse(fullUriPath);
     }
 
     @After
@@ -77,59 +79,76 @@
         if (mTestFile != null) {
             mTestFile.delete();
         }
-        for (Uri uri : mShouldBeCleaned) {
-            mAppContext
-                    .getContentResolver()
-                    .delete(
-                            uri,
-                            /* selection */
-                            null,
-                            /* selectionArgs */
-                            null);
+        if (mTestDir != null) {
+            mTestDir.delete();
         }
+        if (mTestSubdir != null) {
+            mTestSubdir.delete();
+        }
+        for (Uri uri : mShouldBeCleaned) {
+            mResolver.delete(
+                    uri,
+                    /** selection * */
+                    null,
+                    /** selectionArgs * */
+                    null);
+        }
+    }
+
+    private void createTestDirectories() throws Exception {
+        mTestDir = new File(Environment.getExternalStorageDirectory(), TEST_DIRECTORY);
+        mTestDir.mkdir();
+        mTestSubdir = new File(mTestDir, TEST_SUBDIRECTORY);
+        mTestSubdir.mkdir();
+        createTestFile(mTestDir);
+    }
+
+    private void createTestFile(File parentDir) throws Exception {
+        mTestFile = new File(parentDir, TEST_FILE);
+        mTestFile.createNewFile();
+
+        mTestFileUri = createContentUri(mTestFile.getAbsolutePath());
     }
 
     /** Test that we can delete a file from the content provider. */
     @Test
     public void testDelete() throws Exception {
-        ContentResolver resolver = mAppContext.getContentResolver();
-        Uri uriResult = resolver.insert(mTestUri, mCv);
-        mShouldBeCleaned.add(mTestUri);
+        createTestFile(Environment.getExternalStorageDirectory());
+        Uri uriResult = mResolver.insert(mTestFileUri, mCv);
+        mShouldBeCleaned.add(mTestFileUri);
         // Insert is successful
-        assertEquals(mTestUri, uriResult);
+        assertEquals(mTestFileUri, uriResult);
+
         // Trying to insert again is inop
-        Uri reInsert = resolver.insert(mTestUri, mCv);
+        Uri reInsert = mResolver.insert(mTestFileUri, mCv);
         assertNull(reInsert);
+
         // Now delete
         int affected =
-                resolver.delete(
-                        mTestUri,
-                        /* selection */
+                mResolver.delete(
+                        mTestFileUri,
+                        /** selection * */
                         null,
                         /* selectionArgs */
                         null);
         assertEquals(1, affected);
         // File should have been deleted.
         assertFalse(mTestFile.exists());
+
         // We can now insert again
         mTestFile.createNewFile();
-        uriResult = resolver.insert(mTestUri, mCv);
-        assertEquals(mTestUri, uriResult);
+        uriResult = mResolver.insert(mTestFileUri, mCv);
+        assertEquals(mTestFileUri, uriResult);
     }
 
-    /** Test that querying the content provider is working. */
+    /** Test that querying the content provider for a single File returns null. */
     @Test
-    public void testQuery() throws Exception {
-        ContentResolver resolver = mAppContext.getContentResolver();
-        Uri uriResult = resolver.insert(mTestUri, mCv);
-        mShouldBeCleaned.add(mTestUri);
-        // Insert is successful
-        assertEquals(mTestUri, uriResult);
-
+    public void testQueryForFile() throws Exception {
+        createTestFile(Environment.getExternalStorageDirectory());
         Cursor cursor =
-                resolver.query(
-                        mTestUri,
-                        /* projection */
+                mResolver.query(
+                        mTestFileUri,
+                        /** projection * */
                         null,
                         /* selection */
                         null,
@@ -142,35 +161,33 @@
             String[] columns = cursor.getColumnNames();
             assertEquals(ManagedFileContentProvider.COLUMNS, columns);
             assertTrue(cursor.moveToNext());
+
+            // Test values in all columns and enforce column ordering.
+            // Name
+            assertEquals(TEST_FILE, cursor.getString(0));
             // Absolute path
             assertEquals(
                     Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + TEST_FILE,
-                    cursor.getString(0));
-            // Uri
-            assertEquals(mTestUri.toString(), cursor.getString(1));
+                    cursor.getString(1));
+            // Is directory
+            assertEquals("false", cursor.getString(2));
             // Type
-            assertEquals("text/plain", cursor.getString(2));
+            assertEquals("text/plain", cursor.getString(3));
             // Metadata
-            assertNull(cursor.getString(3));
+            assertNull(cursor.getString(4));
         } finally {
             cursor.close();
         }
     }
 
-    /** Test that querying the content provider is working when abstracting the sdcard */
+    /** Test that querying the content provider for a file is working when abstracting the sdcard */
     @Test
-    public void testQuery_sdcard() throws Exception {
-        ContentResolver resolver = mAppContext.getContentResolver();
-        Uri uriResult = resolver.insert(mTestUri, mCv);
-        mShouldBeCleaned.add(mTestUri);
-        // Insert is successful
-        assertEquals(mTestUri, uriResult);
-
-        String sdcardUriPath = String.format("%s/sdcard/%s", CONTENT_PROVIDER, mTestFile.getName());
-        Uri sdcardUri = Uri.parse(sdcardUriPath);
+    public void testQueryForFile_sdcard() throws Exception {
+        createTestFile(Environment.getExternalStorageDirectory());
+        Uri sdcardUri = createContentUri(String.format("sdcard/%s", mTestFile.getName()));
 
         Cursor cursor =
-                resolver.query(
+                mResolver.query(
                         sdcardUri,
                         /* projection */
                         null,
@@ -185,18 +202,100 @@
             String[] columns = cursor.getColumnNames();
             assertEquals(ManagedFileContentProvider.COLUMNS, columns);
             assertTrue(cursor.moveToNext());
+
+            // Test values in all columns and enforce column ordering.
+            // Name
+            assertEquals(TEST_FILE, cursor.getString(0));
             // Absolute path
             assertEquals(
                     Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + TEST_FILE,
-                    cursor.getString(0));
-            // Uri
-            assertEquals(sdcardUri.toString(), cursor.getString(1));
+                    cursor.getString(1));
+            // Is directory
+            assertEquals("false", cursor.getString(2));
             // Type
-            assertEquals("text/plain", cursor.getString(2));
+            assertEquals("text/plain", cursor.getString(3));
             // Metadata
-            assertNull(cursor.getString(3));
+            assertNull(cursor.getString(4));
         } finally {
             cursor.close();
         }
     }
+
+    /**
+     * Test that querying the content provider for a directory returns content of the directory -
+     * one row per each subdirectory/file.
+     */
+    @Test
+    public void testQueryForDirectoryContent() throws Exception {
+        createTestDirectories();
+
+        mTestDirUri = createContentUri(mTestDir.getAbsolutePath());
+        Cursor cursor =
+                mResolver.query(
+                        mTestDirUri,
+                        /** projection * */
+                        null,
+                        /** selection * */
+                        null,
+                        /** selectionArgs* */
+                        null,
+                        /** sortOrder * */
+                        null);
+        try {
+            // One row for subdir, one row for a file.
+            assertEquals(2, cursor.getCount());
+            String[] columns = cursor.getColumnNames();
+            assertEquals(ManagedFileContentProvider.COLUMNS, columns);
+
+            // Test the file.
+            assertTrue(cursor.moveToNext());
+            // Test values in all columns and enforce column ordering.
+            // Name
+            assertEquals(TEST_FILE, cursor.getString(0));
+            // Absolute path
+            assertEquals(
+                    Environment.getExternalStorageDirectory().getAbsolutePath()
+                            + "/"
+                            + TEST_DIRECTORY
+                            + "/"
+                            + TEST_FILE,
+                    cursor.getString(1));
+            // Is directory
+            assertEquals("false", cursor.getString(2));
+            // Type
+            assertEquals("text/plain", cursor.getString(3));
+            // Metadata
+            assertNull(cursor.getString(4));
+
+            // Test the subdirectory.
+            assertTrue(cursor.moveToNext());
+            // Test values in all columns and enforce column ordering.
+            // Name
+            assertEquals(TEST_SUBDIRECTORY, cursor.getString(0));
+            // Absolute path
+            assertEquals(
+                    Environment.getExternalStorageDirectory().getAbsolutePath()
+                            + "/"
+                            + TEST_DIRECTORY
+                            + "/"
+                            + TEST_SUBDIRECTORY,
+                    cursor.getString(1));
+            // Is directory
+            assertEquals("true", cursor.getString(2));
+            // Type
+            assertNull(cursor.getString(3));
+            // Metadata
+            assertNull(cursor.getString(4));
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private Uri createContentUri(String path) {
+        Uri.Builder builder = new Uri.Builder();
+        return builder.scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(CONTENT_PROVIDER_AUTHORITY)
+                .appendPath(path)
+                .build();
+    }
 }
diff --git a/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java b/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java
index 2cb1c2e..f759512 100644
--- a/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java
+++ b/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java
@@ -31,9 +31,9 @@
 import java.io.FileNotFoundException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
-import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -44,14 +44,21 @@
  * <p>This implementation aims to be standard and work in all situations.
  */
 public class ManagedFileContentProvider extends ContentProvider {
-
+    public static final String COLUMN_NAME = "name";
     public static final String COLUMN_ABSOLUTE_PATH = "absolute_path";
-    public static final String COLUMN_URI = "uri";
+    public static final String COLUMN_DIRECTORY = "is_directory";
     public static final String COLUMN_MIME_TYPE = "mime_type";
     public static final String COLUMN_METADATA = "metadata";
+
     // TODO: Complete the list of columns
     public static final String[] COLUMNS =
-            new String[] {COLUMN_ABSOLUTE_PATH, COLUMN_URI, COLUMN_MIME_TYPE, COLUMN_METADATA};
+            new String[] {
+                COLUMN_NAME,
+                COLUMN_ABSOLUTE_PATH,
+                COLUMN_DIRECTORY,
+                COLUMN_MIME_TYPE,
+                COLUMN_METADATA
+            };
 
     private static String TAG = "ManagedFileContentProvider";
     private static MimeTypeMap sMimeMap = MimeTypeMap.getSingleton();
@@ -64,6 +71,19 @@
         return true;
     }
 
+    /**
+     * Use a content URI with absolute device path embedded to get information about a file or a
+     * directory on the device.
+     *
+     * @param uri A content uri that contains the path to the desired file/directory.
+     * @param projection - not supported.
+     * @param selection - not supported.
+     * @param selectionArgs - not supported.
+     * @param sortOrder - not supported.
+     * @return A {@link Cursor} containing the results of the query. Cursor contains a single row
+     *     for files and for directories it returns one row for each {@link File} returned by {@link
+     *     File#listFiles()}.
+     */
     @Nullable
     @Override
     public Cursor query(
@@ -78,13 +98,7 @@
             final MatrixCursor cursor = new MatrixCursor(COLUMNS, mFileTracker.size());
             for (Map.Entry<Uri, ContentValues> path : mFileTracker.entrySet()) {
                 String metadata = path.getValue().getAsString(COLUMN_METADATA);
-                cursor.addRow(
-                        new String[] {
-                            getFileForUri(path.getKey()).getAbsolutePath(),
-                            uri.toString(),
-                            getType(path.getKey()),
-                            metadata
-                        });
+                cursor.addRow(getRow(COLUMNS, getFileForUri(path.getKey()), metadata));
             }
             return cursor;
         }
@@ -94,47 +108,27 @@
             return null;
         }
 
-        // If a particular file is requested, find it and return it.
-        List<String> filePaths = new ArrayList<>();
-        if (file.isDirectory()) {
-            readDirectory(filePaths, file);
-        } else {
-            // If not a directory, return a single row - the name of the file.
-            filePaths.add(file.getAbsolutePath());
+        if (!file.isDirectory()) {
+            // Just return the information about the file itself.
+            final MatrixCursor cursor = new MatrixCursor(COLUMNS, 1);
+            cursor.addRow(getRow(COLUMNS, file, /* metadata= */ null));
+            return cursor;
         }
 
-        // Add all the paths to the cursor.
-        final MatrixCursor cursor = new MatrixCursor(COLUMNS, filePaths.size());
-        for (String path : filePaths) {
-            // TODO: Return a properly formed uri for each filepath
-            cursor.addRow(
-                    new String[] {
-                        path,
-                        uri.toString(),
-                        getType(uri),
-                        /* metadata */
-                        null
-                    });
+        // Otherwise return the content of the directory - similar to doing ls command.
+        File[] files = file.listFiles();
+        sortFilesByAbsolutePath(files);
+        final MatrixCursor cursor = new MatrixCursor(COLUMNS, files.length + 1);
+        for (File child : files) {
+            cursor.addRow(getRow(COLUMNS, child, /* metadata= */ null));
         }
-
         return cursor;
     }
 
     @Nullable
     @Override
     public String getType(@NonNull Uri uri) {
-        final File file = getFileForUri(uri);
-
-        final int lastDot = file.getName().lastIndexOf('.');
-        if (lastDot >= 0) {
-            final String extension = file.getName().substring(lastDot + 1);
-            final String mime = sMimeMap.getMimeTypeFromExtension(extension);
-            if (mime != null) {
-                return mime;
-            }
-        }
-
-        return "application/octet-stream";
+        return getType(getFileForUri(uri));
     }
 
     @Nullable
@@ -206,6 +200,43 @@
         return ParcelFileDescriptor.open(file, fileMode);
     }
 
+    private Object[] getRow(String[] columns, File file, String metadata) {
+        Object[] values = new Object[columns.length];
+        for (int i = 0; i < columns.length; i++) {
+            values[i] = getColumnValue(columns[i], file, metadata);
+        }
+        return values;
+    }
+
+    private Object getColumnValue(String columnName, File file, String metadata) {
+        Object value = null;
+        if (COLUMN_NAME.equals(columnName)) {
+            value = file.getName();
+        } else if (COLUMN_ABSOLUTE_PATH.equals(columnName)) {
+            value = file.getAbsolutePath();
+        } else if (COLUMN_DIRECTORY.equals(columnName)) {
+            value = file.isDirectory();
+        } else if (COLUMN_METADATA.equals(columnName)) {
+            value = metadata;
+        } else if (COLUMN_MIME_TYPE.equals(columnName)) {
+            value = file.isDirectory() ? null : getType(file);
+        }
+        return value;
+    }
+
+    private String getType(@NonNull File file) {
+        final int lastDot = file.getName().lastIndexOf('.');
+        if (lastDot >= 0) {
+            final String extension = file.getName().substring(lastDot + 1);
+            final String mime = sMimeMap.getMimeTypeFromExtension(extension);
+            if (mime != null) {
+                return mime;
+            }
+        }
+
+        return "application/octet-stream";
+    }
+
     private File getFileForUri(@NonNull Uri uri) {
         // TODO: apply the /sdcard resolution to query() too.
         String uriPath = uri.getPath();
@@ -222,16 +253,6 @@
         return new File(uriPath);
     }
 
-    private void readDirectory(List<String> files, File dir) {
-        for (File f : dir.listFiles()) {
-            if (f.isDirectory()) {
-                readDirectory(files, f);
-            } else {
-                files.add(f.getAbsolutePath());
-            }
-        }
-    }
-
     /** Copied from FileProvider.java. */
     private static int modeToMode(String mode) {
         int modeBits;
@@ -282,4 +303,15 @@
         }
         return count;
     }
+
+    private void sortFilesByAbsolutePath(File[] files) {
+        Arrays.sort(
+                files,
+                new Comparator<File>() {
+                    @Override
+                    public int compare(File f1, File f2) {
+                        return f1.getAbsolutePath().compareTo(f2.getAbsolutePath());
+                    }
+                });
+    }
 }