Merge changes Id78aab9c,I51096b18 into nyc-dev

* changes:
  Speed up ConnectivityServiceTest.
  Make it easier to test code that uses WakeupMessage.
diff --git a/media/java/android/media/browse/MediaBrowser.java b/media/java/android/media/browse/MediaBrowser.java
index 9e67c15..ada0e2c 100644
--- a/media/java/android/media/browse/MediaBrowser.java
+++ b/media/java/android/media/browse/MediaBrowser.java
@@ -633,7 +633,6 @@
                     return;
                 }
 
-                List<MediaItem> data = list == null ? null : list.getList();
                 if (DBG) {
                     Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
                 }
@@ -644,10 +643,19 @@
                     // Tell the app.
                     SubscriptionCallback subscriptionCallback = subscription.getCallback(options);
                     if (subscriptionCallback != null) {
+                        List<MediaItem> data = list == null ? null : list.getList();
                         if (options == null) {
-                            subscriptionCallback.onChildrenLoaded(parentId, data);
+                            if (data == null) {
+                                subscriptionCallback.onError(parentId);
+                            } else {
+                                subscriptionCallback.onChildrenLoaded(parentId, data);
+                            }
                         } else {
-                            subscriptionCallback.onChildrenLoaded(parentId, data, options);
+                            if (data == null) {
+                                subscriptionCallback.onError(parentId, options);
+                            } else {
+                                subscriptionCallback.onChildrenLoaded(parentId, data, options);
+                            }
                         }
                         return;
                     }
@@ -848,21 +856,21 @@
          * Called when the list of children is loaded or updated.
          *
          * @param parentId The media id of the parent media item.
-         * @param children The children which were loaded, or null if the id is invalid.
+         * @param children The children which were loaded.
          */
-        public void onChildrenLoaded(@NonNull String parentId, List<MediaItem> children) {
+        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
         }
 
         /**
          * Called when the list of children is loaded or updated.
          *
          * @param parentId The media id of the parent media item.
-         * @param children The children which were loaded, or null if the id is invalid.
+         * @param children The children which were loaded.
          * @param options A bundle of service-specific arguments sent to the media
          *            browse service. The contents of this bundle may affect the
          *            information returned when browsing.
          */
-        public void onChildrenLoaded(@NonNull String parentId, List<MediaItem> children,
+        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
                 @NonNull Bundle options) {
         }
 
diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java
index 480acd9..b5ea2a0 100644
--- a/media/java/android/service/media/MediaBrowserService.java
+++ b/media/java/android/service/media/MediaBrowserService.java
@@ -45,6 +45,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 
@@ -120,8 +121,8 @@
      * they are done. If more than one of those methods is called, an exception will
      * be thrown.
      *
