[multi-part] Enable bidrectional sorting
am: 11d23483bf
Change-Id: I6df9c0963cd94586d2f467cf3893e0646eaec98d
diff --git a/res/menu/activity.xml b/res/menu/activity.xml
index ed47bbc..eb28fe1 100644
--- a/res/menu/activity.xml
+++ b/res/menu/activity.xml
@@ -63,15 +63,7 @@
android:icon="@drawable/ic_menu_sortby"
android:showAsAction="ifRoom">
<menu>
- <item
- android:id="@+id/menu_sort_name"
- android:title="@string/sort_name" />
- <item
- android:id="@+id/menu_sort_date"
- android:title="@string/sort_date" />
- <item
- android:id="@+id/menu_sort_size"
- android:title="@string/sort_size" />
+ <!-- A submenu placeholder for SortMenuController to add menu item -->
</menu>
</item>
<item
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 507fd81..8cc864e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -85,21 +85,17 @@
<string name="button_dismiss">Dismiss</string>
<string name="button_retry">Try Again</string>
- <!-- Mode that sorts documents by their display name alphabetically [CHAR LIMIT=24] -->
- <string name="sort_name">By name</string>
- <!-- Mode that sorts documents by their last modified time in descending order; most recent first [CHAR LIMIT=24] -->
- <string name="sort_date">By date modified</string>
- <!-- Mode that sorts documents by their file size in descending order; largest first [CHAR LIMIT=24] -->
- <string name="sort_size">By size</string>
+ <!-- A phrase to indicate which dimension items should be sorted by, such as By Name, By Size etc. [CHAR_LIMIT=24] -->
+ <string name="sort_phrase">By <xliff:g id="dimension" example="Name">%1$s</xliff:g></string>
<!-- Table header for file name [CHAR_LIMIT=24] -->
- <string name="column_name">Name</string>
+ <string name="sort_dimension_name">Name</string>
<!-- Table header for metadata of downloaded files, such as download source and progress. [CHAR_LIMIT=24] -->
- <string name="column_summary">Summary</string>
- <!-- Table header for last modified time. [CHAR_LIMIT=24] -->
- <string name="column_date">Modified</string>
- <!-- Table header for file size. [CHAR_LIMIT=24] -->
- <string name="column_size">Size</string>
+ <string name="sort_dimension_summary">Summary</string>
+ <!-- Table header for last modified time. [CHAR_LIMIT=12] -->
+ <string name="sort_dimension_date">Modified</string>
+ <!-- Table header for file size. [CHAR_LIMIT=12] -->
+ <string name="sort_dimension_size">Size</string>
<!-- content description to describe ascending sorting used with upward arrow in table header. -->
<string name="sort_direction_ascending">Ascending</string>
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index e9b1743..d9da559 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -64,6 +64,7 @@
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperations;
import com.android.documentsui.sorting.SortController;
+import com.android.documentsui.sorting.SortMenuController;
import com.android.documentsui.sorting.SortModel;
import java.io.FileNotFoundException;
@@ -127,6 +128,7 @@
private long mStartTime;
private SortController mSortController;
+ private SortMenuController mSortMenuController;
public abstract void onDocumentPicked(DocumentInfo doc, Model model);
public abstract void onDocumentsPicked(List<DocumentInfo> docs);
@@ -171,7 +173,7 @@
getContentResolver().registerContentObserver(
RootsCache.sNotificationUri, false, mRootsCacheObserver);
- mSearchManager = new SearchViewManager(this, icicle);
+ mSearchManager = new SearchViewManager(this, icicle, mState.sortModel);
DocumentsToolbar toolbar = (DocumentsToolbar) findViewById(R.id.toolbar);
setActionBar(toolbar);
@@ -182,7 +184,9 @@
mNavigator = new NavigationViewManager(mDrawer, toolbar, mState, this, breadcrumb);
- mSortController = new SortController(mState.sortModel);
+ mSortController = new SortController(mState.sortModel, this);
+ mSortMenuController = new SortMenuController(getResources());
+ mSortController.manage(mSortMenuController);
// Base classes must update result in their onCreate.
setResult(Activity.RESULT_CANCELED);
@@ -197,6 +201,8 @@
boolean fullBarSearch = getResources().getBoolean(R.bool.full_bar_search_view);
mSearchManager.install((DocumentsToolbar) findViewById(R.id.toolbar), fullBarSearch);
+ mSortMenuController.install(menu.findItem(R.id.menu_sort));
+
return showMenu;
}
@@ -307,18 +313,6 @@
// SearchViewManager listens for this directly.
return false;
- case R.id.menu_sort_name:
- setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
- return true;
-
- case R.id.menu_sort_date:
- setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
- return true;
-
- case R.id.menu_sort_size:
- setUserSortOrder(State.SORT_ORDER_SIZE);
- return true;
-
case R.id.menu_grid:
setViewMode(State.MODE_GRID);
return true;
@@ -530,29 +524,6 @@
}
/**
- * Set state sort order based on explicit user action.
- */
- void setUserSortOrder(int sortOrder) {
- switch(sortOrder) {
- case State.SORT_ORDER_DISPLAY_NAME:
- Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_NAME);
- break;
- case State.SORT_ORDER_LAST_MODIFIED:
- Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_DATE);
- break;
- case State.SORT_ORDER_SIZE:
- Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_SIZE);
- break;
- }
-
- mState.userSortOrder = sortOrder;
- DirectoryFragment dir = getDirectoryFragment();
- if (dir != null) {
- dir.onSortOrderChanged();
- }
- }
-
- /**
* Set mode based on explicit user action.
*/
void setViewMode(@ViewMode int mode) {
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index d2e918c..995c050 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -16,12 +16,6 @@
package com.android.documentsui;
-import static com.android.documentsui.Shared.DEBUG;
-import static com.android.documentsui.Shared.TAG;
-import static com.android.documentsui.State.SORT_ORDER_DISPLAY_NAME;
-import static com.android.documentsui.State.SORT_ORDER_LAST_MODIFIED;
-import static com.android.documentsui.State.SORT_ORDER_SIZE;
-
import android.content.AsyncTaskLoader;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
@@ -35,9 +29,9 @@
import android.provider.DocumentsContract.Document;
import android.util.Log;
-import com.android.documentsui.dirlist.DirectoryFragment;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;
+import com.android.documentsui.sorting.SortModel;
import libcore.io.IoUtils;
@@ -45,6 +39,8 @@
public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
+ private static final String TAG = "DirectoryLoader";
+
private static final String[] SEARCH_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
@@ -52,7 +48,7 @@
private final int mType;
private final RootInfo mRoot;
private final Uri mUri;
- private final int mUserSortOrder;
+ private final SortModel mModel;
private final boolean mSearchMode;
private DocumentInfo mDoc;
@@ -60,12 +56,12 @@
private DirectoryResult mResult;
public DirectoryLoader(Context context, int type, RootInfo root, DocumentInfo doc, Uri uri,
- int userSortOrder, boolean inSearchMode) {
+ SortModel model, boolean inSearchMode) {
super(context, ProviderExecutor.forAuthority(root.authority));
mType = type;
mRoot = root;
mUri = uri;
- mUserSortOrder = userSortOrder;
+ mModel = model;
mDoc = doc;
mSearchMode = inSearchMode;
}
@@ -84,6 +80,7 @@
final DirectoryResult result = new DirectoryResult();
result.doc = mDoc;
+ result.sortModel = mModel;
// Use default document when searching
if (mSearchMode) {
@@ -98,30 +95,12 @@
}
}
- if (mUserSortOrder != State.SORT_ORDER_UNKNOWN) {
- result.sortOrder = mUserSortOrder;
- } else {
- if ((mDoc.flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0) {
- result.sortOrder = State.SORT_ORDER_LAST_MODIFIED;
- } else {
- result.sortOrder = State.SORT_ORDER_DISPLAY_NAME;
- }
- }
-
- // Search always uses ranking from provider
- if (mSearchMode) {
- result.sortOrder = State.SORT_ORDER_UNKNOWN;
- }
-
- if (DEBUG)
- Log.d(TAG, "userSortOrder=" + mUserSortOrder + ", sortOrder=" + result.sortOrder);
-
ContentProviderClient client = null;
- Cursor cursor = null;
+ Cursor cursor;
try {
client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
cursor = client.query(
- mUri, null, null, null, getQuerySortOrder(result.sortOrder), mSignal);
+ mUri, null, null, null, mModel.getDocumentSortQuery(), mSignal);
if (cursor == null) {
throw new RemoteException("Provider returned null");
}
@@ -211,17 +190,4 @@
getContext().getContentResolver().unregisterContentObserver(mObserver);
}
-
- public static String getQuerySortOrder(int sortOrder) {
- switch (sortOrder) {
- case SORT_ORDER_DISPLAY_NAME:
- return Document.COLUMN_DISPLAY_NAME + " ASC";
- case SORT_ORDER_LAST_MODIFIED:
- return Document.COLUMN_LAST_MODIFIED + " DESC";
- case SORT_ORDER_SIZE:
- return Document.COLUMN_SIZE + " DESC";
- default:
- return null;
- }
- }
}
diff --git a/src/com/android/documentsui/DirectoryResult.java b/src/com/android/documentsui/DirectoryResult.java
index 6268643..f7ccc48 100644
--- a/src/com/android/documentsui/DirectoryResult.java
+++ b/src/com/android/documentsui/DirectoryResult.java
@@ -16,13 +16,11 @@
package com.android.documentsui;
-import static com.android.documentsui.State.MODE_UNKNOWN;
-import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
-
import android.content.ContentProviderClient;
import android.database.Cursor;
import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.sorting.SortModel;
import libcore.io.IoUtils;
@@ -31,8 +29,7 @@
public Cursor cursor;
public Exception exception;
public DocumentInfo doc;
-
- public int sortOrder = SORT_ORDER_UNKNOWN;
+ public SortModel sortModel;
@Override
public void close() {
@@ -40,5 +37,6 @@
ContentProviderClient.releaseQuietly(client);
cursor = null;
client = null;
+ sortModel = null;
}
}
diff --git a/src/com/android/documentsui/MenuManager.java b/src/com/android/documentsui/MenuManager.java
index 0edd18e..3249183 100644
--- a/src/com/android/documentsui/MenuManager.java
+++ b/src/com/android/documentsui/MenuManager.java
@@ -53,9 +53,7 @@
updateFileSize(menu.findItem(R.id.menu_file_size), directoryDetails);
updateModePicker(menu.findItem(
R.id.menu_grid), menu.findItem(R.id.menu_list), directoryDetails);
- updateSort(menu.findItem(R.id.menu_sort),
- menu.findItem(R.id.menu_sort_size),
- directoryDetails);
+ // Sort menu item is managed by SortMenuManager
updateAdvanced(menu.findItem(R.id.menu_advanced), directoryDetails);
Menus.disableHiddenItems(menu);
@@ -115,13 +113,6 @@
? R.string.menu_file_size_hide : R.string.menu_file_size_show);
}
- void updateSort(MenuItem sort, MenuItem sortSize, DirectoryDetails directoryDetails) {
- // Search uses backend ranking; no sorting, recents doesn't support sort.
- sort.setEnabled(!directoryDetails.isInRecents() && !mSearchManager.isSearching());
- sort.setVisible(true);
- sortSize.setVisible(mState.getShowSize()); // Only sort by size when file sizes are visible
- }
-
void updateAdvanced(MenuItem advanced, DirectoryDetails directoryDetails) {
advanced.setVisible(mState.showAdvancedOption);
advanced.setTitle(mState.showAdvancedOption && mState.showAdvanced
diff --git a/src/com/android/documentsui/RecentsLoader.java b/src/com/android/documentsui/RecentsLoader.java
index cebc9b0..0a559ca 100644
--- a/src/com/android/documentsui/RecentsLoader.java
+++ b/src/com/android/documentsui/RecentsLoader.java
@@ -18,7 +18,6 @@
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.Shared.TAG;
-import static com.android.documentsui.State.SORT_ORDER_LAST_MODIFIED;
import android.app.ActivityManager;
import android.content.AsyncTaskLoader;
@@ -37,10 +36,10 @@
import com.android.documentsui.model.RootInfo;
import com.android.internal.annotations.GuardedBy;
-import com.google.common.util.concurrent.AbstractFuture;
-
import libcore.io.IoUtils;
+import com.google.common.util.concurrent.AbstractFuture;
+
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
@@ -82,8 +81,6 @@
@GuardedBy("mTasks")
private final HashMap<RootInfo, RecentsTask> mTasks = new HashMap<>();
- private final int mSortOrder = State.SORT_ORDER_LAST_MODIFIED;
-
private CountDownLatch mFirstPassLatch;
private volatile boolean mFirstPassDone;
@@ -168,7 +165,7 @@
}
final DirectoryResult result = new DirectoryResult();
- result.sortOrder = SORT_ORDER_LAST_MODIFIED;
+ result.sortModel = mState.sortModel;
final Cursor merged;
if (cursors.size() > 0) {
@@ -288,7 +285,7 @@
final Uri uri = DocumentsContract.buildRecentDocumentsUri(authority, rootId);
final Cursor cursor = client.query(
- uri, null, null, null, DirectoryLoader.getQuerySortOrder(mSortOrder));
+ uri, null, null, null, mState.sortModel.getDocumentSortQuery());
mWithRoot = new RootCursorWrapper(authority, rootId, cursor, MAX_DOCS_FROM_ROOT);
} catch (Exception e) {
diff --git a/src/com/android/documentsui/SearchViewManager.java b/src/com/android/documentsui/SearchViewManager.java
index 46a14e6..de2d155 100644
--- a/src/com/android/documentsui/SearchViewManager.java
+++ b/src/com/android/documentsui/SearchViewManager.java
@@ -33,6 +33,7 @@
import android.widget.SearchView.OnQueryTextListener;
import com.android.documentsui.model.RootInfo;
+import com.android.documentsui.sorting.SortModel;
/**
* Manages searching UI behavior.
@@ -58,9 +59,14 @@
private MenuItem mMenuItem;
private SearchView mSearchView;
- public SearchViewManager(SearchManagerListener listener, @Nullable Bundle savedState) {
+ // We need to disable sorting during search.
+ private SortModel mSortModel;
+
+ public SearchViewManager(
+ SearchManagerListener listener, @Nullable Bundle savedState, SortModel sortModel) {
mListener = listener;
mCurrentSearch = savedState != null ? savedState.getString(Shared.EXTRA_QUERY) : null;
+ mSortModel = sortModel;
}
public void setSearchMangerListener(SearchManagerListener listener) {
@@ -186,6 +192,8 @@
Menu menu = mActionBar.getMenu();
menu.setGroupVisible(R.id.group_hide_when_searching, false);
}
+
+ mSortModel.setSortEnabled(false);
}
/**
@@ -214,6 +222,8 @@
}
mListener.onSearchFinished();
+ mSortModel.setSortEnabled(true);
+
return false;
}
diff --git a/src/com/android/documentsui/State.java b/src/com/android/documentsui/State.java
index 2ee8961..5ae247d 100644
--- a/src/com/android/documentsui/State.java
+++ b/src/com/android/documentsui/State.java
@@ -74,11 +74,6 @@
public static final int MODE_LIST = 1;
public static final int MODE_GRID = 2;
- public static final int SORT_ORDER_UNKNOWN = 0;
- public static final int SORT_ORDER_DISPLAY_NAME = 1;
- public static final int SORT_ORDER_LAST_MODIFIED = 2;
- public static final int SORT_ORDER_SIZE = 3;
-
public @ActionType int action;
public String[] acceptMimes;
@@ -87,10 +82,6 @@
/** Current sort state */
public SortModel sortModel;
- /** Explicit user choice */
- public int userSortOrder = SORT_ORDER_UNKNOWN;
- /** Derived after loader */
- public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME;
public boolean allowMultiple;
public boolean forceSize;
@@ -199,7 +190,6 @@
public void writeToParcel(Parcel out, int flags) {
out.writeInt(action);
out.writeStringArray(acceptMimes);
- out.writeInt(userSortOrder);
out.writeInt(allowMultiple ? 1 : 0);
out.writeInt(forceSize ? 1 : 0);
out.writeInt(mShowSize ? 1 : 0);
@@ -229,7 +219,6 @@
final State state = new State();
state.action = in.readInt();
state.acceptMimes = in.readStringArray();
- state.userSortOrder = in.readInt();
state.allowMultiple = in.readInt() != 0;
state.forceSize = in.readInt() != 0;
state.mShowSize = in.readInt() != 0;
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index f2696d0..5ecdb88 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -19,7 +19,6 @@
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.State.MODE_GRID;
import static com.android.documentsui.State.MODE_LIST;
-import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
import static com.android.documentsui.model.DocumentInfo.getCursorInt;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
@@ -104,7 +103,9 @@
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperations;
-import com.android.documentsui.sorting.SortController;
+import com.android.documentsui.sorting.SortDimension;
+import com.android.documentsui.sorting.SortDimension.SortDirection;
+import com.android.documentsui.sorting.SortModel;
import java.io.IOException;
import java.lang.annotation.Retention;
@@ -163,7 +164,8 @@
private String mStateKey;
- private int mLastSortOrder = SORT_ORDER_UNKNOWN;
+ private SortDimension mLastSortDimension;
+ private @SortDirection int mLastSortDirection;
private DocumentsAdapter mAdapter;
private FragmentTuner mTuner;
private DocumentClipper mClipper;
@@ -194,6 +196,12 @@
private MenuManager mMenuManager;
private TableHeaderController mTableHeaderController;
+ 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) {
+ getLoaderManager().restartLoader(LOADER_ID, null, this);
+ }
+ };
@Override
public View onCreateView(
@@ -343,12 +351,45 @@
boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
mIconHelper.setThumbnailsEnabled(!svelte);
- mTuner.mSortController.manage(mTableHeaderController, getDisplayState().derivedMode);
+ // If mDocument is null, we sort it by last modified by default because it's in Recents.
+ final boolean prefersLastModified =
+ (mDocument != null)
+ ? (mDocument.flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0
+ : true;
+ // Call this before adding the listener to avoid restarting the loader one more time
+ state.sortModel.setDefaultDimension(
+ prefersLastModified
+ ? SortModel.SORT_DIMENSION_ID_DATE
+ : SortModel.SORT_DIMENSION_ID_TITLE);
// Kick off loader at least once
getLoaderManager().restartLoader(LOADER_ID, null, this);
}
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ mTuner.mSortController.manage(mTableHeaderController, getDisplayState().derivedMode);
+ // Add listener to update contents on sort model change
+ getDisplayState().sortModel.addListener(mSortListener);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ // Remove listener to avoid leak.
+ mTuner.mSortController.clean(mTableHeaderController);
+ getDisplayState().sortModel.removeListener(mSortListener);
+
+ // Remember last scroll location
+ final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
+ getView().saveHierarchyState(container);
+ final State state = getDisplayState();
+ state.dirState.put(mStateKey, container);
+ }
+
public void retainState(RetainedState state) {
state.selection = mSelectionMgr.getSelection(new Selection());
}
@@ -472,17 +513,6 @@
return false;
}
- @Override
- public void onStop() {
- super.onStop();
-
- // Remember last scroll location
- final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
- getView().saveHierarchyState(container);
- final State state = getDisplayState();
- state.dirState.put(mStateKey, container);
- }
-
public void onDisplayStateChanged() {
updateDisplayState();
}
@@ -1654,7 +1684,7 @@
contentsUri = DocumentsContract.setManageMode(contentsUri);
}
return new DirectoryLoader(
- context, mType, mRoot, mDocument, contentsUri, state.userSortOrder,
+ context, mType, mRoot, mDocument, contentsUri, state.sortModel,
mSearchMode);
case TYPE_RECENT_OPEN:
final RootsCache roots = DocumentsApplication.getRootsCache(context);
@@ -1678,8 +1708,6 @@
mAdapter.notifyDataSetChanged();
mModel.update(result);
- state.derivedSortOrder = result.sortOrder;
-
updateLayout(state.derivedMode);
if (mRestoredSelection != null) {
@@ -1691,16 +1719,20 @@
// Restore any previous instance state
final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
+ final int curSortedDimensionId = state.sortModel.getSortedDimensionId();
+ final SortDimension curSortedDimension =
+ state.sortModel.getDimensionById(curSortedDimensionId);
if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
getView().restoreHierarchyState(container);
- } else if (mLastSortOrder != state.derivedSortOrder) {
- // The derived sort order takes the user sort order into account, but applies
- // directory-specific defaults when the user doesn't explicitly set the sort
- // order. Scroll to the top if the sort order actually changed.
+ } else if (mLastSortDimension != curSortedDimension
+ || mLastSortDimension == null
+ || mLastSortDirection != curSortedDimension.getSortDirection()) {
+ // Scroll to the top if the sort order actually changed.
mRecView.smoothScrollToPosition(0);
}
- mLastSortOrder = state.derivedSortOrder;
+ mLastSortDimension = curSortedDimension;
+ mLastSortDirection = curSortedDimension.getSortDirection();
mTuner.onModelLoaded(mModel, mType, mSearchMode);
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 5c15228..1a171bd 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -17,9 +17,6 @@
package com.android.documentsui.dirlist;
import static com.android.documentsui.Shared.DEBUG;
-import static com.android.documentsui.State.SORT_ORDER_DISPLAY_NAME;
-import static com.android.documentsui.State.SORT_ORDER_LAST_MODIFIED;
-import static com.android.documentsui.State.SORT_ORDER_SIZE;
import static com.android.documentsui.model.DocumentInfo.getCursorLong;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
@@ -38,6 +35,8 @@
import com.android.documentsui.Shared;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.sorting.SortDimension;
+import com.android.documentsui.sorting.SortModel;
import java.util.ArrayList;
import java.util.HashMap;
@@ -59,10 +58,10 @@
private Map<String, Integer> mPositions = new HashMap<>();
/**
* A sorted array of model IDs for the files currently in the Model. Sort order is determined
- * by {@link #mSortOrder}
+ * by {@link #mSortModel}
*/
private String mIds[] = new String[0];
- private int mSortOrder = SORT_ORDER_DISPLAY_NAME;
+ private SortModel mSortModel;
@Nullable String info;
@Nullable String error;
@@ -104,7 +103,7 @@
mCursor = result.cursor;
mCursorCount = mCursor.getCount();
- mSortOrder = result.sortOrder;
+ mSortModel = result.sortModel;
doc = result.doc;
updateModelData();
@@ -135,12 +134,13 @@
String[] displayNames = null;
long[] longValues = null;
- switch (mSortOrder) {
- case SORT_ORDER_DISPLAY_NAME:
+ final int id = mSortModel.getSortedDimensionId();
+ switch (id) {
+ case SortModel.SORT_DIMENSION_ID_TITLE:
displayNames = new String[mCursorCount];
break;
- case SORT_ORDER_LAST_MODIFIED:
- case SORT_ORDER_SIZE:
+ case SortModel.SORT_DIMENSION_ID_DATE:
+ case SortModel.SORT_DIMENSION_ID_SIZE:
longValues = new long[mCursorCount];
break;
}
@@ -169,28 +169,29 @@
mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
isDirs[pos] = Document.MIME_TYPE_DIR.equals(mimeType);
- switch(mSortOrder) {
- case SORT_ORDER_DISPLAY_NAME:
+ switch(id) {
+ case SortModel.SORT_DIMENSION_ID_TITLE:
final String displayName = getCursorString(
mCursor, Document.COLUMN_DISPLAY_NAME);
displayNames[pos] = displayName;
break;
- case SORT_ORDER_LAST_MODIFIED:
+ case SortModel.SORT_DIMENSION_ID_DATE:
longValues[pos] = getLastModified(mCursor);
break;
- case SORT_ORDER_SIZE:
+ case SortModel.SORT_DIMENSION_ID_SIZE:
longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
break;
}
}
- switch (mSortOrder) {
- case SORT_ORDER_DISPLAY_NAME:
- binarySort(displayNames, isDirs, positions, mIds);
+ final SortDimension dimension = mSortModel.getDimensionById(id);
+ switch (id) {
+ case SortModel.SORT_DIMENSION_ID_TITLE:
+ binarySort(displayNames, isDirs, positions, mIds, dimension.getSortDirection());
break;
- case SORT_ORDER_LAST_MODIFIED:
- case SORT_ORDER_SIZE:
- binarySort(longValues, isDirs, positions, mIds);
+ case SortModel.SORT_DIMENSION_ID_DATE:
+ case SortModel.SORT_DIMENSION_ID_SIZE:
+ binarySort(longValues, isDirs, positions, mIds, dimension.getSortDirection());
break;
}
@@ -211,7 +212,12 @@
* @param positions Cursor positions to be sorted.
* @param ids Model IDs to be sorted.
*/
- private static void binarySort(String[] sortKey, boolean[] isDirs, int[] positions, String[] ids) {
+ private static void binarySort(
+ String[] sortKey,
+ boolean[] isDirs,
+ int[] positions,
+ String[] ids,
+ @SortDimension.SortDirection int direction) {
final int count = positions.length;
for (int start = 1; start < count; start++) {
final int pivotPosition = positions[start];
@@ -235,7 +241,17 @@
} else {
final String lhs = pivotValue;
final String rhs = sortKey[mid];
- compare = Shared.compareToIgnoreCaseNullable(lhs, rhs);
+ switch (direction) {
+ case SortDimension.SORT_DIRECTION_ASCENDING:
+ compare = Shared.compareToIgnoreCaseNullable(lhs, rhs);
+ break;
+ case SortDimension.SORT_DIRECTION_DESCENDING:
+ compare = -Shared.compareToIgnoreCaseNullable(lhs, rhs);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown sorting direction: " + direction);
+ }
}
if (compare < 0) {
@@ -284,7 +300,11 @@
* @param ids Model IDs to be sorted.
*/
private static void binarySort(
- long[] sortKey, boolean[] isDirs, int[] positions, String[] ids) {
+ long[] sortKey,
+ boolean[] isDirs,
+ int[] positions,
+ String[] ids,
+ @SortDimension.SortDirection int direction) {
final int count = positions.length;
for (int start = 1; start < count; start++) {
final int pivotPosition = positions[start];
@@ -308,9 +328,17 @@
} else {
final long lhs = pivotValue;
final long rhs = sortKey[mid];
- // Sort in descending numerical order. This matches legacy behaviour, which
- // yields largest or most recent items on top.
- compare = -Long.compare(lhs, rhs);
+ switch (direction) {
+ case SortDimension.SORT_DIRECTION_ASCENDING:
+ compare = Long.compare(lhs, rhs);
+ break;
+ case SortDimension.SORT_DIRECTION_DESCENDING:
+ compare = -Long.compare(lhs, rhs);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown sorting direction: " + direction);
+ }
}
// If numerical comparison yields a tie, use document ID as a tie breaker. This
@@ -358,7 +386,7 @@
/**
* @return Timestamp for the given document. Some docs (e.g. active downloads) have a null
* timestamp - these will be replaced with MAX_LONG so that such files get sorted to the top
- * when sorting by date.
+ * when sorting descending by date.
*/
long getLastModified(Cursor cursor) {
long l = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
@@ -410,6 +438,14 @@
return DocumentInfo.getUri(cursor);
}
+ /**
+ * @return An ordered array of model IDs representing the documents in the model. It is sorted
+ * according to the current sort order, which was set by the last model update.
+ */
+ public String[] getModelIds() {
+ return mIds;
+ }
+
void addUpdateListener(UpdateListener listener) {
mUpdateListeners.add(listener);
}
@@ -418,7 +454,7 @@
mUpdateListeners.remove(listener);
}
- static interface UpdateListener {
+ interface UpdateListener {
/**
* Called when a successful update has occurred.
*/
@@ -429,12 +465,4 @@
*/
void onModelUpdateFailed(Exception e);
}
-
- /**
- * @return An ordered array of model IDs representing the documents in the model. It is sorted
- * according to the current sort order, which was set by the last model update.
- */
- public String[] getModelIds() {
- return mIds;
- }
}
diff --git a/src/com/android/documentsui/dirlist/header/TableHeaderController.java b/src/com/android/documentsui/dirlist/header/TableHeaderController.java
index 8a0074a..a48f963 100644
--- a/src/com/android/documentsui/dirlist/header/TableHeaderController.java
+++ b/src/com/android/documentsui/dirlist/header/TableHeaderController.java
@@ -22,6 +22,7 @@
import com.android.documentsui.R;
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;
@@ -63,7 +64,7 @@
mModel = model;
if (mModel != null) {
- onModelUpdate(mModel);
+ onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED);
mModel.addListener(mModelUpdaterListener);
}
@@ -74,7 +75,7 @@
mTableHeader.setVisibility(visibility);
}
- private void onModelUpdate(SortModel model) {
+ 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);
@@ -99,6 +100,6 @@
private void onCellClicked(View v) {
SortDimension dimension = (SortDimension) v.getTag();
- mModel.sortBy(dimension.getId(), dimension.getNextDirection());
+ mModel.sortByUser(dimension.getId(), dimension.getNextDirection());
}
}
diff --git a/src/com/android/documentsui/sorting/SortController.java b/src/com/android/documentsui/sorting/SortController.java
index 630b4ef..dde07d3 100644
--- a/src/com/android/documentsui/sorting/SortController.java
+++ b/src/com/android/documentsui/sorting/SortController.java
@@ -17,8 +17,10 @@
package com.android.documentsui.sorting;
import android.annotation.Nullable;
+import android.content.Context;
import android.view.View;
+import com.android.documentsui.Metrics;
import com.android.documentsui.State;
import com.android.documentsui.dirlist.header.TableHeaderController;
@@ -32,24 +34,62 @@
private static final WidgetController DUMMY_CONTROLLER = new WidgetController() {};
private final SortModel mModel;
- private WidgetController mTableHeaderController = DUMMY_CONTROLLER;
+ private final Context mContext;
- public SortController(SortModel model) {
+ private WidgetController mTableHeaderController = DUMMY_CONTROLLER;
+ private WidgetController mSortMenuController = DUMMY_CONTROLLER;
+
+ public SortController(SortModel model, Context context) {
mModel = model;
+ mContext = context.getApplicationContext();
+
+ mModel.setMetricRecorder(this::recordSortMetric);
}
public void manage(
- @Nullable TableHeaderController tableHeaderController, @State.ViewMode int mode) {
- if (tableHeaderController == null) {
+ @Nullable TableHeaderController controller, @State.ViewMode int mode) {
+ assert(mTableHeaderController == DUMMY_CONTROLLER);
+
+ if (controller == null) {
return;
}
- mTableHeaderController = tableHeaderController;
+ mTableHeaderController = controller;
mTableHeaderController.setModel(mModel);
setVisibilityPerViewMode(mTableHeaderController, mode, View.GONE, View.VISIBLE);
}
+ public void clean(@Nullable TableHeaderController controller) {
+ assert(controller == null || mTableHeaderController == controller);
+
+ if (controller != null) {
+ controller.setModel(null);
+ }
+
+ mTableHeaderController = DUMMY_CONTROLLER;
+ }
+
+ public void manage(SortMenuController controller) {
+ assert(mSortMenuController == DUMMY_CONTROLLER);
+
+ if (controller != null) {
+ controller.setModel(mModel);
+ }
+
+ mSortMenuController = controller;
+ }
+
+ public void clean(SortMenuController controller) {
+ assert(mSortMenuController == controller);
+
+ if (controller != null) {
+ controller.setModel(null);
+ }
+
+ mSortMenuController = DUMMY_CONTROLLER;
+ }
+
public void onViewModeChanged(@State.ViewMode int mode) {
setVisibilityPerViewMode(mTableHeaderController, mode, View.GONE, View.VISIBLE);
}
@@ -71,6 +111,20 @@
}
}
+ 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 interface WidgetController {
default void setModel(SortModel model) {}
default void setVisibility(int visibility) {}
diff --git a/src/com/android/documentsui/sorting/SortDimension.java b/src/com/android/documentsui/sorting/SortDimension.java
index 6881c97..9d235a1 100644
--- a/src/com/android/documentsui/sorting/SortDimension.java
+++ b/src/com/android/documentsui/sorting/SortDimension.java
@@ -124,6 +124,41 @@
}
@Override
+ public boolean equals(Object o) {
+ if (o == null || !(o instanceof SortDimension)) {
+ return false;
+ }
+
+ if (this == o) {
+ return true;
+ }
+
+ SortDimension other = (SortDimension) o;
+
+ return mId == other.mId
+ && mLabelId == other.mLabelId
+ && mDataType == other.mDataType
+ && mSortCapability == other.mSortCapability
+ && mDefaultSortDirection == other.mDefaultSortDirection
+ && mSortDirection == other.mSortDirection
+ && mVisibility == other.mVisibility;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder().append("SortDimension{")
+ .append("id=").append(mId)
+ .append(", labelId=").append(mLabelId)
+ .append(", dataType=").append(mDataType)
+ .append(", sortCapability=").append(mSortCapability)
+ .append(", defaultSortDirection=").append(mDefaultSortDirection)
+ .append(", sortDirection=").append(mSortDirection)
+ .append(", visibility=").append(mVisibility)
+ .append("}")
+ .toString();
+ }
+
+ @Override
public int describeContents() {
return 0;
}
@@ -145,13 +180,13 @@
@Override
public SortDimension createFromParcel(Parcel in) {
int id = in.readInt();
- @StringRes int lableId = in.readInt();
+ @StringRes int labelId = in.readInt();
@DataType int dataType = in.readInt();
int sortCapability = in.readInt();
int defaultSortDirection = in.readInt();
SortDimension column =
- new SortDimension(id, lableId, dataType, sortCapability, defaultSortDirection);
+ new SortDimension(id, labelId, dataType, sortCapability, defaultSortDirection);
column.mSortDirection = in.readInt();
column.mVisibility = in.readInt();
diff --git a/src/com/android/documentsui/sorting/SortMenuController.java b/src/com/android/documentsui/sorting/SortMenuController.java
new file mode 100644
index 0000000..f5f9a2f
--- /dev/null
+++ b/src/com/android/documentsui/sorting/SortMenuController.java
@@ -0,0 +1,158 @@
+/*
+ * 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.sorting;
+
+import android.content.res.Resources;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+
+import com.android.documentsui.R;
+
+import java.util.Formatter;
+
+/**
+ * Displays and manages menu items related to sorting. It adds menu items for each
+ * {@link SortDimension} that supports sorting.
+ */
+public class SortMenuController implements SortController.WidgetController {
+
+ private final String mPhrase;
+ private final Resources mRes;
+
+ private final SortModel.UpdateListener mListener = this::onModelUpdate;
+ private final MenuItem.OnMenuItemClickListener mItemClickListener = this::onMenuItemClicked;
+
+ private boolean mVisible = true;
+ private MenuItem mMenu;
+ private SortModel mModel;
+
+ public SortMenuController(Resources res) {
+ mRes = res;
+ mPhrase = mRes.getString(R.string.sort_phrase);
+ }
+
+ public void install(MenuItem menu) {
+ assert(menu.hasSubMenu());
+ mMenu = menu;
+
+ initItem();
+ onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED);
+ }
+
+ @Override
+ public void setModel(SortModel sortModel) {
+ if (mModel != null) {
+ clearItem();
+ mModel.removeListener(mListener);
+ }
+
+ mModel = sortModel;
+
+ if (mModel != null) {
+ initItem();
+ onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED);
+ mModel.addListener(mListener);
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mVisible = (visibility == View.VISIBLE);
+ mMenu.setVisible(mVisible);
+ }
+
+ private void initItem() {
+ if (mMenu == null || mModel == null) {
+ return;
+ }
+
+ mMenu.setVisible(mVisible);
+
+ SubMenu menu = mMenu.getSubMenu();
+
+ StringBuilder builder = new StringBuilder();
+ Formatter formatter = new Formatter(builder);
+ for (int i = 0; i < mModel.getSize(); ++i) {
+ final SortDimension dimension = mModel.getDimensionAt(i);
+
+ // We don't need to add menu item if a dimension is not sortable
+ if (dimension.getSortCapability() == SortDimension.SORT_CAPABILITY_NONE) {
+ continue;
+ }
+
+ String title = formatter
+ .format(mPhrase, mRes.getString(dimension.getLabelId()))
+ .toString();
+
+ // Clean the underlying builder so that we can reuse it for next menu item.
+ builder.setLength(0);
+ menu.add(0, dimension.getId(), Menu.NONE, title);
+ }
+ }
+
+ private void clearItem() {
+ if (mMenu == null) {
+ return;
+ }
+
+ mMenu.getSubMenu().clear();
+ }
+
+ /**
+ * Update the state of menu items based on {@link SortModel}. Note it doesn't add or remove any
+ * menu item, but set the visibility.
+ * @param model the new model
+ */
+ private void onModelUpdate(SortModel model, @SortModel.UpdateType int updateType) {
+ // Sort menu doesn't record sort direction so there is nothing to update if only sort order
+ // has changed.
+ if (mMenu == null || updateType == SortModel.UPDATE_TYPE_SORTING) {
+ return;
+ }
+
+ mMenu.setEnabled(mModel.isSortEnabled());
+
+ SubMenu menu = mMenu.getSubMenu();
+ if (mModel.isSortEnabled()) {
+ for (int i = 0; i < menu.size(); ++i) {
+ MenuItem item = menu.getItem(i);
+ SortDimension dimension = mModel.getDimensionById(item.getItemId());
+
+ bindItem(item, dimension);
+ }
+ }
+ }
+
+ private void bindItem(MenuItem item, SortDimension dimension) {
+ item.setVisible(dimension.getVisibility() == View.VISIBLE);
+ if (dimension.getVisibility() == View.VISIBLE) {
+ item.setOnMenuItemClickListener(mItemClickListener);
+ } else {
+ item.setOnMenuItemClickListener(null);
+ }
+ }
+
+ private boolean onMenuItemClicked(MenuItem item) {
+ final SortDimension dimension = mModel.getDimensionById(item.getItemId());
+
+ // Click on menu item will only sort stuff in its default direction
+ mModel.sortByUser(dimension.getId(), dimension.getDefaultSortDirection());
+ return true;
+ }
+}
diff --git a/src/com/android/documentsui/sorting/SortModel.java b/src/com/android/documentsui/sorting/SortModel.java
index b5067fd..b812de7 100644
--- a/src/com/android/documentsui/sorting/SortModel.java
+++ b/src/com/android/documentsui/sorting/SortModel.java
@@ -16,9 +16,14 @@
package com.android.documentsui.sorting;
+import static com.android.documentsui.Shared.DEBUG;
+
import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
+import android.provider.DocumentsContract.Document;
+import android.util.Log;
import android.util.SparseArray;
import android.view.View;
@@ -29,12 +34,14 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.function.Consumer;
/**
* Sort model that contains all columns and their sorting state.
*/
public class SortModel implements Parcelable {
@IntDef({
+ SORT_DIMENSION_ID_UNKNOWN,
SORT_DIMENSION_ID_TITLE,
SORT_DIMENSION_ID_SUMMARY,
SORT_DIMENSION_ID_DATE,
@@ -42,16 +49,48 @@
})
@Retention(RetentionPolicy.SOURCE)
public @interface SortDimensionId {}
+ public static final int SORT_DIMENSION_ID_UNKNOWN = 0;
public static final int SORT_DIMENSION_ID_TITLE = android.R.id.title;
public static final int SORT_DIMENSION_ID_SUMMARY = android.R.id.summary;
public static final int SORT_DIMENSION_ID_SIZE = R.id.size;
public static final int SORT_DIMENSION_ID_DATE = R.id.date;
+ @IntDef({
+ UPDATE_TYPE_UNSPECIFIED,
+ UPDATE_TYPE_STATUS,
+ UPDATE_TYPE_VISIBILITY,
+ UPDATE_TYPE_SORTING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface UpdateType {}
+ /**
+ * Default value for update type. Anything can be changed if the type is unspecified.
+ */
+ public static final int UPDATE_TYPE_UNSPECIFIED = 0;
+ /**
+ * Indicates the status of sorting has changed, i.e. whether soring is enabled.
+ */
+ public static final int UPDATE_TYPE_STATUS = 1;
+ /**
+ * Indicates the visibility of at least one dimension has changed.
+ */
+ public static final int UPDATE_TYPE_VISIBILITY = 2;
+ /**
+ * Indicates the sorting order has changed, either because the sorted dimension has changed or
+ * the sort direction has changed.
+ */
+ public static final int UPDATE_TYPE_SORTING = 3;
+
+ private static final String TAG = "SortModel";
+
private final SparseArray<SortDimension> mDimensions;
private transient final List<UpdateListener> mListeners;
+ private transient Consumer<SortDimension> mMetricRecorder;
- private SortDimension mSortedDimension;
+ private int mDefaultDimensionId = SORT_DIMENSION_ID_UNKNOWN;
+ private boolean mIsUserSpecified = false;
+ private @Nullable SortDimension mSortedDimension;
private boolean mIsSortEnabled = true;
@@ -59,6 +98,10 @@
mDimensions = new SparseArray<>(columns.size());
for (SortDimension column : columns) {
+ if (column.getId() == SORT_DIMENSION_ID_UNKNOWN) {
+ throw new IllegalArgumentException(
+ "SortDimension id can't be " + SORT_DIMENSION_ID_UNKNOWN + ".");
+ }
if (mDimensions.get(column.getId()) != null) {
throw new IllegalStateException(
"SortDimension id must be unique. Duplicate id: " + column.getId());
@@ -77,40 +120,87 @@
return mDimensions.valueAt(index);
}
- public SortDimension getDimensionById(int id) {
+ public @Nullable SortDimension getDimensionById(int id) {
return mDimensions.get(id);
}
- public SortDimension getSortedDimension() {
- return mSortedDimension;
+ /**
+ * Gets the sorted dimension id.
+ * @return the sorted dimension id or {@link #SORT_DIMENSION_ID_UNKNOWN} if there is no sorted
+ * dimension.
+ */
+ public int getSortedDimensionId() {
+ return mSortedDimension != null ? mSortedDimension.getId() : SORT_DIMENSION_ID_UNKNOWN;
}
public void setSortEnabled(boolean enabled) {
- if (!enabled) {
- clearSortDirection();
- }
mIsSortEnabled = enabled;
- notifyListeners();
+ notifyListeners(UPDATE_TYPE_STATUS);
}
public boolean isSortEnabled() {
return mIsSortEnabled;
}
- public void sortBy(int columnId, @SortDimension.SortDirection int direction) {
+ /**
+ * Sort by the default direction of the given dimension if user has never specified any sort
+ * direction before.
+ * @param dimensionId the id of the dimension
+ */
+ public void setDefaultDimension(int dimensionId) {
+ final boolean mayNeedSorting = (mDefaultDimensionId != dimensionId);
+
+ mDefaultDimensionId = dimensionId;
+
+ if (mayNeedSorting) {
+ sortOnDefault();
+ }
+ }
+
+ void setMetricRecorder(Consumer<SortDimension> metricRecorder) {
+ mMetricRecorder = metricRecorder;
+ }
+
+ /**
+ * Sort by given dimension and direction. Should only be used when user explicitly asks to sort
+ * docs.
+ * @param dimensionId the id of the dimension
+ * @param direction the direction to sort docs in
+ */
+ public void sortByUser(int dimensionId, @SortDimension.SortDirection int direction) {
if (!mIsSortEnabled) {
throw new IllegalStateException("Sort is not enabled.");
}
- if (mDimensions.get(columnId) == null) {
- throw new IllegalArgumentException("Unknown column id: " + columnId);
+
+ SortDimension dimension = mDimensions.get(dimensionId);
+ if (dimension == null) {
+ throw new IllegalArgumentException("Unknown column id: " + dimensionId);
}
- SortDimension newSortedDimension = mDimensions.get(columnId);
- if ((direction & newSortedDimension.getSortCapability()) == 0) {
- throw new IllegalStateException(
- "SortDimension " + columnId + " can't be sorted in direction " + direction);
+ sortByDimension(dimension, direction);
+
+ if (mMetricRecorder != null) {
+ mMetricRecorder.accept(dimension);
}
+
+ mIsUserSpecified = true;
+ }
+
+ private void sortByDimension(
+ SortDimension newSortedDimension, @SortDimension.SortDirection int direction) {
+ if (newSortedDimension == mSortedDimension
+ && mSortedDimension.mSortDirection == direction) {
+ // Sort direction not changed, no need to proceed.
+ return;
+ }
+
+ if ((newSortedDimension.getSortCapability() & direction) == 0) {
+ throw new IllegalStateException(
+ "Dimension with id: " + newSortedDimension.getId()
+ + " can't be sorted in direction:" + direction);
+ }
+
switch (direction) {
case SortDimension.SORT_DIRECTION_ASCENDING:
case SortDimension.SORT_DIRECTION_DESCENDING:
@@ -126,7 +216,7 @@
mSortedDimension = newSortedDimension;
- notifyListeners();
+ notifyListeners(UPDATE_TYPE_SORTING);
}
public void setDimensionVisibility(int columnId, int visibility) {
@@ -134,12 +224,49 @@
mDimensions.get(columnId).mVisibility = visibility;
- notifyListeners();
+ notifyListeners(UPDATE_TYPE_VISIBILITY);
}
- private void notifyListeners() {
+ public @Nullable String getDocumentSortQuery() {
+ final int id = getSortedDimensionId();
+ final String columnName;
+ switch (id) {
+ case SortModel.SORT_DIMENSION_ID_UNKNOWN:
+ return null;
+ case SortModel.SORT_DIMENSION_ID_TITLE:
+ columnName = Document.COLUMN_DISPLAY_NAME;
+ break;
+ case SortModel.SORT_DIMENSION_ID_DATE:
+ columnName = Document.COLUMN_LAST_MODIFIED;
+ break;
+ case SortModel.SORT_DIMENSION_ID_SIZE:
+ columnName = Document.COLUMN_SIZE;
+ break;
+ default:
+ throw new IllegalStateException(
+ "Unexpected sort dimension id: " + id);
+ }
+
+ final SortDimension dimension = getDimensionById(id);
+ final String direction;
+ switch (dimension.getSortDirection()) {
+ case SortDimension.SORT_DIRECTION_ASCENDING:
+ direction = " ASC";
+ break;
+ case SortDimension.SORT_DIRECTION_DESCENDING:
+ direction = " DESC";
+ break;
+ default:
+ throw new IllegalStateException(
+ "Unexpected sort direction: " + dimension.getSortDirection());
+ }
+
+ return columnName + direction;
+ }
+
+ private void notifyListeners(@UpdateType int updateType) {
for (int i = mListeners.size() - 1; i >= 0; --i) {
- mListeners.get(i).onModelUpdate(this);
+ mListeners.get(i).onModelUpdate(this, updateType);
}
}
@@ -156,6 +283,66 @@
mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
mSortedDimension = null;
}
+
+ mIsUserSpecified = false;
+
+ sortOnDefault();
+ }
+
+ /**
+ * Sort by default dimension and direction if there is no history of user specifying a sort
+ * order.
+ */
+ private void sortOnDefault() {
+ if (!mIsUserSpecified) {
+ SortDimension dimension = mDimensions.get(mDefaultDimensionId);
+ if (dimension == null) {
+ if (DEBUG) Log.d(TAG, "No default sort dimension.");
+ return;
+ }
+
+ sortByDimension(dimension, dimension.getDefaultSortDirection());
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || !(o instanceof SortModel)) {
+ return false;
+ }
+
+ if (this == o) {
+ return true;
+ }
+
+ SortModel other = (SortModel) o;
+ if (mDimensions.size() != other.mDimensions.size()) {
+ return false;
+ }
+ for (int i = 0; i < mDimensions.size(); ++i) {
+ final SortDimension dimension = mDimensions.valueAt(i);
+ final int id = dimension.getId();
+ if (!dimension.equals(other.getDimensionById(id))) {
+ return false;
+ }
+ }
+
+ return mDefaultDimensionId == other.mDefaultDimensionId
+ && mIsSortEnabled == other.mIsSortEnabled
+ && (mSortedDimension == other.mSortedDimension
+ || mSortedDimension.equals(other.mSortedDimension));
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("SortModel{")
+ .append("enabled=").append(mIsSortEnabled)
+ .append(", dimensions=").append(mDimensions)
+ .append(", defaultDimensionId=").append(mDefaultDimensionId)
+ .append(", sortedDimension=").append(mSortedDimension)
+ .append("}")
+ .toString();
}
@Override
@@ -169,18 +356,28 @@
for (int i = 0; i < mDimensions.size(); ++i) {
out.writeParcelable(mDimensions.valueAt(i), flag);
}
+
+ out.writeInt(mDefaultDimensionId);
+ out.writeInt(mIsSortEnabled ? 1 : 0);
+ out.writeInt(mSortedDimension.getId());
}
public static Parcelable.Creator<SortModel> CREATOR = new Parcelable.Creator<SortModel>() {
@Override
public SortModel createFromParcel(Parcel in) {
- int size = in.readInt();
+ final int size = in.readInt();
Collection<SortDimension> columns = new ArrayList<>(size);
for (int i = 0; i < size; ++i) {
columns.add(in.readParcelable(getClass().getClassLoader()));
}
- return new SortModel(columns);
+ SortModel model = new SortModel(columns);
+
+ model.mDefaultDimensionId = in.readInt();
+ model.mIsSortEnabled = (in.readInt() == 1);
+ model.mSortedDimension = model.getDimensionById(in.readInt());
+
+ return model;
}
@Override
@@ -201,7 +398,7 @@
// Name column
dimensions.add(builder
.withId(SORT_DIMENSION_ID_TITLE)
- .withLabelId(R.string.column_name)
+ .withLabelId(R.string.sort_dimension_name)
.withDataType(SortDimension.DATA_TYPE_STRING)
.withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
.withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
@@ -213,7 +410,7 @@
// Summary is only visible in Downloads and Recents root.
dimensions.add(builder
.withId(SORT_DIMENSION_ID_SUMMARY)
- .withLabelId(R.string.column_summary)
+ .withLabelId(R.string.sort_dimension_summary)
.withDataType(SortDimension.DATA_TYPE_STRING)
.withSortCapability(SortDimension.SORT_CAPABILITY_NONE)
.withVisibility(View.INVISIBLE)
@@ -223,7 +420,7 @@
// Size column
dimensions.add(builder
.withId(SORT_DIMENSION_ID_SIZE)
- .withLabelId(R.string.column_size)
+ .withLabelId(R.string.sort_dimension_size)
.withDataType(SortDimension.DATA_TYPE_NUMBER)
.withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
.withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
@@ -234,7 +431,7 @@
// Date column
dimensions.add(builder
.withId(SORT_DIMENSION_ID_DATE)
- .withLabelId(R.string.column_date)
+ .withLabelId(R.string.sort_dimension_date)
.withDataType(SortDimension.DATA_TYPE_NUMBER)
.withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
.withDefaultSortDirection(SortDimension.SORT_DIRECTION_DESCENDING)
@@ -246,6 +443,6 @@
}
public interface UpdateListener {
- void onModelUpdate(SortModel newModel);
+ void onModelUpdate(SortModel newModel, @UpdateType int updateType);
}
}
diff --git a/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java b/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
index 685263d..d8f06ee 100644
--- a/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
+++ b/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
@@ -18,6 +18,7 @@
import static com.android.documentsui.State.ACTION_CREATE;
import static com.android.documentsui.State.ACTION_OPEN;
+
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -25,7 +26,6 @@
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
-import com.android.documentsui.sorting.SortModel;
import com.android.documentsui.model.RootInfo;
import com.android.documentsui.testing.TestDirectoryDetails;
import com.android.documentsui.testing.TestMenu;
@@ -54,8 +54,6 @@
private TestMenuItem cut;
private TestMenuItem copy;
private TestMenuItem paste;
- private TestMenuItem sort;
- private TestMenuItem sortSize;
private TestMenuItem advanced;
private TestMenuItem settings;
private TestMenuItem eject;
@@ -81,8 +79,6 @@
cut = testMenu.findItem(R.id.menu_cut_to_clipboard);
copy = testMenu.findItem(R.id.menu_copy_to_clipboard);
paste = testMenu.findItem(R.id.menu_paste_from_clipboard);
- sort = testMenu.findItem(R.id.menu_sort);
- sortSize = testMenu.findItem(R.id.menu_sort_size);
advanced = testMenu.findItem(R.id.menu_advanced);
settings = testMenu.findItem(R.id.menu_settings);
eject = testMenu.findItem(R.id.menu_eject_root);
@@ -93,7 +89,6 @@
testRootInfo = new RootInfo();
state.action = ACTION_CREATE;
state.allowMultiple = true;
- state.sortModel = SortModel.createModel();
}
@Test
@@ -132,8 +127,6 @@
DocumentsMenuManager mgr = new DocumentsMenuManager(testSearchManager, state);
mgr.updateOptionMenu(testMenu, directoryDetails);
- sort.assertEnabled();
- sortSize.assertInvisible();
advanced.assertInvisible();
advanced.assertTitle(R.string.menu_advanced_show);
createDir.assertDisabled();
@@ -142,15 +135,6 @@
}
@Test
- public void testOptionMenu_hideSize() {
- state.setShowSize(true);
- DocumentsMenuManager mgr = new DocumentsMenuManager(testSearchManager, state);
- mgr.updateOptionMenu(testMenu, directoryDetails);
-
- sortSize.assertVisible();
- }
-
- @Test
public void testOptionMenu_notPicking() {
state.action = ACTION_OPEN;
state.derivedMode = State.MODE_LIST;
@@ -189,7 +173,6 @@
DocumentsMenuManager mgr = new DocumentsMenuManager(testSearchManager, state);
mgr.updateOptionMenu(testMenu, directoryDetails);
- sort.assertDisabled();
grid.assertInvisible();
list.assertInvisible();
}
diff --git a/tests/src/com/android/documentsui/FilesMenuManagerTest.java b/tests/src/com/android/documentsui/FilesMenuManagerTest.java
index fa50427..1da1a10 100644
--- a/tests/src/com/android/documentsui/FilesMenuManagerTest.java
+++ b/tests/src/com/android/documentsui/FilesMenuManagerTest.java
@@ -22,7 +22,6 @@
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
-import com.android.documentsui.sorting.SortModel;
import com.android.documentsui.model.RootInfo;
import com.android.documentsui.testing.TestDirectoryDetails;
import com.android.documentsui.testing.TestMenu;
@@ -52,8 +51,6 @@
private TestMenuItem cut;
private TestMenuItem copy;
private TestMenuItem paste;
- private TestMenuItem sort;
- private TestMenuItem sortSize;
private TestMenuItem advanced;
private TestMenuItem eject;
private TestSelectionDetails selectionDetails;
@@ -77,8 +74,6 @@
cut = testMenu.findItem(R.id.menu_cut_to_clipboard);
copy = testMenu.findItem(R.id.menu_copy_to_clipboard);
paste = testMenu.findItem(R.id.menu_paste_from_clipboard);
- sort = testMenu.findItem(R.id.menu_sort);
- sortSize = testMenu.findItem(R.id.menu_sort_size);
advanced = testMenu.findItem(R.id.menu_advanced);
eject = testMenu.findItem(R.id.menu_eject_root);
@@ -91,8 +86,6 @@
directoryDetails = new TestDirectoryDetails();
testSearchManager = new TestSearchViewManager();
testRootInfo = new RootInfo();
-
- state.sortModel = SortModel.createModel();
}
@Test
@@ -157,8 +150,6 @@
FilesMenuManager mgr = new FilesMenuManager(testSearchManager, state);
mgr.updateOptionMenu(testMenu, directoryDetails);
- sort.assertEnabled();
- sortSize.assertInvisible();
advanced.assertInvisible();
advanced.assertTitle(R.string.menu_advanced_show);
createDir.assertDisabled();
@@ -167,15 +158,6 @@
}
@Test
- public void testOptionMenu_hideSize() {
- state.setShowSize(true);
- FilesMenuManager mgr = new FilesMenuManager(testSearchManager, state);
- mgr.updateOptionMenu(testMenu, directoryDetails);
-
- sortSize.assertVisible();
- }
-
- @Test
public void testOptionMenu_showAdvanced() {
state.showAdvanced = true;
state.showAdvancedOption = true;
@@ -187,15 +169,6 @@
}
@Test
- public void testOptionMenu_inRecents() {
- directoryDetails.isInRecents = true;
- FilesMenuManager mgr = new FilesMenuManager(testSearchManager, state);
- mgr.updateOptionMenu(testMenu, directoryDetails);
-
- sort.assertDisabled();
- }
-
- @Test
public void testOptionMenu_canCreateDirectory() {
directoryDetails.canCreateDirectory = true;
FilesMenuManager mgr = new FilesMenuManager(testSearchManager, state);
diff --git a/tests/src/com/android/documentsui/dirlist/ModelTest.java b/tests/src/com/android/documentsui/dirlist/ModelTest.java
index b816287..a582f21 100644
--- a/tests/src/com/android/documentsui/dirlist/ModelTest.java
+++ b/tests/src/com/android/documentsui/dirlist/ModelTest.java
@@ -30,9 +30,11 @@
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.RootCursorWrapper;
import com.android.documentsui.Shared;
-import com.android.documentsui.State;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.sorting.SortDimension;
+import com.android.documentsui.sorting.SortModel;
+import com.android.documentsui.testing.SortModels;
import java.util.ArrayList;
import java.util.BitSet;
@@ -74,6 +76,7 @@
private Context context;
private Model model;
private TestContentProvider provider;
+ private SortModel sortModel;
public void setUp() {
setupTestContext();
@@ -92,9 +95,11 @@
row.add(Document.COLUMN_SIZE, rand.nextInt());
}
cursor = c;
+ sortModel = SortModels.createTestSortModel();
DirectoryResult r = new DirectoryResult();
r.cursor = cursor;
+ r.sortModel = sortModel;
// Instantiate the model with a dummy view adapter and listener that (for now) do nothing.
model = new Model();
@@ -140,6 +145,7 @@
// Update the model, then make sure it contains all the expected items.
DirectoryResult r = new DirectoryResult();
r.cursor = cIn;
+ r.sortModel = SortModels.createTestSortModel();
model.update(r);
assertEquals(ITEM_COUNT * 2, model.getItemCount());
@@ -178,14 +184,17 @@
}
}
- // Tests sorting by item name.
- public void testSort_names() {
+ // Tests sorting ascending by item name.
+ public void testSort_names_ascending() {
BitSet seen = new BitSet(ITEM_COUNT);
List<String> names = new ArrayList<>();
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_TITLE,
+ SortDimension.SORT_DIRECTION_ASCENDING);
+
DirectoryResult r = new DirectoryResult();
r.cursor = cursor;
- r.sortOrder = State.SORT_ORDER_DISPLAY_NAME;
+ r.sortModel = sortModel;
model.update(r);
for (String id: model.getModelIds()) {
@@ -200,11 +209,63 @@
}
}
- // Tests sorting by item size.
- public void testSort_sizes() {
+ // Tests sorting descending by item name.
+ public void testSort_names_descending() {
+ BitSet seen = new BitSet(ITEM_COUNT);
+ List<String> names = new ArrayList<>();
+
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_TITLE,
+ SortDimension.SORT_DIRECTION_DESCENDING);
+
DirectoryResult r = new DirectoryResult();
r.cursor = cursor;
- r.sortOrder = State.SORT_ORDER_SIZE;
+ r.sortModel = sortModel;
+ model.update(r);
+
+ for (String id: model.getModelIds()) {
+ Cursor c = model.getItem(id);
+ seen.set(c.getPosition());
+ names.add(DocumentInfo.getCursorString(c, Document.COLUMN_DISPLAY_NAME));
+ }
+
+ assertEquals(ITEM_COUNT, seen.cardinality());
+ for (int i = 0; i < names.size()-1; ++i) {
+ assertTrue(Shared.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) >= 0);
+ }
+ }
+
+ // Tests sorting by item size.
+ public void testSort_sizes_ascending() {
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+ SortDimension.SORT_DIRECTION_ASCENDING);
+
+ DirectoryResult r = new DirectoryResult();
+ r.cursor = cursor;
+ r.sortModel = sortModel;
+ model.update(r);
+
+ BitSet seen = new BitSet(ITEM_COUNT);
+ int previousSize = Integer.MIN_VALUE;
+ for (String id: model.getModelIds()) {
+ Cursor c = model.getItem(id);
+ seen.set(c.getPosition());
+ // Check sort order - descending numerical
+ int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
+ assertTrue(previousSize <= size);
+ previousSize = size;
+ }
+ // Check that all items were accounted for.
+ assertEquals(ITEM_COUNT, seen.cardinality());
+ }
+
+ // Tests sorting by item size.
+ public void testSort_sizes_descending() {
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+ SortDimension.SORT_DIRECTION_DESCENDING);
+
+ DirectoryResult r = new DirectoryResult();
+ r.cursor = cursor;
+ r.sortModel = sortModel;
model.update(r);
BitSet seen = new BitSet(ITEM_COUNT);
@@ -222,7 +283,7 @@
}
// Tests that directories and files are properly bucketed when sorting by size
- public void testSort_sizesWithBucketing() {
+ public void testSort_sizesWithBucketing_ascending() {
MatrixCursor c = new MatrixCursor(COLUMNS);
for (int i = 0; i < ITEM_COUNT; ++i) {
@@ -235,9 +296,65 @@
row.add(Document.COLUMN_MIME_TYPE, mimeType);
}
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+ SortDimension.SORT_DIRECTION_ASCENDING);
+
DirectoryResult r = new DirectoryResult();
r.cursor = c;
- r.sortOrder = State.SORT_ORDER_SIZE;
+ r.sortModel = sortModel;
+ model.update(r);
+
+ boolean seenAllDirs = false;
+ int previousSize = Integer.MIN_VALUE;
+ BitSet seen = new BitSet(ITEM_COUNT);
+ // Iterate over items in sort order. Once we've encountered a document (i.e. not a
+ // directory), all subsequent items must also be documents. That is, all directories are
+ // bucketed at the front of the list, sorted by size, followed by documents, sorted by size.
+ for (String id: model.getModelIds()) {
+ Cursor cOut = model.getItem(id);
+ seen.set(cOut.getPosition());
+
+ String mimeType = DocumentInfo.getCursorString(cOut, Document.COLUMN_MIME_TYPE);
+ if (seenAllDirs) {
+ assertFalse(Document.MIME_TYPE_DIR.equals(mimeType));
+ } else {
+ if (!Document.MIME_TYPE_DIR.equals(mimeType)) {
+ seenAllDirs = true;
+ // Reset the previous size seen, because documents are bucketed separately by
+ // the sort.
+ previousSize = Integer.MIN_VALUE;
+ }
+ }
+ // Check sort order - descending numerical
+ int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
+ assertTrue(previousSize <= size);
+ previousSize = size;
+ }
+
+ // Check that all items were accounted for.
+ assertEquals(ITEM_COUNT, seen.cardinality());
+ }
+
+ // Tests that directories and files are properly bucketed when sorting by size
+ public void testSort_sizesWithBucketing_descending() {
+ MatrixCursor c = new MatrixCursor(COLUMNS);
+
+ for (int i = 0; i < ITEM_COUNT; ++i) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+ row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+ row.add(Document.COLUMN_SIZE, i);
+ // Interleave directories and text files.
+ String mimeType =(i % 2 == 0) ? Document.MIME_TYPE_DIR : "text/*";
+ row.add(Document.COLUMN_MIME_TYPE, mimeType);
+ }
+
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+ SortDimension.SORT_DIRECTION_DESCENDING);
+
+ DirectoryResult r = new DirectoryResult();
+ r.cursor = c;
+ r.sortModel = sortModel;
model.update(r);
boolean seenAllDirs = false;
@@ -271,7 +388,7 @@
assertEquals(ITEM_COUNT, seen.cardinality());
}
- public void testSort_time() {
+ public void testSort_time_ascending() {
final int DL_COUNT = 3;
MatrixCursor c = new MatrixCursor(COLUMNS);
Set<String> currentDownloads = new HashSet<>();
@@ -292,9 +409,52 @@
currentDownloads.add(id);
}
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_DATE,
+ SortDimension.SORT_DIRECTION_ASCENDING);
+
DirectoryResult r = new DirectoryResult();
r.cursor = c;
- r.sortOrder = State.SORT_ORDER_LAST_MODIFIED;
+ r.sortModel = sortModel;
+ model.update(r);
+
+ String[] ids = model.getModelIds();
+
+ // Check that all items were accounted for
+ assertEquals(ITEM_COUNT + DL_COUNT, ids.length);
+
+ // Check that active downloads are sorted to the bottom.
+ for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
+ assertTrue(currentDownloads.contains(ids[i]));
+ }
+ }
+
+ public void testSort_time_descending() {
+ final int DL_COUNT = 3;
+ MatrixCursor c = new MatrixCursor(COLUMNS);
+ Set<String> currentDownloads = new HashSet<>();
+
+ // Add some files
+ for (int i = 0; i < ITEM_COUNT; i++) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+ row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+ row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
+ }
+ // Add some current downloads (no timestamp)
+ for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ String id = Integer.toString(i);
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+ row.add(Document.COLUMN_DOCUMENT_ID, id);
+ currentDownloads.add(id);
+ }
+
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_DATE,
+ SortDimension.SORT_DIRECTION_DESCENDING);
+
+ DirectoryResult r = new DirectoryResult();
+ r.cursor = c;
+ r.sortModel = sortModel;
model.update(r);
String[] ids = model.getModelIds();
diff --git a/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java b/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
index 7c324e7..51b157a 100644
--- a/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
+++ b/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
@@ -28,6 +28,7 @@
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.RootCursorWrapper;
import com.android.documentsui.State;
+import com.android.documentsui.testing.SortModels;
@SmallTest
public class SectionBreakDocumentsAdapterWrapperTest extends AndroidTestCase {
@@ -76,7 +77,7 @@
}
DirectoryResult r = new DirectoryResult();
r.cursor = c;
- r.sortOrder = State.SORT_ORDER_SIZE;
+ r.sortModel = SortModels.createTestSortModel();
mModel.update(r);
assertEquals(mModel.getItemCount(), mAdapter.getItemCount());
@@ -102,7 +103,7 @@
}
DirectoryResult r = new DirectoryResult();
r.cursor = c;
- r.sortOrder = State.SORT_ORDER_SIZE;
+ r.sortModel = SortModels.createTestSortModel();
mModel.update(r);
assertEquals(mModel.getItemCount() + 1, mAdapter.getItemCount());
diff --git a/tests/src/com/android/documentsui/dirlist/TestModel.java b/tests/src/com/android/documentsui/dirlist/TestModel.java
index 2d819ff..6615b92 100644
--- a/tests/src/com/android/documentsui/dirlist/TestModel.java
+++ b/tests/src/com/android/documentsui/dirlist/TestModel.java
@@ -21,6 +21,7 @@
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.RootCursorWrapper;
+import com.android.documentsui.testing.SortModels;
import java.util.Random;
@@ -59,6 +60,7 @@
DirectoryResult r = new DirectoryResult();
r.cursor = c;
+ r.sortModel = SortModels.createTestSortModel();
update(r);
}
diff --git a/tests/src/com/android/documentsui/sorting/SortModelTest.java b/tests/src/com/android/documentsui/sorting/SortModelTest.java
new file mode 100644
index 0000000..70d01f6
--- /dev/null
+++ b/tests/src/com/android/documentsui/sorting/SortModelTest.java
@@ -0,0 +1,291 @@
+/*
+ * 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.sorting;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Parcel;
+import android.support.annotation.Nullable;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.View;
+
+import com.android.documentsui.R;
+import com.android.documentsui.sorting.SortModel.UpdateListener;
+import com.android.documentsui.sorting.SortModel.UpdateType;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SortModelTest {
+
+ private static final SortDimension DIMENSION_1 = new SortDimension.Builder()
+ .withId(1)
+ .withLabelId(R.string.sort_dimension_name)
+ .withDataType(SortDimension.DATA_TYPE_STRING)
+ .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
+ .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
+ .withVisibility(View.VISIBLE)
+ .build();
+
+ private static final SortDimension DIMENSION_2 = new SortDimension.Builder()
+ .withId(2)
+ .withLabelId(R.string.sort_dimension_date)
+ .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
+ .withDefaultSortDirection(SortDimension.SORT_DIRECTION_DESCENDING)
+ .build();
+
+ private static final SortDimension DIMENSION_3 = new SortDimension.Builder()
+ .withId(3)
+ .withLabelId(R.string.sort_dimension_size)
+ .withDataType(SortDimension.DATA_TYPE_NUMBER)
+ .withSortCapability(SortDimension.SORT_CAPABILITY_NONE)
+ .build();
+
+ private static final SortDimension[] DIMENSIONS = new SortDimension[] {
+ DIMENSION_1,
+ DIMENSION_2,
+ DIMENSION_3
+ };
+
+ private static final DummyListener DUMMY_LISTENER = new DummyListener();
+
+ private SortModel mModel;
+
+ @Before
+ public void setUp() {
+ mModel = new SortModel(Arrays.asList(DIMENSIONS));
+ mModel.addListener(DUMMY_LISTENER);
+ }
+
+ @Test
+ public void testEnabledByDefault() {
+ assertTrue(mModel.isSortEnabled());
+ }
+
+ @Test
+ public void testSizeEquals() {
+ assertEquals(DIMENSIONS.length, mModel.getSize());
+ }
+
+ @Test
+ public void testDimensionSame_getDimensionAt() {
+ for (int i = 0; i < DIMENSIONS.length; ++i) {
+ assertSame(DIMENSIONS[i], mModel.getDimensionAt(i));
+ }
+ }
+
+ @Test
+ public void testDimensionSame_getDimensionById() {
+ for (SortDimension dimension : DIMENSIONS) {
+ assertSame(dimension, mModel.getDimensionById(dimension.getId()));
+ }
+ }
+
+ @Test
+ public void testSetDimensionVisibility() {
+ assertEquals(View.VISIBLE, DIMENSION_1.getVisibility());
+
+ mModel.setDimensionVisibility(DIMENSION_1.getId(), View.GONE);
+
+ assertEquals(View.GONE, DIMENSION_1.getVisibility());
+ assertEquals(SortModel.UPDATE_TYPE_VISIBILITY, DUMMY_LISTENER.mLastUpdateType);
+ }
+
+ @Test
+ public void testNotSortedByDefault() {
+ assertEquals(SortModel.SORT_DIMENSION_ID_UNKNOWN, mModel.getSortedDimensionId());
+ }
+
+ @Test
+ public void testSortByDefault() {
+ mModel.setDefaultDimension(DIMENSION_1.getId());
+
+ SortDimension sortedDimension = getSortedDimension();
+ assertSame(DIMENSION_1, sortedDimension);
+ assertEquals(DIMENSION_1.getDefaultSortDirection(), sortedDimension.getSortDirection());
+
+ assertSame(mModel, DUMMY_LISTENER.mLastSortModel);
+ assertEquals(SortModel.UPDATE_TYPE_SORTING, DUMMY_LISTENER.mLastUpdateType);
+ }
+
+ @Test
+ public void testSortByUser() {
+ mModel.sortByUser(DIMENSION_1.getId(), SortDimension.SORT_DIRECTION_DESCENDING);
+
+ SortDimension sortedDimension = getSortedDimension();
+ assertSame(DIMENSION_1, sortedDimension);
+ assertEquals(SortDimension.SORT_DIRECTION_DESCENDING, sortedDimension.getSortDirection());
+
+ assertSame(mModel, DUMMY_LISTENER.mLastSortModel);
+ assertEquals(SortModel.UPDATE_TYPE_SORTING, DUMMY_LISTENER.mLastUpdateType);
+ }
+
+ @Test
+ public void testOrderNotChanged_sortByDefaultAfterSortByUser() {
+ mModel.sortByUser(DIMENSION_1.getId(), SortDimension.SORT_DIRECTION_DESCENDING);
+ mModel.setDefaultDimension(DIMENSION_2.getId());
+
+ SortDimension sortedDimension = getSortedDimension();
+ assertSame(DIMENSION_1, sortedDimension);
+ assertEquals(SortDimension.SORT_DIRECTION_DESCENDING, sortedDimension.getSortDirection());
+
+ assertSame(mModel, DUMMY_LISTENER.mLastSortModel);
+ assertEquals(SortModel.UPDATE_TYPE_SORTING, DUMMY_LISTENER.mLastUpdateType);
+ }
+
+ @Test
+ public void testOrderChanged_sortByUserAfterSortByDefault() {
+ mModel.setDefaultDimension(DIMENSION_2.getId());
+ mModel.sortByUser(DIMENSION_1.getId(), SortDimension.SORT_DIRECTION_DESCENDING);
+
+ SortDimension sortedDimension = getSortedDimension();
+ assertSame(DIMENSION_1, sortedDimension);
+ assertEquals(SortDimension.SORT_DIRECTION_DESCENDING, sortedDimension.getSortDirection());
+
+ assertSame(mModel, DUMMY_LISTENER.mLastSortModel);
+ assertEquals(SortModel.UPDATE_TYPE_SORTING, DUMMY_LISTENER.mLastUpdateType);
+ }
+
+ @Test
+ public void testSortByUserTwice() {
+ mModel.sortByUser(DIMENSION_1.getId(), SortDimension.SORT_DIRECTION_DESCENDING);
+ mModel.sortByUser(DIMENSION_2.getId(), SortDimension.SORT_DIRECTION_ASCENDING);
+
+ SortDimension sortedDimension = getSortedDimension();
+ assertSame(DIMENSION_2, sortedDimension);
+ assertEquals(SortDimension.SORT_DIRECTION_ASCENDING, sortedDimension.getSortDirection());
+
+ assertEquals(SortDimension.SORT_DIRECTION_NONE, DIMENSION_1.getSortDirection());
+ }
+
+ @Test
+ public void testSortByUserTwice_sameDimension() {
+ mModel.sortByUser(DIMENSION_1.getId(), SortDimension.SORT_DIRECTION_DESCENDING);
+ mModel.sortByUser(DIMENSION_1.getId(), SortDimension.SORT_DIRECTION_ASCENDING);
+
+ SortDimension sortedDimension = getSortedDimension();
+ assertSame(DIMENSION_1, sortedDimension);
+ assertEquals(SortDimension.SORT_DIRECTION_ASCENDING, sortedDimension.getSortDirection());
+ }
+
+ @Test
+ public void testSetSortEnabled() {
+ mModel.setSortEnabled(false);
+
+ assertFalse(mModel.isSortEnabled());
+ }
+
+ @Test
+ public void testSetDefaultDimension_sortDisabled() {
+ mModel.setSortEnabled(false);
+
+ mModel.setDefaultDimension(DIMENSION_1.getId());
+
+ SortDimension sortedDimension = getSortedDimension();
+ assertSame(DIMENSION_1, sortedDimension);
+ assertEquals(DIMENSION_1.getDefaultSortDirection(), sortedDimension.getSortDirection());
+ }
+
+ @Test
+ public void testSortByUser_sortDisabled() {
+ mModel.setSortEnabled(false);
+
+ try {
+ mModel.sortByUser(DIMENSION_1.getId(), SortDimension.SORT_DIRECTION_ASCENDING);
+ fail("Expect exception but not raised.");
+ } catch(IllegalStateException expected) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testSetDefaultDimension_noSortingCapability() {
+ try {
+ mModel.setDefaultDimension(DIMENSION_3.getId());
+ fail("Expect exception but not raised.");
+ } catch(IllegalStateException expected) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testSortByUser_noSortingCapability() {
+ try {
+ mModel.sortByUser(DIMENSION_3.getId(), SortDimension.SORT_DIRECTION_DESCENDING);
+ fail("Expect exception but not raised.");
+ } catch(IllegalStateException expected) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testParceling() {
+ mModel.setDefaultDimension(DIMENSION_1.getId());
+ mModel.sortByUser(DIMENSION_2.getId(), SortDimension.SORT_DIRECTION_DESCENDING);
+ mModel.setDimensionVisibility(DIMENSION_3.getId(), View.GONE);
+
+ Parcel write = Parcel.obtain();
+ Parcel read = Parcel.obtain();
+ final SortModel restored;
+ try {
+ mModel.writeToParcel(write, 0);
+ final byte[] data = write.marshall();
+
+ read.unmarshall(data, 0, data.length);
+ read.setDataPosition(0);
+ restored = SortModel.CREATOR.createFromParcel(read);
+ } finally {
+ write.recycle();
+ read.recycle();
+ }
+
+ assertNotNull(restored);
+ assertEquals(DIMENSIONS.length, restored.getSize());
+ for (SortDimension dimension : DIMENSIONS) {
+ assertEquals(dimension, restored.getDimensionById(dimension.getId()));
+ }
+ assertEquals(mModel.getSortedDimensionId(), restored.getSortedDimensionId());
+ }
+
+ private @Nullable SortDimension getSortedDimension() {
+ final int sortedDimensionId = mModel.getSortedDimensionId();
+ return mModel.getDimensionById(sortedDimensionId);
+ }
+
+ private static class DummyListener implements UpdateListener {
+
+ private SortModel mLastSortModel;
+ private @UpdateType int mLastUpdateType;
+
+ @Override
+ public void onModelUpdate(SortModel newModel, @UpdateType int updateType) {
+ mLastSortModel = newModel;
+ mLastUpdateType = updateType;
+ }
+ }
+}
diff --git a/tests/src/com/android/documentsui/testing/SortModels.java b/tests/src/com/android/documentsui/testing/SortModels.java
new file mode 100644
index 0000000..d7d68e7
--- /dev/null
+++ b/tests/src/com/android/documentsui/testing/SortModels.java
@@ -0,0 +1,30 @@
+/*
+ * 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.testing;
+
+import com.android.documentsui.sorting.SortModel;
+
+public final class SortModels {
+
+ private SortModels() {}
+
+ public static SortModel createTestSortModel() {
+ // We use sort model class itself for now until there is need to differentiate their
+ // behaviors.
+ return SortModel.createModel();
+ }
+}
diff --git a/tests/src/com/android/documentsui/testing/TestMenu.java b/tests/src/com/android/documentsui/testing/TestMenu.java
index a8699b9..8daad65 100644
--- a/tests/src/com/android/documentsui/testing/TestMenu.java
+++ b/tests/src/com/android/documentsui/testing/TestMenu.java
@@ -53,7 +53,6 @@
R.id.menu_grid,
R.id.menu_list,
R.id.menu_sort,
- R.id.menu_sort_size,
R.id.menu_advanced,
R.id.menu_eject_root);
}
diff --git a/tests/src/com/android/documentsui/testing/TestSearchViewManager.java b/tests/src/com/android/documentsui/testing/TestSearchViewManager.java
index 29ae3bd..a8d4ce0 100644
--- a/tests/src/com/android/documentsui/testing/TestSearchViewManager.java
+++ b/tests/src/com/android/documentsui/testing/TestSearchViewManager.java
@@ -19,6 +19,7 @@
import android.os.Bundle;
import com.android.documentsui.SearchViewManager;
+import com.android.documentsui.sorting.SortModel;
/**
* Test copy of {@link com.android.documentsui.SearchViewManager}
@@ -31,12 +32,13 @@
boolean updateMenuCalled;
boolean showMenuCalled;
- public TestSearchViewManager(SearchManagerListener listener, Bundle savedState) {
- super(listener, savedState);
+ public TestSearchViewManager(
+ SearchManagerListener listener, Bundle savedState, SortModel sortModel) {
+ super(listener, savedState, sortModel);
}
public TestSearchViewManager() {
- super(null, null);
+ super(null, null, null);
}
@Override