| /* |
| * Copyright (C) 2015 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; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.ContextWrapper; |
| import android.database.Cursor; |
| import android.database.MatrixCursor; |
| import android.provider.DocumentsContract.Document; |
| import android.test.AndroidTestCase; |
| import android.test.mock.MockContentResolver; |
| import android.test.suitebuilder.annotation.SmallTest; |
| |
| 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 java.util.ArrayList; |
| import java.util.BitSet; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Random; |
| import java.util.Set; |
| |
| @SmallTest |
| public class ModelTest extends AndroidTestCase { |
| |
| private static final int ITEM_COUNT = 10; |
| private static final String AUTHORITY = "test_authority"; |
| |
| private static final String[] COLUMNS = new String[]{ |
| RootCursorWrapper.COLUMN_AUTHORITY, |
| Document.COLUMN_DOCUMENT_ID, |
| Document.COLUMN_FLAGS, |
| Document.COLUMN_DISPLAY_NAME, |
| Document.COLUMN_SIZE, |
| Document.COLUMN_LAST_MODIFIED, |
| Document.COLUMN_MIME_TYPE |
| }; |
| |
| private static final String[] NAMES = new String[] { |
| "4", |
| "foo", |
| "1", |
| "bar", |
| "*(Ljifl;a", |
| "0", |
| "baz", |
| "2", |
| "3", |
| "%$%VD" |
| }; |
| |
| private Cursor cursor; |
| private Context context; |
| private Model model; |
| private TestContentProvider provider; |
| |
| public void setUp() { |
| setupTestContext(); |
| |
| Random rand = new Random(); |
| |
| 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_FLAGS, Document.FLAG_SUPPORTS_DELETE); |
| // Generate random document names and sizes. This forces the model's internal sort code |
| // to actually do something. |
| row.add(Document.COLUMN_DISPLAY_NAME, NAMES[i]); |
| row.add(Document.COLUMN_SIZE, rand.nextInt()); |
| } |
| cursor = c; |
| |
| DirectoryResult r = new DirectoryResult(); |
| r.cursor = cursor; |
| |
| // Instantiate the model with a dummy view adapter and listener that (for now) do nothing. |
| model = new Model(); |
| model.addUpdateListener(new DummyListener()); |
| model.update(r); |
| } |
| |
| // Tests that the model is properly emptied out after a null update. |
| public void testNullUpdate() { |
| model.update(null); |
| |
| assertTrue(model.isEmpty()); |
| assertEquals(0, model.getItemCount()); |
| assertEquals(0, model.getModelIds().length); |
| } |
| |
| // Tests that the item count is correct. |
| public void testItemCount() { |
| assertEquals(ITEM_COUNT, model.getItemCount()); |
| } |
| |
| // Tests multiple authorities with clashing document IDs. |
| public void testModelIdIsUnique() { |
| MatrixCursor cIn = new MatrixCursor(COLUMNS); |
| |
| // Make two sets of items with the same IDs, under different authorities. |
| final String AUTHORITY0 = "auth0"; |
| final String AUTHORITY1 = "auth1"; |
| for (int i = 0; i < ITEM_COUNT; ++i) { |
| MatrixCursor.RowBuilder row0 = cIn.newRow(); |
| row0.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY0); |
| row0.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); |
| |
| MatrixCursor.RowBuilder row1 = cIn.newRow(); |
| row1.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY1); |
| row1.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); |
| } |
| |
| // Update the model, then make sure it contains all the expected items. |
| DirectoryResult r = new DirectoryResult(); |
| r.cursor = cIn; |
| model.update(r); |
| |
| assertEquals(ITEM_COUNT * 2, model.getItemCount()); |
| BitSet b0 = new BitSet(ITEM_COUNT); |
| BitSet b1 = new BitSet(ITEM_COUNT); |
| |
| for (String id: model.getModelIds()) { |
| Cursor cOut = model.getItem(id); |
| String authority = |
| DocumentInfo.getCursorString(cOut, RootCursorWrapper.COLUMN_AUTHORITY); |
| String docId = DocumentInfo.getCursorString(cOut, Document.COLUMN_DOCUMENT_ID); |
| |
| switch (authority) { |
| case AUTHORITY0: |
| b0.set(Integer.parseInt(docId)); |
| break; |
| case AUTHORITY1: |
| b1.set(Integer.parseInt(docId)); |
| break; |
| default: |
| fail("Unrecognized authority string"); |
| } |
| } |
| |
| assertEquals(ITEM_COUNT, b0.cardinality()); |
| assertEquals(ITEM_COUNT, b1.cardinality()); |
| } |
| |
| // Tests the base case for Model.getItem. |
| public void testGetItem() { |
| String[] ids = model.getModelIds(); |
| assertEquals(ITEM_COUNT, ids.length); |
| for (int i = 0; i < ITEM_COUNT; ++i) { |
| Cursor c = model.getItem(ids[i]); |
| assertEquals(i, c.getPosition()); |
| } |
| } |
| |
| // Tests sorting by item name. |
| public void testSort_names() { |
| BitSet seen = new BitSet(ITEM_COUNT); |
| List<String> names = new ArrayList<>(); |
| |
| DirectoryResult r = new DirectoryResult(); |
| r.cursor = cursor; |
| r.sortOrder = State.SORT_ORDER_DISPLAY_NAME; |
| 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() { |
| 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()); |
| // 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() { |
| 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()); |
| } |
| |
| public void testSort_time() { |
| 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); |
| } |
| |
| DirectoryResult r = new DirectoryResult(); |
| r.cursor = c; |
| r.sortOrder = State.SORT_ORDER_LAST_MODIFIED; |
| 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 top. |
| for (int i = 0; i < DL_COUNT; i++) { |
| assertTrue(currentDownloads.contains(ids[i])); |
| } |
| } |
| |
| private void setupTestContext() { |
| final MockContentResolver resolver = new MockContentResolver(); |
| context = new ContextWrapper(getContext()) { |
| @Override |
| public ContentResolver getContentResolver() { |
| return resolver; |
| } |
| }; |
| provider = new TestContentProvider(); |
| resolver.addProvider(AUTHORITY, provider); |
| } |
| |
| private Selection positionToSelection(int... positions) { |
| String[] ids = model.getModelIds(); |
| Selection s = new Selection(); |
| // Construct a selection of the given positions. |
| for (int p: positions) { |
| s.add(ids[p]); |
| } |
| return s; |
| } |
| |
| private static class DummyListener implements Model.UpdateListener { |
| public void onModelUpdate(Model model) {} |
| public void onModelUpdateFailed(Exception e) {} |
| } |
| } |