-     * @see MediaBrowserService#onLoadChildren
-     * @see MediaBrowserService#onLoadItem
+     * @see #onLoadChildren
+     * @see #onLoadItem
      */
     public class Result<T> {
         private Object mDebug;
@@ -367,10 +368,16 @@
      * {@link Result#detach result.detach} may be called before returning from
      * this function, and then {@link Result#sendResult result.sendResult}
      * called when the loading is complete.
+     * </p><p>
+     * In case the media item does not have any children, call {@link Result#sendResult}
+     * with an empty list which is not {@code null}. If {@code null} is sent that means
+     * the given {@code parentId} is invalid and {@link MediaBrowser.SubscriptionCallback#onError}
+     * will be called.
+     * </p>
      *
      * @param parentId The id of the parent media item whose children are to be
      *            queried.
-     * @param result The Result to send the list of children to, or null if the
+     * @param result The Result to send the list of children to. Send null if the
      *            id is invalid.
      */
     public abstract void onLoadChildren(@NonNull String parentId,
@@ -385,10 +392,16 @@
      * {@link Result#detach result.detach} may be called before returning from
      * this function, and then {@link Result#sendResult result.sendResult}
      * called when the loading is complete.
+     * </p><p>
+     * In case the media item does not have any children, call {@link Result#sendResult}
+     * with an empty list which is not {@code null}. If {@code null} is sent that means
+     * the given {@code parentId} is invalid and {@link MediaBrowser.SubscriptionCallback#onError}
+     * will be called.
+     * </p>
      *
      * @param parentId The id of the parent media item whose children are to be
      *            queried.
-     * @param result The Result to send the list of children to, or null if the
+     * @param result The Result to send the list of children to. Send null if the
      *            id is invalid.
      * @param options A bundle of service-specific arguments sent from the media
      *            browse. The information returned through the result should be
@@ -416,7 +429,7 @@
      *
      * @param itemId The id for the specific
      *            {@link android.media.browse.MediaBrowser.MediaItem}.
-     * @param result The Result to send the item to, or null if the id is
+     * @param result The Result to send the item to. Send null if the id is
      *            invalid.
      */
     public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
@@ -630,6 +643,9 @@
 
     private List<MediaBrowser.MediaItem> applyOptions(List<MediaBrowser.MediaItem> list,
             final Bundle options) {
+        if (list == null) {
+            return null;
+        }
         int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1);
         int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1);
         if (page == -1 && pageSize == -1) {
@@ -638,7 +654,7 @@
         int fromIndex = pageSize * (page - 1);
         int toIndex = fromIndex + pageSize;
         if (page < 1 || pageSize < 1 || fromIndex >= list.size()) {
-            return null;
+            return Collections.EMPTY_LIST;
         }
         if (toIndex > list.size()) {
             toIndex = list.size();
diff --git a/packages/DocumentsUI/Android.mk b/packages/DocumentsUI/Android.mk
index 6dfc3bb..d5e48b5 100644
--- a/packages/DocumentsUI/Android.mk
+++ b/packages/DocumentsUI/Android.mk
@@ -38,5 +38,4 @@
 LOCAL_CERTIFICATE := platform
 
 include $(BUILD_PACKAGE)
-
-include $(LOCAL_PATH)/tests/Android.mk
+include $(call all-makefiles-under, $(LOCAL_PATH))
diff --git a/packages/DocumentsUI/perf-tests/Android.mk b/packages/DocumentsUI/perf-tests/Android.mk
new file mode 100644
index 0000000..c83094e
--- /dev/null
+++ b/packages/DocumentsUI/perf-tests/Android.mk
@@ -0,0 +1,22 @@
+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) \
+    $(call all-java-files-under, ../tests/src/com/android/documentsui/bots) \
+    ../tests/src/com/android/documentsui/ActivityTest.java \
+    ../tests/src/com/android/documentsui/DocumentsProviderHelper.java \
+    ../tests/src/com/android/documentsui/StubProvider.java
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target ub-uiautomator
+
+LOCAL_PACKAGE_NAME := DocumentsUIPerfTests
+LOCAL_INSTRUMENTATION_FOR := DocumentsUI
+
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
+
diff --git a/packages/DocumentsUI/perf-tests/AndroidManifest.xml b/packages/DocumentsUI/perf-tests/AndroidManifest.xml
new file mode 100644
index 0000000..97353e7
--- /dev/null
+++ b/packages/DocumentsUI/perf-tests/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.documentsui.perftests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <provider
+            android:name="com.android.documentsui.StressProvider"
+            android:authorities="com.android.documentsui.stressprovider"
+            android:exported="true"
+            android:grantUriPermissions="true"
+            android:permission="android.permission.MANAGE_DOCUMENTS"
+            android:enabled="true">
+            <intent-filter>
+                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
+            </intent-filter>
+        </provider>
+    </application>
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="com.android.documentsui"
+        android:label="Performance tests for DocumentsUI" />
+
+</manifest>
diff --git a/packages/DocumentsUI/perf-tests/src/com/android/documentsui/FilesActivityPerfTest.java b/packages/DocumentsUI/perf-tests/src/com/android/documentsui/FilesActivityPerfTest.java
new file mode 100644
index 0000000..8c39aac
--- /dev/null
+++ b/packages/DocumentsUI/perf-tests/src/com/android/documentsui/FilesActivityPerfTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2016 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.documentsui;
+
+import static com.android.documentsui.StressProvider.DEFAULT_AUTHORITY;
+import static com.android.documentsui.StressProvider.STRESS_ROOT_0_ID;
+import static com.android.documentsui.StressProvider.STRESS_ROOT_1_ID;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import com.android.documentsui.model.RootInfo;
+import com.android.documentsui.EventListener;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+
+@LargeTest
+public class FilesActivityPerfTest extends ActivityTest<FilesActivity> {
+
+    // Constants starting with KEY_ are used to report metrics to APCT.
+    private static final String KEY_FILES_LISTED_PERFORMANCE_FIRST =
+            "files-listed-performance-first";
+
+    private static final String KEY_FILES_LISTED_PERFORMANCE_MEDIAN =
+            "files-listed-performance-median";
+
+    private static final String TESTED_URI =
+            "content://com.android.documentsui.stressprovider/document/STRESS_ROOT_1_DOC";
+
+    private static final int NUM_MEASUREMENTS = 10;
+
+    public FilesActivityPerfTest() {
+        super(FilesActivity.class);
+    }
+
+    @Override
+    protected RootInfo getInitialRoot() {
+        return rootDir0;
+    }
+
+    @Override
+    protected String getTestingProviderAuthority() {
+        return DEFAULT_AUTHORITY;
+    }
+
+    @Override
+    protected void setupTestingRoots() throws RemoteException {
+        rootDir0 = mDocsHelper.getRoot(STRESS_ROOT_0_ID);
+        rootDir1 = mDocsHelper.getRoot(STRESS_ROOT_1_ID);
+    }
+
+    @Override
+    public void initTestFiles() throws RemoteException {
+        // Nothing to create, already done by StressProvider.
+    }
+
+    public void testFilesListedPerformance() throws Exception {
+        final BaseActivity activity = getActivity();
+
+        final List<Long> measurements = new ArrayList<Long>();
+        CountDownLatch signal;
+        EventListener listener;
+        for (int i = 0; i < 10; i++) {
+            signal = new CountDownLatch(1);
+            listener = new EventListener() {
+                @Override
+                public void onDirectoryNavigated(Uri uri) {
+                    if (uri != null && TESTED_URI.equals(uri.toString())) {
+                        mStartTime = System.currentTimeMillis();
+                    } else {
+                        mStartTime = -1;
+                    }
+                }
+
+                @Override
+                public void onDirectoryLoaded(Uri uri) {
+                    if (uri == null || !TESTED_URI.equals(uri.toString())) {
+                        return;
+                    }
+                    assertTrue(mStartTime != -1);
+                    getInstrumentation().waitForIdle(new Runnable() {
+                        @Override
+                        public void run() {
+                            assertTrue(mStartTime != -1);
+                            measurements.add(System.currentTimeMillis() - mStartTime);
+                            signal.countDown();
+                        }
+                    });
+                }
+
+                private long mStartTime = -1;
+            };
+
+            try {
+                activity.addEventListener(listener);
+                bots.roots.openRoot(STRESS_ROOT_1_ID);
+                signal.await();
+            } finally {
+                activity.removeEventListener(listener);
+            }
+
+            assertEquals(i, measurements.size());
+
+            // Go back to the empty root.
+            bots.roots.openRoot(STRESS_ROOT_0_ID);
+        }
+
+        assertEquals(NUM_MEASUREMENTS, measurements.size());
+
+        final Bundle status = new Bundle();
+        status.putDouble(KEY_FILES_LISTED_PERFORMANCE_FIRST, measurements.get(0));
+
+        final Long[] rawMeasurements = measurements.toArray(new Long[NUM_MEASUREMENTS]);
+        Arrays.sort(rawMeasurements);
+
+        final long median = rawMeasurements[NUM_MEASUREMENTS / 2 - 1];
+        status.putDouble(KEY_FILES_LISTED_PERFORMANCE_MEDIAN, median);
+
+        getInstrumentation().sendStatus(Activity.RESULT_OK, status);
+    }
+}
diff --git a/packages/DocumentsUI/perf-tests/src/com/android/documentsui/StressProvider.java b/packages/DocumentsUI/perf-tests/src/com/android/documentsui/StressProvider.java
new file mode 100644
index 0000000..1bc802a
--- /dev/null
+++ b/packages/DocumentsUI/perf-tests/src/com/android/documentsui/StressProvider.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2016 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.documentsui;
+
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.MatrixCursor.RowBuilder;
+import android.database.MatrixCursor;
+import android.os.CancellationSignal;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsProvider;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Provider with thousands of files for testing loading time of directories in DocumentsUI.
+ * It doesn't support any file operations.
+ */
+public class StressProvider extends DocumentsProvider {
+
+    public static final String DEFAULT_AUTHORITY = "com.android.documentsui.stressprovider";
+
+    // Empty root.
+    public static final String STRESS_ROOT_0_ID = "STRESS_ROOT_0";
+
+    // Root with thousands of items.
+    public static final String STRESS_ROOT_1_ID = "STRESS_ROOT_1";
+
+    private static final String STRESS_ROOT_0_DOC_ID = "STRESS_ROOT_0_DOC";
+    private static final String STRESS_ROOT_1_DOC_ID = "STRESS_ROOT_1_DOC";
+
+    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
+            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
+            Root.COLUMN_AVAILABLE_BYTES
+    };
+    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
+            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
+            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
+    };
+
+    private String mAuthority = DEFAULT_AUTHORITY;
+    private ArrayList<String> mIds = new ArrayList<>();
+
+    @Override
+    public void attachInfo(Context context, ProviderInfo info) {
+        mAuthority = info.authority;
+        super.attachInfo(context, info);
+    }
+
+    @Override
+    public boolean onCreate() {
+        mIds = new ArrayList();
+        for (int i = 0; i < 10000; i++) {
+            mIds.add(createRandomId(i));
+        }
+        mIds.add(STRESS_ROOT_0_DOC_ID);
+        mIds.add(STRESS_ROOT_1_DOC_ID);
+        return true;
+    }
+
+    @Override
+    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(DEFAULT_ROOT_PROJECTION);
+        includeRoot(result, STRESS_ROOT_0_ID, STRESS_ROOT_0_DOC_ID);
+        includeRoot(result, STRESS_ROOT_1_ID, STRESS_ROOT_1_DOC_ID);
+        return result;
+    }
+
+    @Override
+    public Cursor queryDocument(String documentId, String[] projection)
+            throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(DEFAULT_DOCUMENT_PROJECTION);
+        includeDocument(result, documentId);
+        return result;
+    }
+
+    @Override
+    public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
+            throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(DEFAULT_DOCUMENT_PROJECTION);
+        if (STRESS_ROOT_1_DOC_ID.equals(parentDocumentId)) {
+            for (String id : mIds) {
+                includeDocument(result, id);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
+            throws FileNotFoundException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void includeRoot(MatrixCursor result, String rootId, String docId) {
+        final RowBuilder row = result.newRow();
+        row.add(Root.COLUMN_ROOT_ID, rootId);
+        row.add(Root.COLUMN_FLAGS, 0);
+        row.add(Root.COLUMN_TITLE, rootId);
+        row.add(Root.COLUMN_DOCUMENT_ID, docId);
+    }
+
+    private void includeDocument(MatrixCursor result, String id) {
+        final RowBuilder row = result.newRow();
+        row.add(Document.COLUMN_DOCUMENT_ID, id);
+        row.add(Document.COLUMN_DISPLAY_NAME, id);
+        row.add(Document.COLUMN_SIZE, 0);
+        row.add(Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR);
+        row.add(Document.COLUMN_FLAGS, 0);
+        row.add(Document.COLUMN_LAST_MODIFIED, null);
+    }
+
+    private static String getDocumentIdForFile(File file) {
+        return file.getAbsolutePath();
+    }
+
+    private String createRandomId(int index) {
+        final Random random = new Random(index);
+        final StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < 20; i++) {
+            builder.append((char) (random.nextInt(96) + 32));
+        }
+        builder.append(index);  // Append a number to guarantee uniqueness.
+        return builder.toString();
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
index 46cbbdf..699700b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -39,6 +39,7 @@
 import android.support.annotation.CallSuper;
 import android.support.annotation.LayoutRes;
 import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.Menu;
@@ -67,6 +68,7 @@
     SearchViewManager mSearchManager;
     DrawerController mDrawer;
     NavigationView mNavigator;
+    List<EventListener> mEventListeners = new ArrayList<>();
 
     private final String mTag;
 
@@ -329,6 +331,8 @@
     void openContainerDocument(DocumentInfo doc) {
         assert(doc.isContainer());
 
+        notifyDirectoryNavigated(doc.derivedUri);
+
         mState.pushDocument(doc);
         // Show an opening animation only if pressing "back" would get us back to the
         // previous directory. Especially after opening a root document, pressing
@@ -594,6 +598,28 @@
         return super.onKeyDown(keyCode, event);
     }
 
+    @VisibleForTesting
+    public void addEventListener(EventListener listener) {
+        mEventListeners.add(listener);
+    }
+
+    @VisibleForTesting
+    public void removeEventListener(EventListener listener) {
+        mEventListeners.remove(listener);
+    }
+
+    public void notifyDirectoryLoaded(Uri uri) {
+        for (EventListener listener : mEventListeners) {
+            listener.onDirectoryLoaded(uri);
+        }
+    }
+
+    void notifyDirectoryNavigated(Uri uri) {
+        for (EventListener listener : mEventListeners) {
+            listener.onDirectoryNavigated(uri);
+        }
+    }
+
     /**
      * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't
      * locked, open/close it as appropriate.
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
index 13b7b14..d2e918c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
@@ -59,7 +59,6 @@
     private CancellationSignal mSignal;
     private DirectoryResult mResult;
 
-
     public DirectoryLoader(Context context, int type, RootInfo root, DocumentInfo doc, Uri uri,
             int userSortOrder, boolean inSearchMode) {
         super(context, ProviderExecutor.forAuthority(root.authority));
@@ -84,6 +83,7 @@
         final String authority = mUri.getAuthority();
 
         final DirectoryResult result = new DirectoryResult();
+        result.doc = mDoc;
 
         // Use default document when searching
         if (mSearchMode) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryResult.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryResult.java
index 22e438a..6268643 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryResult.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryResult.java
@@ -22,12 +22,15 @@
 import android.content.ContentProviderClient;
 import android.database.Cursor;
 
+import com.android.documentsui.model.DocumentInfo;
+
 import libcore.io.IoUtils;
 
 public class DirectoryResult implements AutoCloseable {
     ContentProviderClient client;
     public Cursor cursor;
     public Exception exception;
+    public DocumentInfo doc;
 
     public int sortOrder = SORT_ORDER_UNKNOWN;
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/EventListener.java b/packages/DocumentsUI/src/com/android/documentsui/EventListener.java
new file mode 100644
index 0000000..c15e9a6
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/EventListener.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 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.documentsui;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+
+public interface EventListener {
+    /**
+     * @param uri Uri navigated to. If recents, then null.
+     */
+    void onDirectoryNavigated(@Nullable Uri uri);
+
+    /**
+     * @param uri Uri of the loaded directory. If recents, then null.
+     */
+    void onDirectoryLoaded(@Nullable Uri uri);
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index dfceff8..1348a58 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -1290,6 +1290,11 @@
                 showDirectory();
                 mAdapter.notifyDataSetChanged();
             }
+
+            if (!model.isLoading()) {
+                ((BaseActivity) getActivity()).notifyDirectoryLoaded(
+                    model.doc != null ? model.doc.derivedUri : null);
+            }
         }
 
         @Override
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
index 5e55e1a..8170e2a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
@@ -64,6 +64,7 @@
 
     @Nullable String info;
     @Nullable String error;
+    @Nullable DocumentInfo doc;
 
     /**
      * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
@@ -111,6 +112,7 @@
             mPositions.clear();
             info = null;
             error = null;
+            doc = null;
             mIsLoading = false;
             notifyUpdateListeners();
             return;
@@ -125,6 +127,7 @@
         mCursor = result.cursor;
         mCursorCount = mCursor.getCount();
         mSortOrder = result.sortOrder;
+        doc = result.doc;
 
         updateModelData();
 
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/ActivityTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/ActivityTest.java
index 4b98aaf..683fd6c 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/ActivityTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/ActivityTest.java
@@ -32,8 +32,11 @@
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObjectNotFoundException;
 import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
 import android.view.MotionEvent;
 
+import com.android.documentsui.BaseActivity;
+import com.android.documentsui.EventListener;
 import com.android.documentsui.bots.DirectoryListBot;
 import com.android.documentsui.bots.KeyboardBot;
 import com.android.documentsui.bots.RootsListBot;
@@ -64,7 +67,6 @@
 
     public RootInfo rootDir0;
     public RootInfo rootDir1;
-
     ContentResolver mResolver;
     DocumentsProviderHelper mDocsHelper;
     ContentProviderClient mClient;
@@ -84,6 +86,23 @@
         return rootDir0;
     }
 
+    /**
+     * Returns the authority of the testing provider begin used.
+     * By default it's StubProvider's authority.
+     * @return Authority of the provider.
+     */
+    protected String getTestingProviderAuthority() {
+        return DEFAULT_AUTHORITY;
+    }
+
+    /**
+     * Resolves testing roots.
+     */
+    protected void setupTestingRoots() throws RemoteException {
+        rootDir0 = mDocsHelper.getRoot(ROOT_0_ID);
+        rootDir1 = mDocsHelper.getRoot(ROOT_1_ID);
+    }
+
     @Override
     public void setUp() throws Exception {
         device = UiDevice.getInstance(getInstrumentation());
@@ -95,11 +114,10 @@
         Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_MOUSE);
 
         mResolver = context.getContentResolver();
-        mClient = mResolver.acquireUnstableContentProviderClient(DEFAULT_AUTHORITY);
-        mDocsHelper = new DocumentsProviderHelper(DEFAULT_AUTHORITY, mClient);
+        mClient = mResolver.acquireUnstableContentProviderClient(getTestingProviderAuthority());
+        mDocsHelper = new DocumentsProviderHelper(getTestingProviderAuthority(), mClient);
 
-        rootDir0 = mDocsHelper.getRoot(ROOT_0_ID);
-        rootDir1 = mDocsHelper.getRoot(ROOT_1_ID);
+        setupTestingRoots();
 
         launchActivity();
         resetStorage();