Move sort headers out of directory layout.

This allowed some simplification of the sort component lifecycle.
Also move progress bar.
Also, don't reset model when we get a loader-reset when last results
reported "loading". Fixes a bug where progress bar was being hidden too early.
Finally do a little pixel tweaking on sort headers and breadcrumb.

Change-Id: I49a2c70443edacfe911f692054e289bf1d5f6ecb
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 6543dee..2c8c127 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -111,6 +111,7 @@
     DrawerController mDrawer;
     NavigationViewManager mNavigator;
     List<EventListener> mEventListeners = new ArrayList<>();
+    SortController mSortController;
 
     private final String mTag;
     private final ContentObserver mRootsCacheObserver = new ContentObserver(new Handler()) {
@@ -126,7 +127,6 @@
     private boolean mNavDrawerHasFocus;
     private long mStartTime;
 
-    private SortController mSortController;
 
     public abstract void onDocumentPicked(DocumentInfo doc, Model model);
     public abstract void onDocumentsPicked(List<DocumentInfo> docs);
@@ -183,7 +183,8 @@
 
         mNavigator = new NavigationViewManager(mDrawer, toolbar, mState, this, breadcrumb);
 
-        mSortController = new SortController(mState.sortModel, this);
+        mSortController = SortController.create(this, mState.derivedMode, mState.sortModel);
+
 
         // Base classes must update result in their onCreate.
         setResult(Activity.RESULT_CANCELED);
@@ -215,10 +216,6 @@
         super.onDestroy();
     }
 
