Use inotify to update DocumentsUI.

While user is actively looking at a directory, subscribe to inotify
events and notify of content changes to trigger requeries.  Reference
count the observers, since multiple cursors are regularly open during
requeries.

Fix leaking cursors on activity rotation; crazy loader ID generation
is no longer needed.

Bug: 10999396
Change-Id: Iddeb08a056fee80c93df8499874705bcd213a1e2
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index fa8bc9d..775720e 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -84,7 +84,6 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Display the documents inside a single directory.
@@ -127,9 +126,7 @@
     private static final String EXTRA_QUERY = "query";
     private static final String EXTRA_IGNORE_STATE = "ignoreState";
 
-    private static AtomicInteger sLoaderId = new AtomicInteger(4000);
-
-    private final int mLoaderId = sLoaderId.incrementAndGet();
+    private final int mLoaderId = 42;
 
     public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
         show(fm, TYPE_NORMAL, root, doc, null, anim);
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 65e3eee..559e052 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -16,14 +16,17 @@
 
 package com.android.externalstorage;
 
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
 import android.graphics.Point;
+import android.net.Uri;
 import android.os.CancellationSignal;
 import android.os.Environment;
+import android.os.FileObserver;
 import android.os.ParcelFileDescriptor;
 import android.os.storage.StorageManager;
 import android.os.storage.StorageVolume;
@@ -49,6 +52,8 @@
 public class ExternalStorageProvider extends DocumentsProvider {
     private static final String TAG = "ExternalStorage";
 
+    private static final boolean LOG_INOTIFY = false;
+
     public static final String AUTHORITY = "com.android.externalstorage.documents";
 
     // docId format: root:path/to/file
@@ -83,6 +88,9 @@
     @GuardedBy("mRootsLock")
     private HashMap<String, File> mIdToPath;
 
+    @GuardedBy("mObservers")
+    private Map<File, DirectoryObserver> mObservers = Maps.newHashMap();
+
     @Override
     public boolean onCreate() {
         mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
@@ -327,8 +335,9 @@
     public Cursor queryChildDocuments(
             String parentDocumentId, String[] projection, String sortOrder)
             throws FileNotFoundException {
-        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
         final File parent = getFileForDocId(parentDocumentId);
+        final MatrixCursor result = new DirectoryCursor(
+                resolveDocumentProjection(projection), parentDocumentId, parent);
         for (File file : parent.listFiles()) {
             includeFile(result, null, file);
         }
@@ -431,4 +440,86 @@
         }
         return name;
     }
+
+    private void startObserving(File file, Uri notifyUri) {
+        synchronized (mObservers) {
+            DirectoryObserver observer = mObservers.get(file);
+            if (observer == null) {
+                observer = new DirectoryObserver(
+                        file, getContext().getContentResolver(), notifyUri);
+                observer.startWatching();
+                mObservers.put(file, observer);
+            }
+            observer.mRefCount++;
+
+            if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
+        }
+    }
+
+    private void stopObserving(File file) {
+        synchronized (mObservers) {
+            DirectoryObserver observer = mObservers.get(file);
+            if (observer == null) return;
+
+            observer.mRefCount--;
+            if (observer.mRefCount == 0) {
+                mObservers.remove(file);
+                observer.stopWatching();
+            }
+
+            if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
+        }
+    }
+
+    private static class DirectoryObserver extends FileObserver {
+        private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
+                | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
+
+        private final File mFile;
+        private final ContentResolver mResolver;
+        private final Uri mNotifyUri;
+
+        private int mRefCount = 0;
+
+        public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
+            super(file.getAbsolutePath(), NOTIFY_EVENTS);
+            mFile = file;
+            mResolver = resolver;
+            mNotifyUri = notifyUri;
+        }
+
+        @Override
+        public void onEvent(int event, String path) {
+            if ((event & NOTIFY_EVENTS) != 0) {
+                if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
+                mResolver.notifyChange(mNotifyUri, null, false);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
+        }
+    }
+
+    private class DirectoryCursor extends MatrixCursor {
+        private final File mFile;
+
+        public DirectoryCursor(String[] columnNames, String docId, File file) {
+            super(columnNames);
+
+            final Uri notifyUri = DocumentsContract.buildChildDocumentsUri(
+                    AUTHORITY, docId);
+            setNotificationUri(getContext().getContentResolver(), notifyUri);
+
+            mFile = file;
+            startObserving(mFile, notifyUri);
+        }
+
+        @Override
+        public void close() {
+            super.close();
+            stopObserving(mFile);
+        }
+    }
 }