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