-    SortController getSortController() {
-        return mSortController;
-    }
-
     private State getState(@Nullable Bundle icicle) {
         if (icicle != null) {
             State state = icicle.<State>getParcelable(Shared.EXTRA_STATE);
diff --git a/src/com/android/documentsui/DocumentsActivity.java b/src/com/android/documentsui/DocumentsActivity.java
index 017f7ae..8f9c787 100644
--- a/src/com/android/documentsui/DocumentsActivity.java
+++ b/src/com/android/documentsui/DocumentsActivity.java
@@ -406,7 +406,7 @@
     public FragmentTuner createFragmentTuner() {
         // Currently DocumentsTuner maintains a state specific to the fragment instance. Because of
         // that, we create a new instance everytime it is needed
-        return new DocumentsTuner(this, getDisplayState(), getSortController());
+        return new DocumentsTuner(this, getDisplayState(), mSortController);
     }
 
     @Override
diff --git a/src/com/android/documentsui/FilesActivity.java b/src/com/android/documentsui/FilesActivity.java
index 1fefcb5..46a663c 100644
--- a/src/com/android/documentsui/FilesActivity.java
+++ b/src/com/android/documentsui/FilesActivity.java
@@ -483,7 +483,7 @@
 
     @Override
     public FragmentTuner createFragmentTuner() {
-        return new FilesTuner(this, getDisplayState(), getSortController());
+        return new FilesTuner(this, getDisplayState(), mSortController);
     }
 
     @Override
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index c4b4711..0022634 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -194,8 +194,6 @@
     private DragScrollListener mOnDragListener;
     private MenuManager mMenuManager;
 
-    private TableHeaderController mTableHeaderController;
-    private DropdownSortWidgetController mDropdownSortWidgetController;
     private SortModel.UpdateListener mSortListener = (model, updateType) -> {
         // Only when sort order has changed do we need to trigger another loading.
         if ((updateType & SortModel.UPDATE_TYPE_SORTING) != 0) {
@@ -210,7 +208,8 @@
         final View view = inflater.inflate(R.layout.fragment_directory, container, false);
 
         mMessageBar = MessageBar.create(getChildFragmentManager());
-        mProgressBar = view.findViewById(R.id.progressbar);
+        mProgressBar = getActivity().findViewById(R.id.progressbar);
+        assert(mProgressBar != null);
 
         mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
         mRefreshLayout.setOnRefreshListener(this);
@@ -235,10 +234,6 @@
         mRecView.setOnDragListener(mOnDragListener);
         mEmptyView.setOnDragListener(mOnDragListener);
 
-        mTableHeaderController = TableHeaderController.create(view.findViewById(R.id.table_header));
-        mDropdownSortWidgetController =
-                DropdownSortWidgetController.create(view.findViewById(R.id.dropdown_sort_widget));
-
         return view;
     }
 
@@ -386,10 +381,6 @@
     public void onStart() {
         super.onStart();
 
-        mTuner.mSortController.manage(
-                mTableHeaderController,
-                mDropdownSortWidgetController,
-                getDisplayState().derivedMode);
         // Add listener to update contents on sort model change
         getDisplayState().sortModel.addListener(mSortListener);
     }
@@ -398,10 +389,6 @@
     public void onStop() {
         super.onStop();
 
-        // Remove listener to avoid leak.
-        mTuner.mSortController.clean(
-                mTableHeaderController,
-                mDropdownSortWidgetController);
         getDisplayState().sortModel.removeListener(mSortListener);
 
         // Remember last scroll location
@@ -690,15 +677,14 @@
         public void onSelectionChanged() {
             mSelectionMgr.getSelection(mSelected);
             if (mSelected.size() > 0) {
-                if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
-                if (mActionMode == null) {
-                    if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
+                 if (mActionMode == null) {
+                    if (DEBUG) Log.d(TAG, "Starting action mode.");
                     mActionMode = getActivity().startActionMode(this);
                 }
                 updateActionMenu();
             } else {
-                if (DEBUG) Log.d(TAG, "Finishing action mode.");
                 if (mActionMode != null) {
+                    if (DEBUG) Log.d(TAG, "Finishing action mode.");
                     mActionMode.finish();
                 }
             }
@@ -1512,6 +1498,8 @@
     private final class ModelUpdateListener implements Model.UpdateListener {
         @Override
         public void onModelUpdate(Model model) {
+            if (DEBUG) Log.d(TAG, "Received model update. Loading=" + model.isLoading());
+
             if (model.info != null || model.error != null) {
                 mMessageBar.setInfo(model.info);
                 mMessageBar.setError(model.error);
@@ -1637,6 +1625,7 @@
 
     public static void showDirectory(
             FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
+        if (DEBUG) Log.d(TAG, "Showing directory: " + doc.derivedUri);
         create(fm, TYPE_NORMAL, root, doc, null, anim);
     }
 
@@ -1657,6 +1646,7 @@
 
     public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
             String query) {
+        if (DEBUG) Log.d(TAG, "Reloading directory: " + doc.derivedUri);
         DirectoryFragment df = get(fm);
         df.mType = type;
         df.mQuery = query;
@@ -1668,6 +1658,7 @@
 
     public static void create(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
             String query, int anim) {
+        if (DEBUG) Log.d(TAG, "Creating new fragment for directory: " + doc.derivedUri);
         final Bundle args = new Bundle();
         args.putInt(Shared.EXTRA_TYPE, type);
         args.putParcelable(Shared.EXTRA_ROOT, root);
@@ -1723,6 +1714,7 @@
 
     @Override
     public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
+        if (DEBUG) Log.d(TAG, "Creating new loader for: " + mDocument.derivedUri);
         Context context = getActivity();
         State state = getDisplayState();
 
@@ -1736,10 +1728,12 @@
                 if (mTuner.managedModeEnabled()) {
                     contentsUri = DocumentsContract.setManageMode(contentsUri);
                 }
+                if (DEBUG) Log.d(TAG, "Creating new loader for: " + mDocument.derivedUri);
                 return new DirectoryLoader(
                         context, mType, mRoot, mDocument, contentsUri, state.sortModel,
                         mSearchMode);
             case TYPE_RECENT_OPEN:
+                if (DEBUG) Log.d(TAG, "Creating new loader recents.");
                 final RootsCache roots = DocumentsApplication.getRootsCache(context);
                 return new RecentsLoader(context, roots, state);
 
@@ -1750,6 +1744,9 @@
 
     @Override
     public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
+        if (DEBUG) Log.d(TAG, "Loader has finished for: " + mDocument.derivedUri);
+        assert(result != null);
+
         if (!isAdded()) return;
 
         if (mSearchMode) {
@@ -1798,7 +1795,8 @@
 
     @Override
     public void onLoaderReset(Loader<DirectoryResult> loader) {
-        mModel.update(null);
+        if (DEBUG) Log.d(TAG, "Resetting loader for: " + mDocument.derivedUri);
+        mModel.onLoaderReset();
 
         mRefreshLayout.setRefreshing(false);
     }
diff --git a/src/com/android/documentsui/dirlist/DropdownSortWidgetController.java b/src/com/android/documentsui/dirlist/DropdownSortWidgetController.java
index 0a5b310..1ed42c6 100644
--- a/src/com/android/documentsui/dirlist/DropdownSortWidgetController.java
+++ b/src/com/android/documentsui/dirlist/DropdownSortWidgetController.java
@@ -16,7 +16,6 @@
 
 package com.android.documentsui.dirlist;
 
-import android.annotation.Nullable;
 import android.annotation.StringRes;
 import android.view.Gravity;
 import android.view.Menu;
@@ -44,8 +43,7 @@
     private static final int LEVEL_UPWARD = 0;
     private static final int LEVEL_DOWNWARD = 10000;
 
-    private SortModel mModel;
-
+    private final SortModel mModel;
     private final View mWidget;
     private final TextView mDimensionButton;
     private final PopupMenu mMenu;
@@ -55,7 +53,8 @@
     private final OnClickListener mArrowClickListener = this::onChangeDirection;
     private final UpdateListener mUpdateListener = this::onModelUpdate;
 
-    private DropdownSortWidgetController(View widget) {
+    public DropdownSortWidgetController(SortModel model, View widget) {
+        mModel = model;
         mWidget = widget;
 
         mDimensionButton = (TextView) mWidget.findViewById(R.id.sort_dimen_dropdown);
@@ -63,26 +62,11 @@
         mMenu.setOnMenuItemClickListener(this::onSelectDimension);
 
         mArrow = (ImageView) mWidget.findViewById(R.id.sort_arrow);
-    }
 
-    static @Nullable DropdownSortWidgetController create(@Nullable View widget) {
-        return widget == null ? null : new DropdownSortWidgetController(widget);
-    }
+        populateMenuItems();
+        onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED);
 
-    @Override
-    public void setModel(SortModel model) {
-        if (mModel != null) {
-            mModel.removeListener(mUpdateListener);
-        }
-
-        mModel = model;
-
-        if (mModel != null) {
-            populateMenuItems();
-            onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED);
-
-            mModel.addListener(mUpdateListener);
-        }
+        mModel.addListener(mUpdateListener);
     }
 
     @Override
diff --git a/src/com/android/documentsui/dirlist/FragmentTuner.java b/src/com/android/documentsui/dirlist/FragmentTuner.java
index 8dfd490..c4f37eb 100644
--- a/src/com/android/documentsui/dirlist/FragmentTuner.java
+++ b/src/com/android/documentsui/dirlist/FragmentTuner.java
@@ -42,6 +42,10 @@
     final SortController mSortController;
 
     public FragmentTuner(Context context, State state, SortController sortController) {
+        assert(context != null);
+        assert(state != null);
+        assert(sortController != null);
+
         mContext = context;
         mState = state;
         mSortController = sortController;
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 1a171bd..5ddad31 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -79,21 +79,28 @@
         }
     }
 
-    void update(DirectoryResult result) {
-        if (DEBUG) Log.i(TAG, "Updating model with new result set.");
-
-        if (result == null) {
-            mCursor = null;
-            mCursorCount = 0;
-            mIds = new String[0];
-            mPositions.clear();
-            info = null;
-            error = null;
-            doc = null;
-            mIsLoading = false;
-            notifyUpdateListeners();
-            return;
+    void onLoaderReset() {
+        if (mIsLoading) {
+            if (DEBUG) Log.w(TAG, "Received unexpected loader reset while in loading state.");
         }
+    }
+
+    private void reset() {
+        mCursor = null;
+        mCursorCount = 0;
+        mIds = new String[0];
+        mPositions.clear();
+        info = null;
+        error = null;
+        doc = null;
+        mIsLoading = false;
+        notifyUpdateListeners();
+    }
+
+    void update(DirectoryResult result) {
+        assert(result != null);
+
+        if (DEBUG) Log.i(TAG, "Updating model with new result set.");
 
         if (result.exception != null) {
             Log.e(TAG, "Error while loading directory contents", result.exception);
diff --git a/src/com/android/documentsui/dirlist/header/TableHeaderController.java b/src/com/android/documentsui/dirlist/header/TableHeaderController.java
index a48f963..458c527 100644
--- a/src/com/android/documentsui/dirlist/header/TableHeaderController.java
+++ b/src/com/android/documentsui/dirlist/header/TableHeaderController.java
@@ -16,15 +16,15 @@
 
 package com.android.documentsui.dirlist.header;
 
-import android.annotation.Nullable;
 import android.view.View;
 
 import com.android.documentsui.R;
+import com.android.documentsui.sorting.SortController;
 import com.android.documentsui.sorting.SortDimension;
 import com.android.documentsui.sorting.SortModel;
-import com.android.documentsui.sorting.SortModel.UpdateType;
 import com.android.documentsui.sorting.SortModel.SortDimensionId;
-import com.android.documentsui.sorting.SortController;
+
+import javax.annotation.Nullable;
 
 /**
  * View controller for table header that associates header cells in table header and columns.
@@ -37,37 +37,39 @@
     private final HeaderCell mSizeCell;
     private final HeaderCell mDateCell;
 
-    private final SortModel.UpdateListener mModelUpdaterListener = this::onModelUpdate;
+    // We assign this here porque each method reference creates a new object
+    // instance (which is wasteful).
     private final View.OnClickListener mOnCellClickListener = this::onCellClicked;
 
-    private SortModel mModel;
+    private final SortModel mModel;
 
-    public static @Nullable TableHeaderController create(@Nullable View tableHeader) {
-        return (tableHeader == null) ? null : new TableHeaderController(tableHeader);
-    }
+    private TableHeaderController(SortModel sortModel, View tableHeader) {
+        assert(sortModel != null);
+        assert(tableHeader != null);
 
-    private TableHeaderController(View tableHeader) {
+        mModel = sortModel;
         mTableHeader = tableHeader;
 
         mTitleCell = (HeaderCell) tableHeader.findViewById(android.R.id.title);
         mSummaryCell = (HeaderCell) tableHeader.findViewById(android.R.id.summary);
         mSizeCell = (HeaderCell) tableHeader.findViewById(R.id.size);
         mDateCell = (HeaderCell) tableHeader.findViewById(R.id.date);
+
+        onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED);
+
+        mModel.addListener(this::onModelUpdate);
     }
 
-    @Override
-    public void setModel(@Nullable SortModel model) {
-        if (mModel != null) {
-            mModel.removeListener(mModelUpdaterListener);
-        }
+    private void onModelUpdate(SortModel model, int updateTypeUnspecified) {
+        bindCell(mTitleCell, SortModel.SORT_DIMENSION_ID_TITLE);
+        bindCell(mSummaryCell, SortModel.SORT_DIMENSION_ID_SUMMARY);
+        bindCell(mSizeCell, SortModel.SORT_DIMENSION_ID_SIZE);
+        bindCell(mDateCell, SortModel.SORT_DIMENSION_ID_DATE);
+    }
 
-        mModel = model;
-
-        if (mModel != null) {
-            onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED);
-
-            mModel.addListener(mModelUpdaterListener);
-        }
+    public static @Nullable TableHeaderController create(
+            SortModel sortModel, @Nullable View tableHeader) {
+        return (tableHeader == null) ? null : new TableHeaderController(sortModel, tableHeader);
     }
 
     @Override
@@ -75,14 +77,8 @@
         mTableHeader.setVisibility(visibility);
     }
 
-    private void onModelUpdate(SortModel model, @UpdateType int updateType) {
-        bindCell(mTitleCell, SortModel.SORT_DIMENSION_ID_TITLE);
-        bindCell(mSummaryCell, SortModel.SORT_DIMENSION_ID_SUMMARY);
-        bindCell(mSizeCell, SortModel.SORT_DIMENSION_ID_SIZE);
-        bindCell(mDateCell, SortModel.SORT_DIMENSION_ID_DATE);
-    }
-
     private void bindCell(HeaderCell cell, @SortDimensionId int id) {
+        assert(cell != null);
         SortDimension dimension = mModel.getDimensionById(id);
 
         cell.setTag(dimension);
diff --git a/src/com/android/documentsui/sorting/SortController.java b/src/com/android/documentsui/sorting/SortController.java
index b9b3140..4b1aa45 100644
--- a/src/com/android/documentsui/sorting/SortController.java
+++ b/src/com/android/documentsui/sorting/SortController.java
@@ -17,10 +17,11 @@
 package com.android.documentsui.sorting;
 
 import android.annotation.Nullable;
-import android.content.Context;
+import android.app.Activity;
 import android.view.View;
 
 import com.android.documentsui.Metrics;
+import com.android.documentsui.R;
 import com.android.documentsui.State;
 import com.android.documentsui.State.ViewMode;
 import com.android.documentsui.dirlist.DropdownSortWidgetController;
@@ -31,103 +32,73 @@
  * appear in different locations in the UI, like the menu, above the file list (pinned) and embedded
  * at the top of file list... and maybe other places too.
  */
-public class SortController {
+public final class SortController {
 
-    private static final WidgetController DUMMY_CONTROLLER = new WidgetController() {};
+    private final WidgetController mDropdownController;
+    private final @Nullable WidgetController mTableHeaderController;
 
-    private final SortModel mModel;
-    private final Context mContext;
+    public SortController(
+            WidgetController dropdownController,
+            @Nullable WidgetController tableHeaderController) {
 
-    private WidgetController mTableHeaderController = DUMMY_CONTROLLER;
-    private WidgetController mDropdownController = DUMMY_CONTROLLER;
-
-    public SortController(SortModel model, Context context) {
-        mModel = model;
-        mContext = context.getApplicationContext();
-
-        mModel.setMetricRecorder(this::recordSortMetric);
-    }
-
-    public void manage(
-            @Nullable TableHeaderController headerController,
-            @Nullable DropdownSortWidgetController gridController,
-            @ViewMode int mode) {
-        assert(mTableHeaderController == DUMMY_CONTROLLER);
-        assert(mDropdownController == DUMMY_CONTROLLER);
-
-        if (headerController != null) {
-            mTableHeaderController = headerController;
-            mTableHeaderController.setModel(mModel);
-        }
-
-        if (gridController != null) {
-            mDropdownController = gridController;
-            mDropdownController.setModel(mModel);
-        }
-
-        onViewModeChanged(mode);
-    }
-
-    public void clean(
-            @Nullable TableHeaderController headerController,
-            @Nullable DropdownSortWidgetController gridController) {
-        assert(headerController == null || mTableHeaderController == headerController);
-        assert(gridController == null || mDropdownController == gridController);
-
-        if (headerController != null) {
-            headerController.setModel(null);
-        }
-        mTableHeaderController = DUMMY_CONTROLLER;
-
-        if (gridController != null) {
-            gridController.setModel(null);
-        }
-        mDropdownController = DUMMY_CONTROLLER;
+        assert(dropdownController != null);
+        mDropdownController = dropdownController;
+        mTableHeaderController = tableHeaderController;
     }
 
     public void onViewModeChanged(@ViewMode int mode) {
-        setVisibilityPerViewMode(mTableHeaderController, mode, View.GONE, View.VISIBLE);
-
-        if (mTableHeaderController == DUMMY_CONTROLLER) {
+        // in phone layouts we only ever have the dropdown sort controller.
+        if (mTableHeaderController == null) {
             mDropdownController.setVisibility(View.VISIBLE);
-        } else {
-            setVisibilityPerViewMode(mDropdownController, mode, View.VISIBLE, View.GONE);
+            return;
         }
-    }
 
-    private static void setVisibilityPerViewMode(
-            WidgetController controller,
-            @ViewMode int mode,
-            int visibilityInGrid,
-            int visibilityInList) {
+        // in tablet mode, we have fancy pants tabular header.
         switch (mode) {
             case State.MODE_GRID:
-                controller.setVisibility(visibilityInGrid);
+            case State.MODE_UNKNOWN:
+                mTableHeaderController.setVisibility(View.GONE);
+                mDropdownController.setVisibility(View.VISIBLE);
                 break;
             case State.MODE_LIST:
-                controller.setVisibility(visibilityInList);
+                mTableHeaderController.setVisibility(View.VISIBLE);
+                mDropdownController.setVisibility(View.GONE);
                 break;
-            default:
-                throw new IllegalArgumentException("Unknown view mode: " + mode + ".");
         }
     }
 
-    private void recordSortMetric(SortDimension dimension) {
-        switch (dimension.getId()) {
-            case SortModel.SORT_DIMENSION_ID_TITLE:
-                Metrics.logUserAction(mContext, Metrics.USER_ACTION_SORT_NAME);
-                break;
-            case SortModel.SORT_DIMENSION_ID_SIZE:
-                Metrics.logUserAction(mContext, Metrics.USER_ACTION_SORT_SIZE);
-                break;
-            case SortModel.SORT_DIMENSION_ID_DATE:
-                Metrics.logUserAction(mContext, Metrics.USER_ACTION_SORT_DATE);
-                break;
-        }
+    public static SortController create(
+            Activity activity,
+            @ViewMode int initialMode,
+            SortModel sortModel) {
+
+        sortModel.setMetricRecorder((SortDimension dimension) -> {
+            switch (dimension.getId()) {
+                case SortModel.SORT_DIMENSION_ID_TITLE:
+                    Metrics.logUserAction(activity, Metrics.USER_ACTION_SORT_NAME);
+                    break;
+                case SortModel.SORT_DIMENSION_ID_SIZE:
+                    Metrics.logUserAction(activity, Metrics.USER_ACTION_SORT_SIZE);
+                    break;
+                case SortModel.SORT_DIMENSION_ID_DATE:
+                    Metrics.logUserAction(activity, Metrics.USER_ACTION_SORT_DATE);
+                    break;
+            }
+        });
+
+        SortController controller = new SortController(
+                new DropdownSortWidgetController(
+                        sortModel,
+                        activity.findViewById(R.id.dropdown_sort_widget)),
+                TableHeaderController.create(
+                        sortModel,
+                        activity.findViewById(R.id.table_header)));
+
+        controller.onViewModeChanged(initialMode);
+        return controller;
     }
 
     public interface WidgetController {
-        default void setModel(SortModel model) {}
         default void setVisibility(int visibility) {}
     }
 }