Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2015 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.documentsui.dirlist; |
| 18 | |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 19 | import android.content.ContentResolver; |
| 20 | import android.content.Context; |
| 21 | import android.content.ContextWrapper; |
| 22 | import android.database.Cursor; |
| 23 | import android.database.MatrixCursor; |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 24 | import android.provider.DocumentsContract.Document; |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 25 | import android.test.AndroidTestCase; |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 26 | import android.test.mock.MockContentResolver; |
| 27 | import android.test.suitebuilder.annotation.SmallTest; |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 28 | |
| 29 | import com.android.documentsui.DirectoryResult; |
| 30 | import com.android.documentsui.RootCursorWrapper; |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 31 | import com.android.documentsui.State; |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 32 | import com.android.documentsui.dirlist.MultiSelectManager.Selection; |
| 33 | import com.android.documentsui.model.DocumentInfo; |
| 34 | |
| 35 | import java.util.ArrayList; |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 36 | import java.util.BitSet; |
Ben Kwa | e2564f9 | 2016-01-08 15:34:47 -0800 | [diff] [blame] | 37 | import java.util.HashSet; |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 38 | import java.util.List; |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 39 | import java.util.Random; |
Ben Kwa | e2564f9 | 2016-01-08 15:34:47 -0800 | [diff] [blame] | 40 | import java.util.Set; |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 41 | import java.util.concurrent.CountDownLatch; |
| 42 | |
| 43 | @SmallTest |
| 44 | public class ModelTest extends AndroidTestCase { |
| 45 | |
| 46 | private static final int ITEM_COUNT = 10; |
| 47 | private static final String AUTHORITY = "test_authority"; |
Steve McKay | ef16f5f | 2015-12-22 18:15:31 -0800 | [diff] [blame] | 48 | |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 49 | private static final String[] COLUMNS = new String[]{ |
| 50 | RootCursorWrapper.COLUMN_AUTHORITY, |
| 51 | Document.COLUMN_DOCUMENT_ID, |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 52 | Document.COLUMN_FLAGS, |
| 53 | Document.COLUMN_DISPLAY_NAME, |
Ben Kwa | c72a2cb | 2015-12-16 19:42:08 -0800 | [diff] [blame] | 54 | Document.COLUMN_SIZE, |
Ben Kwa | e2564f9 | 2016-01-08 15:34:47 -0800 | [diff] [blame] | 55 | Document.COLUMN_LAST_MODIFIED, |
Ben Kwa | c72a2cb | 2015-12-16 19:42:08 -0800 | [diff] [blame] | 56 | Document.COLUMN_MIME_TYPE |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 57 | }; |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 58 | |
Steve McKay | ef16f5f | 2015-12-22 18:15:31 -0800 | [diff] [blame] | 59 | private static final String[] NAMES = new String[] { |
| 60 | "4", |
| 61 | "foo", |
| 62 | "1", |
| 63 | "bar", |
| 64 | "*(Ljifl;a", |
| 65 | "0", |
| 66 | "baz", |
| 67 | "2", |
| 68 | "3", |
| 69 | "%$%VD" |
| 70 | }; |
| 71 | |
| 72 | private Cursor cursor; |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 73 | private Context context; |
| 74 | private Model model; |
| 75 | private TestContentProvider provider; |
| 76 | |
| 77 | public void setUp() { |
| 78 | setupTestContext(); |
| 79 | |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 80 | Random rand = new Random(); |
| 81 | |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 82 | MatrixCursor c = new MatrixCursor(COLUMNS); |
| 83 | for (int i = 0; i < ITEM_COUNT; ++i) { |
| 84 | MatrixCursor.RowBuilder row = c.newRow(); |
| 85 | row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY); |
| 86 | row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); |
| 87 | row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE); |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 88 | // Generate random document names and sizes. This forces the model's internal sort code |
| 89 | // to actually do something. |
| 90 | row.add(Document.COLUMN_DISPLAY_NAME, NAMES[i]); |
| 91 | row.add(Document.COLUMN_SIZE, rand.nextInt()); |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 92 | } |
| 93 | cursor = c; |
| 94 | |
| 95 | DirectoryResult r = new DirectoryResult(); |
| 96 | r.cursor = cursor; |
| 97 | |
| 98 | // Instantiate the model with a dummy view adapter and listener that (for now) do nothing. |
Steve McKay | ef16f5f | 2015-12-22 18:15:31 -0800 | [diff] [blame] | 99 | model = new Model(context); |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 100 | model.addUpdateListener(new DummyListener()); |
| 101 | model.update(r); |
| 102 | } |
| 103 | |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 104 | // Tests that the model is properly emptied out after a null update. |
| 105 | public void testNullUpdate() { |
| 106 | model.update(null); |
| 107 | |
| 108 | assertTrue(model.isEmpty()); |
| 109 | assertEquals(0, model.getItemCount()); |
| 110 | assertEquals(0, model.getModelIds().size()); |
| 111 | } |
| 112 | |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 113 | // Tests that the item count is correct. |
| 114 | public void testItemCount() { |
| 115 | assertEquals(ITEM_COUNT, model.getItemCount()); |
| 116 | } |
| 117 | |
| 118 | // Tests multiple authorities with clashing document IDs. |
| 119 | public void testModelIdIsUnique() { |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 120 | MatrixCursor cIn = new MatrixCursor(COLUMNS); |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 121 | |
| 122 | // Make two sets of items with the same IDs, under different authorities. |
| 123 | final String AUTHORITY0 = "auth0"; |
| 124 | final String AUTHORITY1 = "auth1"; |
| 125 | for (int i = 0; i < ITEM_COUNT; ++i) { |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 126 | MatrixCursor.RowBuilder row0 = cIn.newRow(); |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 127 | row0.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY0); |
| 128 | row0.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); |
| 129 | |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 130 | MatrixCursor.RowBuilder row1 = cIn.newRow(); |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 131 | row1.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY1); |
| 132 | row1.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); |
| 133 | } |
| 134 | |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 135 | // Update the model, then make sure it contains all the expected items. |
| 136 | DirectoryResult r = new DirectoryResult(); |
| 137 | r.cursor = cIn; |
| 138 | model.update(r); |
| 139 | |
| 140 | assertEquals(ITEM_COUNT * 2, model.getItemCount()); |
| 141 | BitSet b0 = new BitSet(ITEM_COUNT); |
| 142 | BitSet b1 = new BitSet(ITEM_COUNT); |
| 143 | |
| 144 | for (String id: model.getModelIds()) { |
| 145 | Cursor cOut = model.getItem(id); |
| 146 | String authority = |
| 147 | DocumentInfo.getCursorString(cOut, RootCursorWrapper.COLUMN_AUTHORITY); |
| 148 | String docId = DocumentInfo.getCursorString(cOut, Document.COLUMN_DOCUMENT_ID); |
| 149 | |
| 150 | switch (authority) { |
| 151 | case AUTHORITY0: |
| 152 | b0.set(Integer.parseInt(docId)); |
| 153 | break; |
| 154 | case AUTHORITY1: |
| 155 | b1.set(Integer.parseInt(docId)); |
| 156 | break; |
| 157 | default: |
| 158 | fail("Unrecognized authority string"); |
| 159 | } |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 160 | } |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 161 | |
| 162 | assertEquals(ITEM_COUNT, b0.cardinality()); |
| 163 | assertEquals(ITEM_COUNT, b1.cardinality()); |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 164 | } |
| 165 | |
| 166 | // Tests the base case for Model.getItem. |
| 167 | public void testGetItem() { |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 168 | List<String> ids = model.getModelIds(); |
| 169 | assertEquals(ITEM_COUNT, ids.size()); |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 170 | for (int i = 0; i < ITEM_COUNT; ++i) { |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 171 | Cursor c = model.getItem(ids.get(i)); |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 172 | assertEquals(i, c.getPosition()); |
| 173 | } |
| 174 | } |
| 175 | |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 176 | // Tests sorting by item name. |
| 177 | public void testSort_names() { |
| 178 | BitSet seen = new BitSet(ITEM_COUNT); |
| 179 | List<String> names = new ArrayList<>(); |
| 180 | |
| 181 | DirectoryResult r = new DirectoryResult(); |
| 182 | r.cursor = cursor; |
| 183 | r.sortOrder = State.SORT_ORDER_DISPLAY_NAME; |
| 184 | model.update(r); |
| 185 | |
| 186 | for (String id: model.getModelIds()) { |
| 187 | Cursor c = model.getItem(id); |
| 188 | seen.set(c.getPosition()); |
| 189 | names.add(DocumentInfo.getCursorString(c, Document.COLUMN_DISPLAY_NAME)); |
| 190 | } |
| 191 | |
| 192 | assertEquals(ITEM_COUNT, seen.cardinality()); |
| 193 | for (int i = 0; i < names.size()-1; ++i) { |
| 194 | assertTrue(DocumentInfo.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) <= 0); |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | // Tests sorting by item size. |
| 199 | public void testSort_sizes() { |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 200 | DirectoryResult r = new DirectoryResult(); |
| 201 | r.cursor = cursor; |
| 202 | r.sortOrder = State.SORT_ORDER_SIZE; |
| 203 | model.update(r); |
| 204 | |
Ben Kwa | c72a2cb | 2015-12-16 19:42:08 -0800 | [diff] [blame] | 205 | BitSet seen = new BitSet(ITEM_COUNT); |
| 206 | int previousSize = Integer.MAX_VALUE; |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 207 | for (String id: model.getModelIds()) { |
| 208 | Cursor c = model.getItem(id); |
| 209 | seen.set(c.getPosition()); |
Ben Kwa | c72a2cb | 2015-12-16 19:42:08 -0800 | [diff] [blame] | 210 | // Check sort order - descending numerical |
| 211 | int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE); |
| 212 | assertTrue(previousSize >= size); |
| 213 | previousSize = size; |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 214 | } |
Ben Kwa | c72a2cb | 2015-12-16 19:42:08 -0800 | [diff] [blame] | 215 | // Check that all items were accounted for. |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 216 | assertEquals(ITEM_COUNT, seen.cardinality()); |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 217 | } |
| 218 | |
Ben Kwa | c72a2cb | 2015-12-16 19:42:08 -0800 | [diff] [blame] | 219 | // Tests that directories and files are properly bucketed when sorting by size |
| 220 | public void testSort_sizesWithBucketing() { |
| 221 | MatrixCursor c = new MatrixCursor(COLUMNS); |
| 222 | |
| 223 | for (int i = 0; i < ITEM_COUNT; ++i) { |
| 224 | MatrixCursor.RowBuilder row = c.newRow(); |
| 225 | row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY); |
| 226 | row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); |
| 227 | row.add(Document.COLUMN_SIZE, i); |
| 228 | // Interleave directories and text files. |
| 229 | String mimeType =(i % 2 == 0) ? Document.MIME_TYPE_DIR : "text/*"; |
| 230 | row.add(Document.COLUMN_MIME_TYPE, mimeType); |
| 231 | } |
| 232 | |
| 233 | DirectoryResult r = new DirectoryResult(); |
| 234 | r.cursor = c; |
| 235 | r.sortOrder = State.SORT_ORDER_SIZE; |
| 236 | model.update(r); |
| 237 | |
| 238 | boolean seenAllDirs = false; |
| 239 | int previousSize = Integer.MAX_VALUE; |
| 240 | BitSet seen = new BitSet(ITEM_COUNT); |
| 241 | // Iterate over items in sort order. Once we've encountered a document (i.e. not a |
| 242 | // directory), all subsequent items must also be documents. That is, all directories are |
| 243 | // bucketed at the front of the list, sorted by size, followed by documents, sorted by size. |
| 244 | for (String id: model.getModelIds()) { |
| 245 | Cursor cOut = model.getItem(id); |
| 246 | seen.set(cOut.getPosition()); |
| 247 | |
| 248 | String mimeType = DocumentInfo.getCursorString(cOut, Document.COLUMN_MIME_TYPE); |
| 249 | if (seenAllDirs) { |
| 250 | assertFalse(Document.MIME_TYPE_DIR.equals(mimeType)); |
| 251 | } else { |
| 252 | if (!Document.MIME_TYPE_DIR.equals(mimeType)) { |
| 253 | seenAllDirs = true; |
| 254 | // Reset the previous size seen, because documents are bucketed separately by |
| 255 | // the sort. |
| 256 | previousSize = Integer.MAX_VALUE; |
| 257 | } |
| 258 | } |
| 259 | // Check sort order - descending numerical |
| 260 | int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE); |
| 261 | assertTrue(previousSize >= size); |
| 262 | previousSize = size; |
| 263 | } |
| 264 | |
| 265 | // Check that all items were accounted for. |
| 266 | assertEquals(ITEM_COUNT, seen.cardinality()); |
| 267 | } |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 268 | |
Ben Kwa | e2564f9 | 2016-01-08 15:34:47 -0800 | [diff] [blame] | 269 | public void testSort_time() { |
| 270 | final int DL_COUNT = 3; |
| 271 | MatrixCursor c = new MatrixCursor(COLUMNS); |
| 272 | Set<String> currentDownloads = new HashSet<>(); |
| 273 | |
| 274 | // Add some files |
| 275 | for (int i = 0; i < ITEM_COUNT; i++) { |
| 276 | MatrixCursor.RowBuilder row = c.newRow(); |
| 277 | row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY); |
| 278 | row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); |
| 279 | row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis()); |
| 280 | } |
| 281 | // Add some current downloads (no timestamp) |
| 282 | for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) { |
| 283 | MatrixCursor.RowBuilder row = c.newRow(); |
| 284 | String id = Integer.toString(i); |
| 285 | row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY); |
| 286 | row.add(Document.COLUMN_DOCUMENT_ID, id); |
| 287 | currentDownloads.add(Model.createModelId(AUTHORITY, id)); |
| 288 | } |
| 289 | |
| 290 | DirectoryResult r = new DirectoryResult(); |
| 291 | r.cursor = c; |
| 292 | r.sortOrder = State.SORT_ORDER_LAST_MODIFIED; |
| 293 | model.update(r); |
| 294 | |
| 295 | List<String> ids = model.getModelIds(); |
| 296 | |
| 297 | // Check that all items were accounted for |
| 298 | assertEquals(ITEM_COUNT + DL_COUNT, ids.size()); |
| 299 | |
| 300 | // Check that active downloads are sorted to the top. |
| 301 | for (int i = 0; i < DL_COUNT; i++) { |
| 302 | assertTrue(currentDownloads.contains(ids.get(i))); |
| 303 | } |
| 304 | } |
| 305 | |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 306 | // Tests that Model.delete works correctly. |
| 307 | public void testDelete() throws Exception { |
| 308 | // Simulate deleting 2 files. |
| 309 | List<DocumentInfo> docsBefore = getDocumentInfo(2, 3); |
| 310 | delete(2, 3); |
| 311 | |
| 312 | provider.assertWasDeleted(docsBefore.get(0)); |
| 313 | provider.assertWasDeleted(docsBefore.get(1)); |
| 314 | } |
| 315 | |
| 316 | private void setupTestContext() { |
| 317 | final MockContentResolver resolver = new MockContentResolver(); |
| 318 | context = new ContextWrapper(getContext()) { |
| 319 | @Override |
| 320 | public ContentResolver getContentResolver() { |
| 321 | return resolver; |
| 322 | } |
| 323 | }; |
| 324 | provider = new TestContentProvider(); |
| 325 | resolver.addProvider(AUTHORITY, provider); |
| 326 | } |
| 327 | |
| 328 | private Selection positionToSelection(int... positions) { |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 329 | List<String> ids = model.getModelIds(); |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 330 | Selection s = new Selection(); |
| 331 | // Construct a selection of the given positions. |
| 332 | for (int p: positions) { |
Ben Kwa | 862b9641 | 2015-12-07 13:25:27 -0800 | [diff] [blame] | 333 | s.add(ids.get(p)); |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 334 | } |
| 335 | return s; |
| 336 | } |
| 337 | |
| 338 | private void delete(int... positions) throws InterruptedException { |
| 339 | Selection s = positionToSelection(positions); |
| 340 | final CountDownLatch latch = new CountDownLatch(1); |
| 341 | |
| 342 | model.delete( |
| 343 | s, |
| 344 | new Model.DeletionListener() { |
| 345 | @Override |
| 346 | public void onError() { |
| 347 | latch.countDown(); |
| 348 | } |
| 349 | @Override |
| 350 | void onCompletion() { |
| 351 | latch.countDown(); |
| 352 | } |
| 353 | }); |
| 354 | latch.await(); |
| 355 | } |
| 356 | |
| 357 | private List<DocumentInfo> getDocumentInfo(int... positions) { |
| 358 | return model.getDocuments(positionToSelection(positions)); |
| 359 | } |
| 360 | |
| 361 | private static class DummyListener implements Model.UpdateListener { |
| 362 | public void onModelUpdate(Model model) {} |
| 363 | public void onModelUpdateFailed(Exception e) {} |
| 364 | } |
Ben Kwa | db65cd5 | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 365 | } |