Merge "[multi-part] Enable bidirectional sorting in DocumentsUI" into nyc-andromeda-dev
diff --git a/res/animator/arrow_rotate_down.xml b/res/animator/arrow_rotate_down.xml
new file mode 100644
index 0000000..a37cb27
--- /dev/null
+++ b/res/animator/arrow_rotate_down.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:propertyName="level"
+ android:valueFrom="0"
+ android:valueTo="10000"
+ android:valueType="intType"
+ android:duration="200" />
diff --git a/res/animator/arrow_rotate_up.xml b/res/animator/arrow_rotate_up.xml
new file mode 100644
index 0000000..a04076b
--- /dev/null
+++ b/res/animator/arrow_rotate_up.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:propertyName="level"
+ android:valueFrom="10000"
+ android:valueTo="0"
+ android:valueType="intType"
+ android:duration="200" />
diff --git a/res/drawable/ic_arrow_upward.xml b/res/drawable/ic_arrow_upward.xml
new file mode 100644
index 0000000..4ce8342
--- /dev/null
+++ b/res/drawable/ic_arrow_upward.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#000000"
+ android:pathData="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_sort_arrow.xml b/res/drawable/ic_sort_arrow.xml
new file mode 100644
index 0000000..b4b92a5
--- /dev/null
+++ b/res/drawable/ic_sort_arrow.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/ic_arrow_upward"
+ android:fromDegrees="0"
+ android:toDegrees="180"
+ android:pivotX="50%"
+ android:pivotY="50%"/>
diff --git a/res/layout-sw720dp-land/column_headers.xml b/res/layout-sw720dp-land/column_headers.xml
new file mode 100644
index 0000000..5d982d4
--- /dev/null
+++ b/res/layout-sw720dp-land/column_headers.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/table_header"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/doc_header_height"
+ android:background="@color/item_doc_background"
+ android:visibility="gone">
+ <!-- Placeholder for focus indicator -->
+ <View
+ android:layout_width="4dp"
+ android:layout_height="match_parent"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:baselineAligned="false"
+ android:gravity="center_vertical"
+ android:minHeight="@dimen/list_item_height"
+ android:paddingStart="@dimen/list_item_padding"
+ android:paddingEnd="@dimen/list_item_padding"
+ android:orientation="horizontal">
+ <!-- Placeholder for icon -->
+ <View
+ android:layout_width="@dimen/list_item_thumbnail_size"
+ android:layout_height="@dimen/list_item_thumbnail_size"
+ android:layout_gravity="center_vertical"
+ android:layout_marginEnd="16dp"
+ android:layout_marginStart="0dp"/>
+
+ <!-- Column headers -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+
+ <com.android.documentsui.dirlist.header.HeaderCell
+ android:id="@android:id/title"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="0.5"
+ android:layout_marginEnd="12dp"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:animateLayoutChanges="true">
+
+ <include layout="@layout/shared_cell_content" />
+ </com.android.documentsui.dirlist.header.HeaderCell>
+
+ <com.android.documentsui.dirlist.header.HeaderCell
+ android:id="@android:id/summary"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="0.25"
+ android:layout_marginEnd="12dp"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:animateLayoutChanges="true">
+
+ <include layout="@layout/shared_cell_content" />
+ </com.android.documentsui.dirlist.header.HeaderCell>
+
+ <com.android.documentsui.dirlist.header.HeaderCell
+ android:id="@+id/size"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="0.125"
+ android:layout_marginEnd="12dp"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:animateLayoutChanges="true">
+
+ <include layout="@layout/shared_cell_content" />
+ </com.android.documentsui.dirlist.header.HeaderCell>
+
+ <com.android.documentsui.dirlist.header.HeaderCell
+ android:id="@+id/date"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="0.125"
+ android:layout_marginEnd="12dp"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:animateLayoutChanges="true">
+
+ <include layout="@layout/shared_cell_content" />
+ </com.android.documentsui.dirlist.header.HeaderCell>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-sw720dp-land/shared_cell_content.xml b/res/layout-sw720dp-land/shared_cell_content.xml
new file mode 100644
index 0000000..387fe4c
--- /dev/null
+++ b/res/layout-sw720dp-land/shared_cell_content.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <TextView
+ android:id="@+id/label"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/android:TextAppearance.Material.Subhead"
+ android:textColor="?android:attr/textColorSecondary"/>
+
+ <ImageView
+ android:id="@+id/sort_arrow"
+ android:layout_height="@dimen/doc_header_sort_icon_size"
+ android:layout_width="@dimen/doc_header_sort_icon_size"
+ android:layout_marginStart="3dp"
+ android:visibility="gone"
+ android:src="@drawable/ic_sort_arrow"
+ android:contentDescription="@null"/>
+</merge>
diff --git a/res/layout/column_headers.xml b/res/layout/column_headers.xml
new file mode 100644
index 0000000..bee2212
--- /dev/null
+++ b/res/layout/column_headers.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<!-- A placeholder of table header on small screens. This won't inflate any view when it's included
+ into other layouts. -->
+<merge />
diff --git a/res/layout/fragment_directory.xml b/res/layout/fragment_directory.xml
index 1c086a6..25d13f2 100644
--- a/res/layout/fragment_directory.xml
+++ b/res/layout/fragment_directory.xml
@@ -89,18 +89,28 @@
</LinearLayout>
</FrameLayout>
- <android.support.v7.widget.RecyclerView
- android:id="@+id/dir_list"
- android:scrollbars="vertical"
+ <LinearLayout
+ android:id="@+id/file_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:paddingStart="0dp"
- android:paddingEnd="0dp"
- android:paddingTop="0dp"
- android:paddingBottom="0dp"
- android:clipToPadding="false"
- android:scrollbarStyle="outsideOverlay"
- android:drawSelectorOnTop="true"/>
+ android:orientation="vertical">
+
+ <!-- column headers are empty on small screens or portrait mode. -->
+ <include layout="@layout/column_headers" />
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/dir_list"
+ android:scrollbars="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingStart="0dp"
+ android:paddingEnd="0dp"
+ android:paddingTop="0dp"
+ android:paddingBottom="0dp"
+ android:clipToPadding="false"
+ android:scrollbarStyle="outsideOverlay"
+ android:drawSelectorOnTop="true"/>
+ </LinearLayout>
</FrameLayout>
</com.android.documentsui.dirlist.TouchSwipeRefreshLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 7cda341..ec03ba1 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -48,4 +48,6 @@
<dimen name="autoscroll_edge_height">32dp</dimen>
+ <dimen name="doc_header_sort_icon_size">16dp</dimen>
+ <dimen name="doc_header_height">60dp</dimen>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 416bb6f..1a8df54 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -90,6 +90,20 @@
<!-- Mode that sorts documents by their file size in descending order; largest first [CHAR LIMIT=24] -->
<string name="sort_size">By size</string>
+ <!-- Table header for file name [CHAR_LIMIT=24] -->
+ <string name="column_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>
+
+ <!-- content description to describe ascending sorting used with upward arrow in table header. -->
+ <string name="sort_direction_ascending">Ascending</string>
+ <!-- content description to describe descending sorting used with downward arrow in table header. -->
+ <string name="sort_direction_descending">Descending</string>
+
<!-- Accessibility title to open the drawer showing all roots where documents can be stored [CHAR LIMIT=32] -->
<string name="drawer_open">Show roots</string>
<!-- Accessibility title to close the drawer showing all roots where documents can be stored [CHAR LIMIT=32] -->
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index d8d8d3e..e9b1743 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -48,6 +48,7 @@
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
+import android.view.View;
import com.android.documentsui.MenuManager.DirectoryDetails;
import com.android.documentsui.NavigationViewManager.Breadcrumb;
@@ -62,6 +63,8 @@
import com.android.documentsui.model.RootInfo;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperations;
+import com.android.documentsui.sorting.SortController;
+import com.android.documentsui.sorting.SortModel;
import java.io.FileNotFoundException;
import java.util.ArrayList;
@@ -123,6 +126,8 @@
private boolean mNavDrawerHasFocus;
private long mStartTime;
+ private SortController mSortController;
+
public abstract void onDocumentPicked(DocumentInfo doc, Model model);
public abstract void onDocumentsPicked(List<DocumentInfo> docs);
public abstract FragmentTuner createFragmentTuner();
@@ -177,6 +182,8 @@
mNavigator = new NavigationViewManager(mDrawer, toolbar, mState, this, breadcrumb);
+ mSortController = new SortController(mState.sortModel);
+
// Base classes must update result in their onCreate.
setResult(Activity.RESULT_CANCELED);
}
@@ -207,6 +214,10 @@
super.onDestroy();
}
+ SortController getSortController() {
+ return mSortController;
+ }
+
private State getState(@Nullable Bundle icicle) {
if (icicle != null) {
State state = icicle.<State>getParcelable(Shared.EXTRA_STATE);
@@ -218,9 +229,10 @@
final Intent intent = getIntent();
+ state.sortModel = SortModel.createModel();
state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
state.forceSize = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_FILESIZE, false);
- state.showSize = state.forceSize || LocalPreferences.getDisplayFileSize(this);
+ state.setShowSize(state.forceSize || LocalPreferences.getDisplayFileSize(this));
state.initAcceptMimes(intent);
state.excludedAuthorities = getExcludedAuthorities();
@@ -260,6 +272,12 @@
mState.derivedMode = LocalPreferences.getViewMode(this, root, MODE_GRID);
+ // Set summary header's visibility. Only recents and downloads root may have summary in
+ // their docs.
+ mState.sortModel.setDimensionVisibility(
+ SortModel.SORT_DIMENSION_ID_SUMMARY,
+ root.isRecents() || root.isDownloads() ? View.VISIBLE : View.INVISIBLE);
+
// Clear entire backstack and start in new root
mState.onRootChanged(root);
@@ -503,7 +521,7 @@
display ? Metrics.USER_ACTION_SHOW_SIZE : Metrics.USER_ACTION_HIDE_SIZE);
LocalPreferences.setDisplayFileSize(this, display);
- mState.showSize = display;
+ mState.setShowSize(display);
DirectoryFragment dir = getDirectoryFragment();
if (dir != null) {
dir.onDisplayStateChanged();
@@ -555,6 +573,8 @@
if (dir != null) {
dir.onViewModeChanged();
}
+
+ mSortController.onViewModeChanged(mode);
}
public void setPending(boolean pending) {
diff --git a/src/com/android/documentsui/DocumentsActivity.java b/src/com/android/documentsui/DocumentsActivity.java
index 05f36e8..017f7ae 100644
--- a/src/com/android/documentsui/DocumentsActivity.java
+++ b/src/com/android/documentsui/DocumentsActivity.java
@@ -54,6 +54,7 @@
import com.android.documentsui.model.DurableUtils;
import com.android.documentsui.model.RootInfo;
import com.android.documentsui.services.FileOperationService;
+import com.android.documentsui.sorting.SortController;
import libcore.io.IoUtils;
@@ -405,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());
+ return new DocumentsTuner(this, getDisplayState(), getSortController());
}
@Override
diff --git a/src/com/android/documentsui/FilesActivity.java b/src/com/android/documentsui/FilesActivity.java
index 54f3e61..2f7f093 100644
--- a/src/com/android/documentsui/FilesActivity.java
+++ b/src/com/android/documentsui/FilesActivity.java
@@ -472,12 +472,12 @@
@Override
public FragmentTuner createFragmentTuner() {
- return new FilesTuner(this, getDisplayState());
+ return new FilesTuner(this, getDisplayState(), getSortController());
}
@Override
public MenuManager getMenuManager() {
- return mMenuManager;
+ return mMenuManager;
}
@Override
diff --git a/src/com/android/documentsui/MenuManager.java b/src/com/android/documentsui/MenuManager.java
index a23203b..0edd18e 100644
--- a/src/com/android/documentsui/MenuManager.java
+++ b/src/com/android/documentsui/MenuManager.java
@@ -17,7 +17,6 @@
package com.android.documentsui;
import android.annotation.Nullable;
-import android.provider.DocumentsContract.Root;
import android.view.Menu;
import android.view.MenuItem;
@@ -120,7 +119,7 @@
// Search uses backend ranking; no sorting, recents doesn't support sort.
sort.setEnabled(!directoryDetails.isInRecents() && !mSearchManager.isSearching());
sort.setVisible(true);
- sortSize.setVisible(mState.showSize); // Only sort by size when file sizes are visible
+ sortSize.setVisible(mState.getShowSize()); // Only sort by size when file sizes are visible
}
void updateAdvanced(MenuItem advanced, DirectoryDetails directoryDetails) {
diff --git a/src/com/android/documentsui/State.java b/src/com/android/documentsui/State.java
index 9cdf1a8..2ee8961 100644
--- a/src/com/android/documentsui/State.java
+++ b/src/com/android/documentsui/State.java
@@ -24,7 +24,9 @@
import android.os.Parcelable;
import android.util.Log;
import android.util.SparseArray;
+import android.view.View;
+import com.android.documentsui.sorting.SortModel;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DurableUtils;
@@ -83,6 +85,8 @@
/** Derived from local preferences */
public @ViewMode int derivedMode = MODE_GRID;
+ /** Current sort state */
+ public SortModel sortModel;
/** Explicit user choice */
public int userSortOrder = SORT_ORDER_UNKNOWN;
/** Derived after loader */
@@ -90,7 +94,6 @@
public boolean allowMultiple;
public boolean forceSize;
- public boolean showSize;
public boolean localOnly;
public boolean showAdvancedOption;
public boolean showAdvanced;
@@ -119,6 +122,8 @@
private boolean mInitialRootChanged;
private boolean mInitialDocChanged;
+ private boolean mShowSize;
+
/** Instance state for every shown directory */
public HashMap<String, SparseArray<Parcelable>> dirState = new HashMap<>();
@@ -165,6 +170,16 @@
mStackTouched = true;
}
+ public boolean getShowSize() {
+ return forceSize || mShowSize;
+ }
+
+ public void setShowSize(boolean display) {
+ mShowSize = display;
+ sortModel.setDimensionVisibility(
+ SortModel.SORT_DIMENSION_ID_SIZE, getShowSize() ? View.VISIBLE : View.GONE);
+ }
+
// This will return true even when the initial location is set.
// To get a read on if the user has changed something, use #hasInitialLocationChanged.
public boolean hasLocationChanged() {
@@ -187,7 +202,7 @@
out.writeInt(userSortOrder);
out.writeInt(allowMultiple ? 1 : 0);
out.writeInt(forceSize ? 1 : 0);
- out.writeInt(showSize ? 1 : 0);
+ out.writeInt(mShowSize ? 1 : 0);
out.writeInt(localOnly ? 1 : 0);
out.writeInt(showAdvancedOption ? 1 : 0);
out.writeInt(showAdvanced ? 1 : 0);
@@ -200,6 +215,7 @@
out.writeInt(mStackTouched ? 1 : 0);
out.writeInt(mInitialRootChanged ? 1 : 0);
out.writeInt(mInitialDocChanged ? 1 : 0);
+ out.writeParcelable(sortModel, 0);
}
public static final ClassLoaderCreator<State> CREATOR = new ClassLoaderCreator<State>() {
@@ -216,7 +232,7 @@
state.userSortOrder = in.readInt();
state.allowMultiple = in.readInt() != 0;
state.forceSize = in.readInt() != 0;
- state.showSize = in.readInt() != 0;
+ state.mShowSize = in.readInt() != 0;
state.localOnly = in.readInt() != 0;
state.showAdvancedOption = in.readInt() != 0;
state.showAdvanced = in.readInt() != 0;
@@ -229,6 +245,7 @@
state.mStackTouched = in.readInt() != 0;
state.mInitialRootChanged = in.readInt() != 0;
state.mInitialDocChanged = in.readInt() != 0;
+ state.sortModel = in.readParcelable(getClass().getClassLoader());
return state;
}
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index e6e1048..983d82f 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -98,12 +98,14 @@
import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.dirlist.UserInputHandler.DocumentDetails;
+import com.android.documentsui.dirlist.header.TableHeaderController;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;
import com.android.documentsui.services.FileOperation;
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 java.io.IOException;
import java.lang.annotation.Retention;
@@ -157,6 +159,7 @@
private SwipeRefreshLayout mRefreshLayout;
private View mEmptyView;
private RecyclerView mRecView;
+ private View mFileList;
private ListeningGestureDetector mGestureDetector;
private String mStateKey;
@@ -191,6 +194,8 @@
private DragScrollListener mOnDragListener;
private MenuManager mMenuManager;
+ private TableHeaderController mTableHeaderController;
+
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -212,8 +217,8 @@
cancelThumbnailTask(holder.itemView);
}
});
-
mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
+ mFileList = view.findViewById(R.id.file_list);
final int edgeHeight = (int) getResources().getDimension(R.dimen.autoscroll_edge_height);
mOnDragListener = DragScrollListener.create(
@@ -223,6 +228,8 @@
mRecView.setOnDragListener(mOnDragListener);
mEmptyView.setOnDragListener(mOnDragListener);
+ mTableHeaderController = TableHeaderController.create(view.findViewById(R.id.table_header));
+
return view;
}
@@ -337,6 +344,8 @@
boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
mIconHelper.setThumbnailsEnabled(!svelte);
+ mTuner.mSortController.manage(mTableHeaderController, getDisplayState().derivedMode);
+
// Kick off loader at least once
getLoaderManager().restartLoader(LOADER_ID, null, this);
}
@@ -1154,12 +1163,12 @@
mEmptyView.setVisibility(View.VISIBLE);
mEmptyView.requestFocus();
- mRecView.setVisibility(View.GONE);
+ mFileList.setVisibility(View.GONE);
}
private void showDirectory() {
mEmptyView.setVisibility(View.GONE);
- mRecView.setVisibility(View.VISIBLE);
+ mFileList.setVisibility(View.VISIBLE);
mRecView.requestFocus();
}
diff --git a/src/com/android/documentsui/dirlist/FragmentTuner.java b/src/com/android/documentsui/dirlist/FragmentTuner.java
index 5201089..8dfd490 100644
--- a/src/com/android/documentsui/dirlist/FragmentTuner.java
+++ b/src/com/android/documentsui/dirlist/FragmentTuner.java
@@ -29,6 +29,7 @@
import com.android.documentsui.MimePredicate;
import com.android.documentsui.State;
import com.android.documentsui.dirlist.DirectoryFragment.ResultType;
+import com.android.documentsui.sorting.SortController;
/**
* Providers support for specializing the DirectoryFragment to the "host" Activity.
@@ -38,10 +39,12 @@
final Context mContext;
final State mState;
+ final SortController mSortController;
- public FragmentTuner(Context context, State state) {
+ public FragmentTuner(Context context, State state, SortController sortController) {
mContext = context;
mState = state;
+ mSortController = sortController;
}
// Subtly different from isDocumentEnabled. The reason may be illuminated as follows.
@@ -81,8 +84,8 @@
// open the drawer on empty directories on first launch
private boolean mModelPreviousLoaded;
- public DocumentsTuner(Context context, State state) {
- super(context, state);
+ public DocumentsTuner(Context context, State state, SortController sortController) {
+ super(context, state, sortController);
}
@Override
@@ -167,12 +170,10 @@
// open the drawer on empty directories on first launch
private boolean mModelPreviousLoaded;
- public FilesTuner(Context context, State state) {
- super(context, state);
+ public FilesTuner(Context context, State state, SortController sortController) {
+ super(context, state, sortController);
}
-
-
@Override
void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch) {
// When launched into empty root, open drawer.
diff --git a/src/com/android/documentsui/dirlist/GridDocumentHolder.java b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
index 7ba4bdd..e9f22f9 100644
--- a/src/com/android/documentsui/dirlist/GridDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
@@ -151,7 +151,7 @@
mDate.setText(Shared.formatTime(mContext, docLastModified));
}
- if (!state.showSize || Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
+ if (!state.getShowSize() || Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
mSize.setVisibility(View.GONE);
} else {
mSize.setVisibility(View.VISIBLE);
diff --git a/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
index e88be0c..746a44d 100644
--- a/src/com/android/documentsui/dirlist/ListDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
@@ -160,7 +160,7 @@
mDate.setText(null);
}
- if (state.showSize && docSize > -1) {
+ if (state.getShowSize() && docSize > -1) {
hasDetails = true;
mSize.setVisibility(View.VISIBLE);
mSize.setText(Formatter.formatFileSize(mContext, docSize));
diff --git a/src/com/android/documentsui/dirlist/header/HeaderCell.java b/src/com/android/documentsui/dirlist/header/HeaderCell.java
new file mode 100644
index 0000000..f5a04f0
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/header/HeaderCell.java
@@ -0,0 +1,125 @@
+/*
+ * 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.dirlist.header;
+
+import android.animation.AnimatorInflater;
+import android.animation.LayoutTransition;
+import android.animation.ObjectAnimator;
+import android.annotation.AnimatorRes;
+import android.annotation.StringRes;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.documentsui.R;
+import com.android.documentsui.sorting.SortDimension;
+
+/**
+ * A clickable, sortable table header cell layout.
+ *
+ * It updates its display when it binds to {@link SortDimension} and changes the status of sorting
+ * when it's clicked.
+ */
+public class HeaderCell extends LinearLayout {
+
+ private static final long ANIMATION_DURATION = 100;
+
+ private @SortDimension.SortDirection int mCurDirection = SortDimension.SORT_DIRECTION_NONE;
+
+ public HeaderCell(Context context) {
+ this(context, null);
+ }
+
+ public HeaderCell(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ LayoutTransition transition = getLayoutTransition();
+ transition.setDuration(ANIMATION_DURATION);
+ transition.setStartDelay(LayoutTransition.CHANGE_APPEARING, 0);
+ transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
+ transition.setStartDelay(LayoutTransition.CHANGING, 0);
+ }
+
+ void onBind(SortDimension dimension) {
+ setVisibility(dimension.getVisibility());
+
+ if (dimension.getVisibility() == View.VISIBLE) {
+ TextView label = (TextView) findViewById(R.id.label);
+ label.setText(dimension.getLabelId());
+ switch (dimension.getDataType()) {
+ case SortDimension.DATA_TYPE_NUMBER:
+ setDataTypeNumber(label);
+ break;
+ case SortDimension.DATA_TYPE_STRING:
+ setDataTypeString(label);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown column data type: " + dimension.getDataType() + ".");
+ }
+
+ if (mCurDirection != dimension.getSortDirection()) {
+ ImageView arrow = (ImageView) findViewById(R.id.sort_arrow);
+ switch (dimension.getSortDirection()) {
+ case SortDimension.SORT_DIRECTION_NONE:
+ arrow.setVisibility(View.GONE);
+ break;
+ case SortDimension.SORT_DIRECTION_ASCENDING:
+ showArrow(arrow, R.animator.arrow_rotate_up,
+ R.string.sort_direction_ascending);
+ break;
+ case SortDimension.SORT_DIRECTION_DESCENDING:
+ showArrow(arrow, R.animator.arrow_rotate_down,
+ R.string.sort_direction_descending);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown sort direction: " + dimension.getSortDirection() + ".");
+ }
+
+ mCurDirection = dimension.getSortDirection();
+ }
+ }
+ }
+
+ private void showArrow(
+ ImageView arrow, @AnimatorRes int anim, @StringRes int contentDescriptionId) {
+ arrow.setVisibility(View.VISIBLE);
+
+ CharSequence description = getContext().getString(contentDescriptionId);
+ arrow.setContentDescription(description);
+
+ ObjectAnimator animator =
+ (ObjectAnimator) AnimatorInflater.loadAnimator(getContext(), anim);
+ animator.setTarget(arrow.getDrawable());
+ animator.start();
+ }
+
+ private void setDataTypeNumber(View label) {
+ label.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
+ setGravity(Gravity.CENTER_VERTICAL | Gravity.END);
+ }
+
+ private void setDataTypeString(View label) {
+ label.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
+ }
+}
diff --git a/src/com/android/documentsui/dirlist/header/TableHeaderController.java b/src/com/android/documentsui/dirlist/header/TableHeaderController.java
new file mode 100644
index 0000000..8a0074a
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/header/TableHeaderController.java
@@ -0,0 +1,104 @@
+/*
+ * 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.dirlist.header;
+
+import android.annotation.Nullable;
+import android.view.View;
+
+import com.android.documentsui.R;
+import com.android.documentsui.sorting.SortDimension;
+import com.android.documentsui.sorting.SortModel;
+import com.android.documentsui.sorting.SortModel.SortDimensionId;
+import com.android.documentsui.sorting.SortController;
+
+/**
+ * View controller for table header that associates header cells in table header and columns.
+ */
+public final class TableHeaderController implements SortController.WidgetController {
+ private View mTableHeader;
+
+ private final HeaderCell mTitleCell;
+ private final HeaderCell mSummaryCell;
+ private final HeaderCell mSizeCell;
+ private final HeaderCell mDateCell;
+
+ private final SortModel.UpdateListener mModelUpdaterListener = this::onModelUpdate;
+ private final View.OnClickListener mOnCellClickListener = this::onCellClicked;
+
+ private SortModel mModel;
+
+ public static @Nullable TableHeaderController create(@Nullable View tableHeader) {
+ return (tableHeader == null) ? null : new TableHeaderController(tableHeader);
+ }
+
+ private TableHeaderController(View tableHeader) {
+ 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);
+ }
+
+ @Override
+ public void setModel(@Nullable SortModel model) {
+ if (mModel != null) {
+ mModel.removeListener(mModelUpdaterListener);
+ }
+
+ mModel = model;
+
+ if (mModel != null) {
+ onModelUpdate(mModel);
+
+ mModel.addListener(mModelUpdaterListener);
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mTableHeader.setVisibility(visibility);
+ }
+
+ private void onModelUpdate(SortModel model) {
+ 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) {
+ SortDimension dimension = mModel.getDimensionById(id);
+
+ cell.setTag(dimension);
+
+ cell.onBind(dimension);
+ if (mModel.isSortEnabled()
+ && dimension.getVisibility() == View.VISIBLE
+ && dimension.getSortCapability() != SortDimension.SORT_CAPABILITY_NONE) {
+ cell.setOnClickListener(mOnCellClickListener);
+ } else {
+ cell.setOnClickListener(null);
+ }
+ }
+
+ private void onCellClicked(View v) {
+ SortDimension dimension = (SortDimension) v.getTag();
+
+ mModel.sortBy(dimension.getId(), dimension.getNextDirection());
+ }
+}
diff --git a/src/com/android/documentsui/sorting/SortController.java b/src/com/android/documentsui/sorting/SortController.java
new file mode 100644
index 0000000..630b4ef
--- /dev/null
+++ b/src/com/android/documentsui/sorting/SortController.java
@@ -0,0 +1,78 @@
+/*
+ * 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.annotation.Nullable;
+import android.view.View;
+
+import com.android.documentsui.State;
+import com.android.documentsui.dirlist.header.TableHeaderController;
+
+/**
+ * A high level controller that manages sort widgets. This is useful when sort widgets can and will
+ * 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 {
+
+ private static final WidgetController DUMMY_CONTROLLER = new WidgetController() {};
+
+ private final SortModel mModel;
+ private WidgetController mTableHeaderController = DUMMY_CONTROLLER;
+
+ public SortController(SortModel model) {
+ mModel = model;
+ }
+
+ public void manage(
+ @Nullable TableHeaderController tableHeaderController, @State.ViewMode int mode) {
+ if (tableHeaderController == null) {
+ return;
+ }
+
+ mTableHeaderController = tableHeaderController;
+ mTableHeaderController.setModel(mModel);
+
+ setVisibilityPerViewMode(mTableHeaderController, mode, View.GONE, View.VISIBLE);
+ }
+
+ public void onViewModeChanged(@State.ViewMode int mode) {
+ setVisibilityPerViewMode(mTableHeaderController, mode, View.GONE, View.VISIBLE);
+ }
+
+ private static void setVisibilityPerViewMode(
+ WidgetController controller,
+ @State.ViewMode int mode,
+ int visibilityInGrid,
+ int visibilityInList) {
+ switch (mode) {
+ case State.MODE_GRID:
+ controller.setVisibility(visibilityInGrid);
+ break;
+ case State.MODE_LIST:
+ controller.setVisibility(visibilityInList);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown view mode: " + mode + ".");
+ }
+ }
+
+ 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
new file mode 100644
index 0000000..6881c97
--- /dev/null
+++ b/src/com/android/documentsui/sorting/SortDimension.java
@@ -0,0 +1,217 @@
+/*
+ * 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.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.StringRes;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A model class that describes a sort dimension and its sort state.
+ */
+public class SortDimension implements Parcelable {
+
+ /**
+ * This enum is defined as flag because it's also used to denote whether a column can be sorted
+ * in a certain direction.
+ */
+ @IntDef(flag = true, value = {
+ SORT_DIRECTION_NONE,
+ SORT_DIRECTION_ASCENDING,
+ SORT_DIRECTION_DESCENDING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SortDirection {}
+ public static final int SORT_DIRECTION_NONE = 0;
+ public static final int SORT_DIRECTION_ASCENDING = 1;
+ public static final int SORT_DIRECTION_DESCENDING = 2;
+
+ @IntDef({
+ SORT_CAPABILITY_NONE,
+ SORT_CAPABILITY_BOTH_DIRECTION
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface SortCapability {}
+ public static final int SORT_CAPABILITY_NONE = 0;
+ public static final int SORT_CAPABILITY_BOTH_DIRECTION =
+ SORT_DIRECTION_ASCENDING | SORT_DIRECTION_DESCENDING;
+
+ @IntDef({
+ DATA_TYPE_STRING,
+ DATA_TYPE_NUMBER
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DataType {}
+ public static final int DATA_TYPE_STRING = 0;
+ public static final int DATA_TYPE_NUMBER = 1;
+
+ private final int mId;
+ private final @StringRes int mLabelId;
+ private final @DataType int mDataType;
+ private final @SortCapability int mSortCapability;
+ private final @SortDirection int mDefaultSortDirection;
+
+ @SortDirection int mSortDirection = SORT_DIRECTION_NONE;
+ int mVisibility;
+
+ private SortDimension(int id, @StringRes int labelId, @DataType int dataType,
+ @SortCapability int sortCapability, @SortDirection int defaultSortDirection) {
+ mId = id;
+ mLabelId = labelId;
+ mDataType = dataType;
+ mSortCapability = sortCapability;
+ mDefaultSortDirection = defaultSortDirection;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public @StringRes int getLabelId() {
+ return mLabelId;
+ }
+
+ public @DataType int getDataType() {
+ return mDataType;
+ }
+
+ public @SortCapability int getSortCapability() {
+ return mSortCapability;
+ }
+
+ public @SortDirection int getDefaultSortDirection() {
+ return mDefaultSortDirection;
+ }
+
+ public @SortDirection int getNextDirection() {
+ @SortDimension.SortDirection int alternativeDirection =
+ (mDefaultSortDirection == SortDimension.SORT_DIRECTION_ASCENDING)
+ ? SortDimension.SORT_DIRECTION_DESCENDING
+ : SortDimension.SORT_DIRECTION_ASCENDING;
+ @SortDimension.SortDirection int direction =
+ (mSortDirection == mDefaultSortDirection)
+ ? alternativeDirection
+ : mDefaultSortDirection;
+
+ return direction;
+ }
+
+ public @SortDirection int getSortDirection() {
+ return mSortDirection;
+ }
+
+ public int getVisibility() {
+ return mVisibility;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flag) {
+ out.writeInt(mId);
+ out.writeInt(mLabelId);
+ out.writeInt(mDataType);
+ out.writeInt(mSortCapability);
+ out.writeInt(mDefaultSortDirection);
+ out.writeInt(mSortDirection);
+ out.writeInt(mVisibility);
+ }
+
+ public static Parcelable.Creator<SortDimension> CREATOR =
+ new Parcelable.Creator<SortDimension>() {
+
+ @Override
+ public SortDimension createFromParcel(Parcel in) {
+ int id = in.readInt();
+ @StringRes int lableId = in.readInt();
+ @DataType int dataType = in.readInt();
+ int sortCapability = in.readInt();
+ int defaultSortDirection = in.readInt();
+
+ SortDimension column =
+ new SortDimension(id, lableId, dataType, sortCapability, defaultSortDirection);
+
+ column.mSortDirection = in.readInt();
+ column.mVisibility = in.readInt();
+
+ return column;
+ }
+
+ @Override
+ public SortDimension[] newArray(int size) {
+ return new SortDimension[size];
+ }
+ };
+
+ static class Builder {
+ private int mId;
+ private @StringRes int mLabelId;
+ private @DataType int mDataType = DATA_TYPE_STRING;
+ private @SortCapability int mSortCapability = SORT_CAPABILITY_BOTH_DIRECTION;
+ private @SortDirection int mDefaultSortDirection = SORT_DIRECTION_ASCENDING;
+ private int mVisibility = View.VISIBLE;
+
+ Builder withId(int id) {
+ mId = id;
+ return this;
+ }
+
+ Builder withLabelId(@StringRes int labelId) {
+ mLabelId = labelId;
+ return this;
+ }
+
+ Builder withDataType(@DataType int dataType) {
+ mDataType = dataType;
+ return this;
+ }
+
+ Builder withSortCapability(@SortCapability int sortCapability) {
+ mSortCapability = sortCapability;
+ return this;
+ }
+
+ Builder withVisibility(int visibility) {
+ mVisibility = visibility;
+ return this;
+ }
+
+ Builder withDefaultSortDirection(@SortDirection int defaultSortDirection) {
+ mDefaultSortDirection = defaultSortDirection;
+ return this;
+ }
+
+ SortDimension build() {
+ if (mLabelId == 0) {
+ throw new IllegalStateException("Must set labelId.");
+ }
+
+ SortDimension dimension = new SortDimension(
+ mId, mLabelId, mDataType, mSortCapability, mDefaultSortDirection);
+ dimension.mVisibility = mVisibility;
+ return dimension;
+ }
+ }
+}
diff --git a/src/com/android/documentsui/sorting/SortModel.java b/src/com/android/documentsui/sorting/SortModel.java
new file mode 100644
index 0000000..b5067fd
--- /dev/null
+++ b/src/com/android/documentsui/sorting/SortModel.java
@@ -0,0 +1,251 @@
+/*
+ * 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.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.view.View;
+
+import com.android.documentsui.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Sort model that contains all columns and their sorting state.
+ */
+public class SortModel implements Parcelable {
+ @IntDef({
+ SORT_DIMENSION_ID_TITLE,
+ SORT_DIMENSION_ID_SUMMARY,
+ SORT_DIMENSION_ID_DATE,
+ SORT_DIMENSION_ID_SIZE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SortDimensionId {}
+ 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;
+
+ private final SparseArray<SortDimension> mDimensions;
+
+ private transient final List<UpdateListener> mListeners;
+
+ private SortDimension mSortedDimension;
+
+ private boolean mIsSortEnabled = true;
+
+ public SortModel(Collection<SortDimension> columns) {
+ mDimensions = new SparseArray<>(columns.size());
+
+ for (SortDimension column : columns) {
+ if (mDimensions.get(column.getId()) != null) {
+ throw new IllegalStateException(
+ "SortDimension id must be unique. Duplicate id: " + column.getId());
+ }
+ mDimensions.put(column.getId(), column);
+ }
+
+ mListeners = new ArrayList<>();
+ }
+
+ public int getSize() {
+ return mDimensions.size();
+ }
+
+ public SortDimension getDimensionAt(int index) {
+ return mDimensions.valueAt(index);
+ }
+
+ public SortDimension getDimensionById(int id) {
+ return mDimensions.get(id);
+ }
+
+ public SortDimension getSortedDimension() {
+ return mSortedDimension;
+ }
+
+ public void setSortEnabled(boolean enabled) {
+ if (!enabled) {
+ clearSortDirection();
+ }
+ mIsSortEnabled = enabled;
+
+ notifyListeners();
+ }
+
+ public boolean isSortEnabled() {
+ return mIsSortEnabled;
+ }
+
+ public void sortBy(int columnId, @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 newSortedDimension = mDimensions.get(columnId);
+ if ((direction & newSortedDimension.getSortCapability()) == 0) {
+ throw new IllegalStateException(
+ "SortDimension " + columnId + " can't be sorted in direction " + direction);
+ }
+ switch (direction) {
+ case SortDimension.SORT_DIRECTION_ASCENDING:
+ case SortDimension.SORT_DIRECTION_DESCENDING:
+ newSortedDimension.mSortDirection = direction;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown sort direction: " + direction);
+ }
+
+ if (mSortedDimension != null && mSortedDimension != newSortedDimension) {
+ mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
+ }
+
+ mSortedDimension = newSortedDimension;
+
+ notifyListeners();
+ }
+
+ public void setDimensionVisibility(int columnId, int visibility) {
+ assert(mDimensions.get(columnId) != null);
+
+ mDimensions.get(columnId).mVisibility = visibility;
+
+ notifyListeners();
+ }
+
+ private void notifyListeners() {
+ for (int i = mListeners.size() - 1; i >= 0; --i) {
+ mListeners.get(i).onModelUpdate(this);
+ }
+ }
+
+ public void addListener(UpdateListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void removeListener(UpdateListener listener) {
+ mListeners.remove(listener);
+ }
+
+ public void clearSortDirection() {
+ if (mSortedDimension != null) {
+ mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
+ mSortedDimension = null;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flag) {
+ out.writeInt(mDimensions.size());
+ for (int i = 0; i < mDimensions.size(); ++i) {
+ out.writeParcelable(mDimensions.valueAt(i), flag);
+ }
+ }
+
+ public static Parcelable.Creator<SortModel> CREATOR = new Parcelable.Creator<SortModel>() {
+
+ @Override
+ public SortModel createFromParcel(Parcel in) {
+ 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);
+ }
+
+ @Override
+ public SortModel[] newArray(int size) {
+ return new SortModel[size];
+ }
+ };
+
+ /**
+ * Creates a model for all other roots.
+ *
+ * TODO: move definition of columns into xml, and inflate model from it.
+ */
+ public static SortModel createModel() {
+ List<SortDimension> dimensions = new ArrayList<>(4);
+ SortDimension.Builder builder = new SortDimension.Builder();
+
+ // Name column
+ dimensions.add(builder
+ .withId(SORT_DIMENSION_ID_TITLE)
+ .withLabelId(R.string.column_name)
+ .withDataType(SortDimension.DATA_TYPE_STRING)
+ .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
+ .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
+ .withVisibility(View.VISIBLE)
+ .build()
+ );
+
+ // Summary column
+ // Summary is only visible in Downloads and Recents root.
+ dimensions.add(builder
+ .withId(SORT_DIMENSION_ID_SUMMARY)
+ .withLabelId(R.string.column_summary)
+ .withDataType(SortDimension.DATA_TYPE_STRING)
+ .withSortCapability(SortDimension.SORT_CAPABILITY_NONE)
+ .withVisibility(View.INVISIBLE)
+ .build()
+ );
+
+ // Size column
+ dimensions.add(builder
+ .withId(SORT_DIMENSION_ID_SIZE)
+ .withLabelId(R.string.column_size)
+ .withDataType(SortDimension.DATA_TYPE_NUMBER)
+ .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
+ .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
+ .withVisibility(View.VISIBLE)
+ .build()
+ );
+
+ // Date column
+ dimensions.add(builder
+ .withId(SORT_DIMENSION_ID_DATE)
+ .withLabelId(R.string.column_date)
+ .withDataType(SortDimension.DATA_TYPE_NUMBER)
+ .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
+ .withDefaultSortDirection(SortDimension.SORT_DIRECTION_DESCENDING)
+ .withVisibility(View.VISIBLE)
+ .build()
+ );
+
+ return new SortModel(dimensions);
+ }
+
+ public interface UpdateListener {
+ void onModelUpdate(SortModel newModel);
+ }
+}
diff --git a/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java b/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
index ec03173..685263d 100644
--- a/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
+++ b/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
@@ -25,6 +25,7 @@
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;
@@ -92,6 +93,7 @@
testRootInfo = new RootInfo();
state.action = ACTION_CREATE;
state.allowMultiple = true;
+ state.sortModel = SortModel.createModel();
}
@Test
@@ -141,7 +143,7 @@
@Test
public void testOptionMenu_hideSize() {
- state.showSize = true;
+ state.setShowSize(true);
DocumentsMenuManager mgr = new DocumentsMenuManager(testSearchManager, state);
mgr.updateOptionMenu(testMenu, directoryDetails);
diff --git a/tests/src/com/android/documentsui/FilesMenuManagerTest.java b/tests/src/com/android/documentsui/FilesMenuManagerTest.java
index 3644abc..fa50427 100644
--- a/tests/src/com/android/documentsui/FilesMenuManagerTest.java
+++ b/tests/src/com/android/documentsui/FilesMenuManagerTest.java
@@ -22,6 +22,7 @@
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;
@@ -90,6 +91,8 @@
directoryDetails = new TestDirectoryDetails();
testSearchManager = new TestSearchViewManager();
testRootInfo = new RootInfo();
+
+ state.sortModel = SortModel.createModel();
}
@Test
@@ -165,7 +168,7 @@
@Test
public void testOptionMenu_hideSize() {
- state.showSize = true;
+ state.setShowSize(true);
FilesMenuManager mgr = new FilesMenuManager(testSearchManager, state);
mgr.updateOptionMenu(testMenu, directoryDetails);