Loading src/com/android/documentsui/dirlist/DirectoryFragment.java +109 −11 Original line number Diff line number Diff line Loading @@ -593,6 +593,7 @@ public class DirectoryFragment extends Fragment { thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width); if (mGridLayout == null) { mGridLayout = new GridLayoutManager(getContext(), mColumnCount); mGridLayout.setSpanSizeLookup(mAdapter.createSpanSizeLookup()); } layout = mGridLayout; break; Loading Loading @@ -1000,39 +1001,81 @@ public class DirectoryFragment extends Fragment { extends RecyclerView.Adapter<DocumentHolder> implements Model.UpdateListener { static private final String TAG = "DocumentsAdapter"; private static final String TAG = "DocumentsAdapter"; private static final int ITEM_TYPE_LAYOUT_DIVIDER = 0; private static final int ITEM_TYPE_DOCUMENT = 1; private static final int ITEM_TYPE_DIRECTORY = 2; private final Context mContext; /** * An ordered list of model IDs. This is the data structure that determines what shows up in * the UI, and where. */ private List<String> mModelIds = new ArrayList<>(); // The list is divided into two segments - directories, and everything else. Record the // position where the transition happens. private int mDividerPosition; public DocumentsAdapter(Context context) { mContext = context; } public GridLayoutManager.SpanSizeLookup createSpanSizeLookup() { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { // Make layout whitespace span the grid. This has the effect of breaking // grid rows whenever layout whitespace is encountered. if (getItemViewType(position) == ITEM_TYPE_LAYOUT_DIVIDER) { return mColumnCount; } else { return 1; } } }; } @Override public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) { View item = null; switch (viewType) { case ITEM_TYPE_DIRECTORY: case ITEM_TYPE_DOCUMENT: item = createItemView(parent); break; case ITEM_TYPE_LAYOUT_DIVIDER: item = createLayoutWhitespace(); break; } DocumentHolder holder = new DocumentHolder(item); holder.addClickListener(mItemClickListener); holder.addOnKeyListener(mSelectionManager); return holder; } private View createItemView(ViewGroup parent) { final State state = getDisplayState(); final LayoutInflater inflater = LayoutInflater.from(getContext()); View item = null; switch (state.derivedMode) { case MODE_GRID: item = inflater.inflate(R.layout.item_doc_grid, parent, false); break; return inflater.inflate(R.layout.item_doc_grid, parent, false); case MODE_LIST: item = inflater.inflate(R.layout.item_doc_list, parent, false); break; return inflater.inflate(R.layout.item_doc_list, parent, false); case MODE_UNKNOWN: default: throw new IllegalStateException("Unsupported layout mode."); } } DocumentHolder holder = new DocumentHolder(item); holder.addClickListener(mItemClickListener); holder.addOnKeyListener(mSelectionManager); return holder; private View createLayoutWhitespace() { View whitespace = new View(getContext()); whitespace.setVisibility(View.GONE); return whitespace; } /** Loading @@ -1042,6 +1085,11 @@ public class DirectoryFragment extends Fragment { */ @Override public void onBindViewHolder(DocumentHolder holder, int position, List<Object> payload) { if (holder.getItemViewType() == ITEM_TYPE_LAYOUT_DIVIDER) { // Whitespace items are hidden elements with no data to bind. return; } final View itemView = holder.itemView; if (payload.contains(MultiSelectManager.SELECTION_CHANGED_MARKER)) { Loading @@ -1055,6 +1103,11 @@ public class DirectoryFragment extends Fragment { @Override public void onBindViewHolder(DocumentHolder holder, int position) { if (holder.getItemViewType() == ITEM_TYPE_LAYOUT_DIVIDER) { // Whitespace items are hidden elements with no data to bind. return; } final Context context = getContext(); final State state = getDisplayState(); final RootsCache roots = DocumentsApplication.getRootsCache(context); Loading Loading @@ -1225,6 +1278,23 @@ public class DirectoryFragment extends Fragment { @Override public void onModelUpdate(Model model) { mModelIds = Lists.newArrayList(model.getModelIds()); mDividerPosition = 0; // Walk down the list of IDs till we encounter something that's not a directory, and // insert a whitespace element - this introduces a visual break in the grid between // folders and documents. // TODO: This code makes assumptions about the model, namely, that it performs a // bucketed sort where directories will always be ordered before other files. CBB. for (int i = 0; i < mModelIds.size(); ++i) { final String mimeType = getCursorString( model.getItem(mModelIds.get(i)), Document.COLUMN_MIME_TYPE); if (!Document.MIME_TYPE_DIR.equals(mimeType)) { mDividerPosition = i; break; } } mModelIds.add(mDividerPosition, null); } @Override Loading Loading @@ -1289,6 +1359,34 @@ public class DirectoryFragment extends Fragment { public List<String> getModelIds() { return mModelIds; } @Override public int getItemViewType(int position) { if (position < mDividerPosition) { return ITEM_TYPE_DIRECTORY; } else if (position == mDividerPosition) { return ITEM_TYPE_LAYOUT_DIVIDER; } else { return ITEM_TYPE_DOCUMENT; } } /** * Triggers item-change notifications by stable ID. Passing an unrecognized ID will result * in a warning in logcat, but no other error. * * @param id * @param selectionChangedMarker */ public void notifyItemChanged(String id, String selectionChangedMarker) { int position = mModelIds.indexOf(id); if (position >= 0) { notifyItemChanged(position, selectionChangedMarker); } else { Log.w(TAG, "Item change notification received for unknown item: " + id); } } } private static String formatTime(Context context, long when) { Loading src/com/android/documentsui/dirlist/Model.java +64 −38 Original line number Diff line number Diff line Loading @@ -152,17 +152,11 @@ public class Model implements SiblingProvider { private void updateModelData() { int[] positions = new int[mCursorCount]; mIds.clear(); String[] strings = null; long[] longs = null; String[] stringValues = new String[mCursorCount]; long[] longValues = null; switch (mSortOrder) { case SORT_ORDER_DISPLAY_NAME: strings = new String[mCursorCount]; break; case SORT_ORDER_LAST_MODIFIED: case SORT_ORDER_SIZE: longs = new long[mCursorCount]; break; if (mSortOrder == SORT_ORDER_LAST_MODIFIED || mSortOrder == SORT_ORDER_SIZE) { longValues = new long[mCursorCount]; } mCursor.moveToPosition(-1); Loading @@ -177,27 +171,29 @@ public class Model implements SiblingProvider { final String displayName = getCursorString( mCursor, Document.COLUMN_DISPLAY_NAME); if (Document.MIME_TYPE_DIR.equals(mimeType)) { strings[pos] = DocumentInfo.DIR_PREFIX + displayName; stringValues[pos] = DocumentInfo.DIR_PREFIX + displayName; } else { strings[pos] = displayName; stringValues[pos] = displayName; } break; case SORT_ORDER_LAST_MODIFIED: longs[pos] = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED); longValues[pos] = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED); stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE); break; case SORT_ORDER_SIZE: longs[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE); longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE); stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE); break; } } switch (mSortOrder) { case SORT_ORDER_DISPLAY_NAME: binarySort(positions, strings, mIds); binarySort(stringValues, positions, mIds); break; case SORT_ORDER_LAST_MODIFIED: case SORT_ORDER_SIZE: binarySort(positions, longs, mIds); binarySort(longValues, stringValues, positions, mIds); break; } Loading @@ -209,13 +205,19 @@ public class Model implements SiblingProvider { } /** * Borrowed from TimSort.binarySort(), but modified to sort three-column data set. * Sorts model data. Takes three columns of index-corresponded data. The first column is the * sort key. Rows are sorted in ascending alphabetical order on the sort key. This code is based * on TimSort.binarySort(). * * @param sortKey Data is sorted in ascending alphabetical order. * @param positions Cursor positions to be sorted. * @param ids Model IDs to be sorted. */ private static void binarySort(int[] positions, String[] strings, List<String> ids) { private static void binarySort(String[] sortKey, int[] positions, List<String> ids) { final int count = positions.length; for (int start = 1; start < count; start++) { final int pivotPosition = positions[start]; final String pivotValue = strings[start]; final String pivotValue = sortKey[start]; final String pivotId = ids.get(start); int left = 0; Loading @@ -225,7 +227,7 @@ public class Model implements SiblingProvider { int mid = (left + right) >>> 1; final String lhs = pivotValue; final String rhs = strings[mid]; final String rhs = sortKey[mid]; final int compare = DocumentInfo.compareToIgnoreCaseNullable(lhs, rhs); if (compare < 0) { Loading @@ -239,48 +241,68 @@ public class Model implements SiblingProvider { switch (n) { case 2: positions[left + 2] = positions[left + 1]; strings[left + 2] = strings[left + 1]; sortKey[left + 2] = sortKey[left + 1]; ids.set(left + 2, ids.get(left + 1)); case 1: positions[left + 1] = positions[left]; strings[left + 1] = strings[left]; sortKey[left + 1] = sortKey[left]; ids.set(left + 1, ids.get(left)); break; default: System.arraycopy(positions, left, positions, left + 1, n); System.arraycopy(strings, left, strings, left + 1, n); System.arraycopy(sortKey, left, sortKey, left + 1, n); for (int i = n; i >= 1; --i) { ids.set(left + i, ids.get(left + i - 1)); } } positions[left] = pivotPosition; strings[left] = pivotValue; sortKey[left] = pivotValue; ids.set(left, pivotId); } } /** * Borrowed from TimSort.binarySort(), but modified to sort three-column data set. * Sorts model data. Takes four columns of index-corresponded data. The first column is the sort * key, and the second is an array of mime types. The rows are first bucketed by mime type * (directories vs documents) and then each bucket is sorted independently in descending * numerical order on the sort key. This code is based on TimSort.binarySort(). * * @param sortKey Data is sorted in descending numerical order. * @param mimeTypes Corresponding mime types. Directories will be sorted ahead of documents. * @param positions Cursor positions to be sorted. * @param ids Model IDs to be sorted. */ private static void binarySort(int[] positions, long[] longs, List<String> ids) { private static void binarySort( long[] sortKey, String[] mimeTypes, int[] positions, List<String> ids) { final int count = positions.length; for (int start = 1; start < count; start++) { final int pivotPosition = positions[start]; final long pivotValue = longs[start]; final long pivotValue = sortKey[start]; final String pivotMime = mimeTypes[start]; final String pivotId = ids.get(start); int left = 0; int right = start; while (left < right) { int mid = (left + right) >>> 1; int mid = ((left + right) >>> 1); // First bucket by mime type. Directories always go in front. int compare = 0; final boolean lhsIsDir = Document.MIME_TYPE_DIR.equals(pivotMime); final boolean rhsIsDir = Document.MIME_TYPE_DIR.equals(mimeTypes[mid]); if (lhsIsDir && !rhsIsDir) { compare = -1; } else if (!lhsIsDir && rhsIsDir) { compare = 1; } else { final long lhs = pivotValue; final long rhs = longs[mid]; final long rhs = sortKey[mid]; // Sort in descending numerical order. This matches legacy behaviour, which yields // largest or most recent items on top. final int compare = -Long.compare(lhs, rhs); compare = -Long.compare(lhs, rhs); } if (compare < 0) { right = mid; Loading @@ -293,23 +315,27 @@ public class Model implements SiblingProvider { switch (n) { case 2: positions[left + 2] = positions[left + 1]; longs[left + 2] = longs[left + 1]; sortKey[left + 2] = sortKey[left + 1]; mimeTypes[left + 2] = mimeTypes[left + 1]; ids.set(left + 2, ids.get(left + 1)); case 1: positions[left + 1] = positions[left]; longs[left + 1] = longs[left]; sortKey[left + 1] = sortKey[left]; mimeTypes[left + 1] = mimeTypes[left]; ids.set(left + 1, ids.get(left)); break; default: System.arraycopy(positions, left, positions, left + 1, n); System.arraycopy(longs, left, longs, left + 1, n); System.arraycopy(sortKey, left, sortKey, left + 1, n); System.arraycopy(mimeTypes, left, mimeTypes, left + 1, n); for (int i = n; i >= 1; --i) { ids.set(left + i, ids.get(left + i - 1)); } } positions[left] = pivotPosition; longs[left] = pivotValue; sortKey[left] = pivotValue; mimeTypes[left] = pivotMime; ids.set(left, pivotId); } } Loading src/com/android/documentsui/dirlist/MultiSelectManager.java +4 −17 Original line number Diff line number Diff line Loading @@ -462,7 +462,7 @@ public final class MultiSelectManager implements View.OnKeyListener { for (int i = lastListener; i > -1; i--) { mCallbacks.get(i).onItemStateChanged(id, selected); } mEnvironment.notifyItemChanged(id, SELECTION_CHANGED_MARKER); mEnvironment.notifyItemChanged(id); } /** Loading Loading @@ -803,7 +803,7 @@ public final class MultiSelectManager implements View.OnKeyListener { String getModelIdFromAdapterPosition(int position); int getItemCount(); List<String> getModelIds(); void notifyItemChanged(String id, String selectionChangedMarker); void notifyItemChanged(String id); void registerDataObserver(RecyclerView.AdapterDataObserver observer); } Loading Loading @@ -959,21 +959,8 @@ public final class MultiSelectManager implements View.OnKeyListener { } @Override public void notifyItemChanged(String id, String selectionChangedMarker) { // TODO: This could be optimized if we turned on RecyclerView stable IDs. int count = mAdapter.getItemCount(); for (int i = 0; i < count; ++i) { RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(i); // If the view isn't bound, this code never runs because the viewholder won't be // found. That's fine, though, because the only time a view needs to be updated is // when it's bound. if (vh instanceof DirectoryFragment.DocumentHolder) { if (((DirectoryFragment.DocumentHolder) vh).modelId.equals(id)) { mAdapter.notifyItemChanged(i, SELECTION_CHANGED_MARKER); } } } public void notifyItemChanged(String id) { mAdapter.notifyItemChanged(id, SELECTION_CHANGED_MARKER); } @Override Loading tests/src/com/android/documentsui/dirlist/ModelTest.java +58 −10 Original line number Diff line number Diff line Loading @@ -54,7 +54,8 @@ public class ModelTest extends AndroidTestCase { Document.COLUMN_DOCUMENT_ID, Document.COLUMN_FLAGS, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_SIZE Document.COLUMN_SIZE, Document.COLUMN_MIME_TYPE }; private static Cursor cursor; Loading Loading @@ -197,27 +198,74 @@ public class ModelTest extends AndroidTestCase { // Tests sorting by item size. public void testSort_sizes() { BitSet seen = new BitSet(ITEM_COUNT); List<Integer> sizes = new ArrayList<>(); DirectoryResult r = new DirectoryResult(); r.cursor = cursor; r.sortOrder = State.SORT_ORDER_SIZE; model.update(r); BitSet seen = new BitSet(ITEM_COUNT); int previousSize = Integer.MAX_VALUE; for (String id: model.getModelIds()) { Cursor c = model.getItem(id); seen.set(c.getPosition()); sizes.add(DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE)); // 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()); for (int i = 0; i < sizes.size()-1; ++i) { // Note: sizes are sorted descending. assertTrue(sizes.get(i) >= sizes.get(i+1)); } // Tests that directories and files are properly bucketed when sorting by size public void testSort_sizesWithBucketing() { 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); } DirectoryResult r = new DirectoryResult(); r.cursor = c; r.sortOrder = State.SORT_ORDER_SIZE; model.update(r); boolean seenAllDirs = false; int previousSize = Integer.MAX_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.MAX_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 Model.delete works correctly. public void testDelete() throws Exception { Loading tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java +1 −1 Original line number Diff line number Diff line Loading @@ -339,7 +339,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { } @Override public void notifyItemChanged(String id, String selectionChangedMarker) { public void notifyItemChanged(String id) { throw new UnsupportedOperationException(); } Loading Loading
src/com/android/documentsui/dirlist/DirectoryFragment.java +109 −11 Original line number Diff line number Diff line Loading @@ -593,6 +593,7 @@ public class DirectoryFragment extends Fragment { thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width); if (mGridLayout == null) { mGridLayout = new GridLayoutManager(getContext(), mColumnCount); mGridLayout.setSpanSizeLookup(mAdapter.createSpanSizeLookup()); } layout = mGridLayout; break; Loading Loading @@ -1000,39 +1001,81 @@ public class DirectoryFragment extends Fragment { extends RecyclerView.Adapter<DocumentHolder> implements Model.UpdateListener { static private final String TAG = "DocumentsAdapter"; private static final String TAG = "DocumentsAdapter"; private static final int ITEM_TYPE_LAYOUT_DIVIDER = 0; private static final int ITEM_TYPE_DOCUMENT = 1; private static final int ITEM_TYPE_DIRECTORY = 2; private final Context mContext; /** * An ordered list of model IDs. This is the data structure that determines what shows up in * the UI, and where. */ private List<String> mModelIds = new ArrayList<>(); // The list is divided into two segments - directories, and everything else. Record the // position where the transition happens. private int mDividerPosition; public DocumentsAdapter(Context context) { mContext = context; } public GridLayoutManager.SpanSizeLookup createSpanSizeLookup() { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { // Make layout whitespace span the grid. This has the effect of breaking // grid rows whenever layout whitespace is encountered. if (getItemViewType(position) == ITEM_TYPE_LAYOUT_DIVIDER) { return mColumnCount; } else { return 1; } } }; } @Override public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) { View item = null; switch (viewType) { case ITEM_TYPE_DIRECTORY: case ITEM_TYPE_DOCUMENT: item = createItemView(parent); break; case ITEM_TYPE_LAYOUT_DIVIDER: item = createLayoutWhitespace(); break; } DocumentHolder holder = new DocumentHolder(item); holder.addClickListener(mItemClickListener); holder.addOnKeyListener(mSelectionManager); return holder; } private View createItemView(ViewGroup parent) { final State state = getDisplayState(); final LayoutInflater inflater = LayoutInflater.from(getContext()); View item = null; switch (state.derivedMode) { case MODE_GRID: item = inflater.inflate(R.layout.item_doc_grid, parent, false); break; return inflater.inflate(R.layout.item_doc_grid, parent, false); case MODE_LIST: item = inflater.inflate(R.layout.item_doc_list, parent, false); break; return inflater.inflate(R.layout.item_doc_list, parent, false); case MODE_UNKNOWN: default: throw new IllegalStateException("Unsupported layout mode."); } } DocumentHolder holder = new DocumentHolder(item); holder.addClickListener(mItemClickListener); holder.addOnKeyListener(mSelectionManager); return holder; private View createLayoutWhitespace() { View whitespace = new View(getContext()); whitespace.setVisibility(View.GONE); return whitespace; } /** Loading @@ -1042,6 +1085,11 @@ public class DirectoryFragment extends Fragment { */ @Override public void onBindViewHolder(DocumentHolder holder, int position, List<Object> payload) { if (holder.getItemViewType() == ITEM_TYPE_LAYOUT_DIVIDER) { // Whitespace items are hidden elements with no data to bind. return; } final View itemView = holder.itemView; if (payload.contains(MultiSelectManager.SELECTION_CHANGED_MARKER)) { Loading @@ -1055,6 +1103,11 @@ public class DirectoryFragment extends Fragment { @Override public void onBindViewHolder(DocumentHolder holder, int position) { if (holder.getItemViewType() == ITEM_TYPE_LAYOUT_DIVIDER) { // Whitespace items are hidden elements with no data to bind. return; } final Context context = getContext(); final State state = getDisplayState(); final RootsCache roots = DocumentsApplication.getRootsCache(context); Loading Loading @@ -1225,6 +1278,23 @@ public class DirectoryFragment extends Fragment { @Override public void onModelUpdate(Model model) { mModelIds = Lists.newArrayList(model.getModelIds()); mDividerPosition = 0; // Walk down the list of IDs till we encounter something that's not a directory, and // insert a whitespace element - this introduces a visual break in the grid between // folders and documents. // TODO: This code makes assumptions about the model, namely, that it performs a // bucketed sort where directories will always be ordered before other files. CBB. for (int i = 0; i < mModelIds.size(); ++i) { final String mimeType = getCursorString( model.getItem(mModelIds.get(i)), Document.COLUMN_MIME_TYPE); if (!Document.MIME_TYPE_DIR.equals(mimeType)) { mDividerPosition = i; break; } } mModelIds.add(mDividerPosition, null); } @Override Loading Loading @@ -1289,6 +1359,34 @@ public class DirectoryFragment extends Fragment { public List<String> getModelIds() { return mModelIds; } @Override public int getItemViewType(int position) { if (position < mDividerPosition) { return ITEM_TYPE_DIRECTORY; } else if (position == mDividerPosition) { return ITEM_TYPE_LAYOUT_DIVIDER; } else { return ITEM_TYPE_DOCUMENT; } } /** * Triggers item-change notifications by stable ID. Passing an unrecognized ID will result * in a warning in logcat, but no other error. * * @param id * @param selectionChangedMarker */ public void notifyItemChanged(String id, String selectionChangedMarker) { int position = mModelIds.indexOf(id); if (position >= 0) { notifyItemChanged(position, selectionChangedMarker); } else { Log.w(TAG, "Item change notification received for unknown item: " + id); } } } private static String formatTime(Context context, long when) { Loading
src/com/android/documentsui/dirlist/Model.java +64 −38 Original line number Diff line number Diff line Loading @@ -152,17 +152,11 @@ public class Model implements SiblingProvider { private void updateModelData() { int[] positions = new int[mCursorCount]; mIds.clear(); String[] strings = null; long[] longs = null; String[] stringValues = new String[mCursorCount]; long[] longValues = null; switch (mSortOrder) { case SORT_ORDER_DISPLAY_NAME: strings = new String[mCursorCount]; break; case SORT_ORDER_LAST_MODIFIED: case SORT_ORDER_SIZE: longs = new long[mCursorCount]; break; if (mSortOrder == SORT_ORDER_LAST_MODIFIED || mSortOrder == SORT_ORDER_SIZE) { longValues = new long[mCursorCount]; } mCursor.moveToPosition(-1); Loading @@ -177,27 +171,29 @@ public class Model implements SiblingProvider { final String displayName = getCursorString( mCursor, Document.COLUMN_DISPLAY_NAME); if (Document.MIME_TYPE_DIR.equals(mimeType)) { strings[pos] = DocumentInfo.DIR_PREFIX + displayName; stringValues[pos] = DocumentInfo.DIR_PREFIX + displayName; } else { strings[pos] = displayName; stringValues[pos] = displayName; } break; case SORT_ORDER_LAST_MODIFIED: longs[pos] = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED); longValues[pos] = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED); stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE); break; case SORT_ORDER_SIZE: longs[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE); longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE); stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE); break; } } switch (mSortOrder) { case SORT_ORDER_DISPLAY_NAME: binarySort(positions, strings, mIds); binarySort(stringValues, positions, mIds); break; case SORT_ORDER_LAST_MODIFIED: case SORT_ORDER_SIZE: binarySort(positions, longs, mIds); binarySort(longValues, stringValues, positions, mIds); break; } Loading @@ -209,13 +205,19 @@ public class Model implements SiblingProvider { } /** * Borrowed from TimSort.binarySort(), but modified to sort three-column data set. * Sorts model data. Takes three columns of index-corresponded data. The first column is the * sort key. Rows are sorted in ascending alphabetical order on the sort key. This code is based * on TimSort.binarySort(). * * @param sortKey Data is sorted in ascending alphabetical order. * @param positions Cursor positions to be sorted. * @param ids Model IDs to be sorted. */ private static void binarySort(int[] positions, String[] strings, List<String> ids) { private static void binarySort(String[] sortKey, int[] positions, List<String> ids) { final int count = positions.length; for (int start = 1; start < count; start++) { final int pivotPosition = positions[start]; final String pivotValue = strings[start]; final String pivotValue = sortKey[start]; final String pivotId = ids.get(start); int left = 0; Loading @@ -225,7 +227,7 @@ public class Model implements SiblingProvider { int mid = (left + right) >>> 1; final String lhs = pivotValue; final String rhs = strings[mid]; final String rhs = sortKey[mid]; final int compare = DocumentInfo.compareToIgnoreCaseNullable(lhs, rhs); if (compare < 0) { Loading @@ -239,48 +241,68 @@ public class Model implements SiblingProvider { switch (n) { case 2: positions[left + 2] = positions[left + 1]; strings[left + 2] = strings[left + 1]; sortKey[left + 2] = sortKey[left + 1]; ids.set(left + 2, ids.get(left + 1)); case 1: positions[left + 1] = positions[left]; strings[left + 1] = strings[left]; sortKey[left + 1] = sortKey[left]; ids.set(left + 1, ids.get(left)); break; default: System.arraycopy(positions, left, positions, left + 1, n); System.arraycopy(strings, left, strings, left + 1, n); System.arraycopy(sortKey, left, sortKey, left + 1, n); for (int i = n; i >= 1; --i) { ids.set(left + i, ids.get(left + i - 1)); } } positions[left] = pivotPosition; strings[left] = pivotValue; sortKey[left] = pivotValue; ids.set(left, pivotId); } } /** * Borrowed from TimSort.binarySort(), but modified to sort three-column data set. * Sorts model data. Takes four columns of index-corresponded data. The first column is the sort * key, and the second is an array of mime types. The rows are first bucketed by mime type * (directories vs documents) and then each bucket is sorted independently in descending * numerical order on the sort key. This code is based on TimSort.binarySort(). * * @param sortKey Data is sorted in descending numerical order. * @param mimeTypes Corresponding mime types. Directories will be sorted ahead of documents. * @param positions Cursor positions to be sorted. * @param ids Model IDs to be sorted. */ private static void binarySort(int[] positions, long[] longs, List<String> ids) { private static void binarySort( long[] sortKey, String[] mimeTypes, int[] positions, List<String> ids) { final int count = positions.length; for (int start = 1; start < count; start++) { final int pivotPosition = positions[start]; final long pivotValue = longs[start]; final long pivotValue = sortKey[start]; final String pivotMime = mimeTypes[start]; final String pivotId = ids.get(start); int left = 0; int right = start; while (left < right) { int mid = (left + right) >>> 1; int mid = ((left + right) >>> 1); // First bucket by mime type. Directories always go in front. int compare = 0; final boolean lhsIsDir = Document.MIME_TYPE_DIR.equals(pivotMime); final boolean rhsIsDir = Document.MIME_TYPE_DIR.equals(mimeTypes[mid]); if (lhsIsDir && !rhsIsDir) { compare = -1; } else if (!lhsIsDir && rhsIsDir) { compare = 1; } else { final long lhs = pivotValue; final long rhs = longs[mid]; final long rhs = sortKey[mid]; // Sort in descending numerical order. This matches legacy behaviour, which yields // largest or most recent items on top. final int compare = -Long.compare(lhs, rhs); compare = -Long.compare(lhs, rhs); } if (compare < 0) { right = mid; Loading @@ -293,23 +315,27 @@ public class Model implements SiblingProvider { switch (n) { case 2: positions[left + 2] = positions[left + 1]; longs[left + 2] = longs[left + 1]; sortKey[left + 2] = sortKey[left + 1]; mimeTypes[left + 2] = mimeTypes[left + 1]; ids.set(left + 2, ids.get(left + 1)); case 1: positions[left + 1] = positions[left]; longs[left + 1] = longs[left]; sortKey[left + 1] = sortKey[left]; mimeTypes[left + 1] = mimeTypes[left]; ids.set(left + 1, ids.get(left)); break; default: System.arraycopy(positions, left, positions, left + 1, n); System.arraycopy(longs, left, longs, left + 1, n); System.arraycopy(sortKey, left, sortKey, left + 1, n); System.arraycopy(mimeTypes, left, mimeTypes, left + 1, n); for (int i = n; i >= 1; --i) { ids.set(left + i, ids.get(left + i - 1)); } } positions[left] = pivotPosition; longs[left] = pivotValue; sortKey[left] = pivotValue; mimeTypes[left] = pivotMime; ids.set(left, pivotId); } } Loading
src/com/android/documentsui/dirlist/MultiSelectManager.java +4 −17 Original line number Diff line number Diff line Loading @@ -462,7 +462,7 @@ public final class MultiSelectManager implements View.OnKeyListener { for (int i = lastListener; i > -1; i--) { mCallbacks.get(i).onItemStateChanged(id, selected); } mEnvironment.notifyItemChanged(id, SELECTION_CHANGED_MARKER); mEnvironment.notifyItemChanged(id); } /** Loading Loading @@ -803,7 +803,7 @@ public final class MultiSelectManager implements View.OnKeyListener { String getModelIdFromAdapterPosition(int position); int getItemCount(); List<String> getModelIds(); void notifyItemChanged(String id, String selectionChangedMarker); void notifyItemChanged(String id); void registerDataObserver(RecyclerView.AdapterDataObserver observer); } Loading Loading @@ -959,21 +959,8 @@ public final class MultiSelectManager implements View.OnKeyListener { } @Override public void notifyItemChanged(String id, String selectionChangedMarker) { // TODO: This could be optimized if we turned on RecyclerView stable IDs. int count = mAdapter.getItemCount(); for (int i = 0; i < count; ++i) { RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(i); // If the view isn't bound, this code never runs because the viewholder won't be // found. That's fine, though, because the only time a view needs to be updated is // when it's bound. if (vh instanceof DirectoryFragment.DocumentHolder) { if (((DirectoryFragment.DocumentHolder) vh).modelId.equals(id)) { mAdapter.notifyItemChanged(i, SELECTION_CHANGED_MARKER); } } } public void notifyItemChanged(String id) { mAdapter.notifyItemChanged(id, SELECTION_CHANGED_MARKER); } @Override Loading
tests/src/com/android/documentsui/dirlist/ModelTest.java +58 −10 Original line number Diff line number Diff line Loading @@ -54,7 +54,8 @@ public class ModelTest extends AndroidTestCase { Document.COLUMN_DOCUMENT_ID, Document.COLUMN_FLAGS, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_SIZE Document.COLUMN_SIZE, Document.COLUMN_MIME_TYPE }; private static Cursor cursor; Loading Loading @@ -197,27 +198,74 @@ public class ModelTest extends AndroidTestCase { // Tests sorting by item size. public void testSort_sizes() { BitSet seen = new BitSet(ITEM_COUNT); List<Integer> sizes = new ArrayList<>(); DirectoryResult r = new DirectoryResult(); r.cursor = cursor; r.sortOrder = State.SORT_ORDER_SIZE; model.update(r); BitSet seen = new BitSet(ITEM_COUNT); int previousSize = Integer.MAX_VALUE; for (String id: model.getModelIds()) { Cursor c = model.getItem(id); seen.set(c.getPosition()); sizes.add(DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE)); // 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()); for (int i = 0; i < sizes.size()-1; ++i) { // Note: sizes are sorted descending. assertTrue(sizes.get(i) >= sizes.get(i+1)); } // Tests that directories and files are properly bucketed when sorting by size public void testSort_sizesWithBucketing() { 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); } DirectoryResult r = new DirectoryResult(); r.cursor = c; r.sortOrder = State.SORT_ORDER_SIZE; model.update(r); boolean seenAllDirs = false; int previousSize = Integer.MAX_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.MAX_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 Model.delete works correctly. public void testDelete() throws Exception { Loading
tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java +1 −1 Original line number Diff line number Diff line Loading @@ -339,7 +339,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { } @Override public void notifyItemChanged(String id, String selectionChangedMarker) { public void notifyItemChanged(String id) { throw new UnsupportedOperationException(); } Loading