blob: 121eb41c4c2afe65760d06bbdb126c2ab2211d50 [file] [log] [blame]
Ben Kwada858bf2015-12-09 14:33:49 -08001/*
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
17package com.android.documentsui.dirlist;
18
Ben Kwada858bf2015-12-09 14:33:49 -080019import android.content.ContentResolver;
20import android.content.Context;
21import android.content.ContextWrapper;
22import android.database.Cursor;
23import android.database.MatrixCursor;
24import android.net.Uri;
25import android.os.Bundle;
26import android.provider.DocumentsContract;
27import android.provider.DocumentsContract.Document;
28import android.support.v7.widget.RecyclerView;
29import android.test.AndroidTestCase;
30import android.test.mock.MockContentProvider;
31import android.test.mock.MockContentResolver;
32import android.test.suitebuilder.annotation.SmallTest;
33import android.view.ViewGroup;
34
35import com.android.documentsui.DirectoryResult;
36import com.android.documentsui.RootCursorWrapper;
Ben Kwab8a5e082015-12-07 13:25:27 -080037import com.android.documentsui.State;
Ben Kwada858bf2015-12-09 14:33:49 -080038import com.android.documentsui.dirlist.MultiSelectManager.Selection;
39import com.android.documentsui.model.DocumentInfo;
40
41import java.util.ArrayList;
Ben Kwab8a5e082015-12-07 13:25:27 -080042import java.util.BitSet;
Ben Kwada858bf2015-12-09 14:33:49 -080043import java.util.List;
Ben Kwab8a5e082015-12-07 13:25:27 -080044import java.util.Random;
Ben Kwada858bf2015-12-09 14:33:49 -080045import java.util.concurrent.CountDownLatch;
46
47@SmallTest
48public 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 Kwab8a5e082015-12-07 13:25:27 -080055 Document.COLUMN_FLAGS,
56 Document.COLUMN_DISPLAY_NAME,
Ben Kwa6280de02015-12-16 19:42:08 -080057 Document.COLUMN_SIZE,
58 Document.COLUMN_MIME_TYPE
Ben Kwada858bf2015-12-09 14:33:49 -080059 };
60 private static Cursor cursor;
61
62 private Context context;
63 private Model model;
64 private TestContentProvider provider;
Ben Kwab8a5e082015-12-07 13:25:27 -080065 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 Kwada858bf2015-12-09 14:33:49 -080077
78 public void setUp() {
79 setupTestContext();
80
Ben Kwab8a5e082015-12-07 13:25:27 -080081 Random rand = new Random();
82
Ben Kwada858bf2015-12-09 14:33:49 -080083 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 Kwab8a5e082015-12-07 13:25:27 -080089 // 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 Kwada858bf2015-12-09 14:33:49 -080093 }
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 Kwab8a5e082015-12-07 13:25:27 -0800105 // 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 Kwada858bf2015-12-09 14:33:49 -0800114 // 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 Kwab8a5e082015-12-07 13:25:27 -0800121 MatrixCursor cIn = new MatrixCursor(COLUMNS);
Ben Kwada858bf2015-12-09 14:33:49 -0800122
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 Kwab8a5e082015-12-07 13:25:27 -0800127 MatrixCursor.RowBuilder row0 = cIn.newRow();
Ben Kwada858bf2015-12-09 14:33:49 -0800128 row0.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY0);
129 row0.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
130
Ben Kwab8a5e082015-12-07 13:25:27 -0800131 MatrixCursor.RowBuilder row1 = cIn.newRow();
Ben Kwada858bf2015-12-09 14:33:49 -0800132 row1.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY1);
133 row1.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
134 }
135
Ben Kwab8a5e082015-12-07 13:25:27 -0800136 // 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 Kwada858bf2015-12-09 14:33:49 -0800161 }
Ben Kwab8a5e082015-12-07 13:25:27 -0800162
163 assertEquals(ITEM_COUNT, b0.cardinality());
164 assertEquals(ITEM_COUNT, b1.cardinality());
Ben Kwada858bf2015-12-09 14:33:49 -0800165 }
166
167 // Tests the base case for Model.getItem.
168 public void testGetItem() {
Ben Kwab8a5e082015-12-07 13:25:27 -0800169 List<String> ids = model.getModelIds();
170 assertEquals(ITEM_COUNT, ids.size());
Ben Kwada858bf2015-12-09 14:33:49 -0800171 for (int i = 0; i < ITEM_COUNT; ++i) {
Ben Kwab8a5e082015-12-07 13:25:27 -0800172 Cursor c = model.getItem(ids.get(i));
Ben Kwada858bf2015-12-09 14:33:49 -0800173 assertEquals(i, c.getPosition());
174 }
175 }
176
Ben Kwab8a5e082015-12-07 13:25:27 -0800177 // 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 Kwab8a5e082015-12-07 13:25:27 -0800201 DirectoryResult r = new DirectoryResult();
202 r.cursor = cursor;
203 r.sortOrder = State.SORT_ORDER_SIZE;
204 model.update(r);
205
Ben Kwa6280de02015-12-16 19:42:08 -0800206 BitSet seen = new BitSet(ITEM_COUNT);
207 int previousSize = Integer.MAX_VALUE;
Ben Kwab8a5e082015-12-07 13:25:27 -0800208 for (String id: model.getModelIds()) {
209 Cursor c = model.getItem(id);
210 seen.set(c.getPosition());
Ben Kwa6280de02015-12-16 19:42:08 -0800211 // Check sort order - descending numerical
212 int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
213 assertTrue(previousSize >= size);
214 previousSize = size;
Ben Kwab8a5e082015-12-07 13:25:27 -0800215 }
Ben Kwa6280de02015-12-16 19:42:08 -0800216 // Check that all items were accounted for.
Ben Kwab8a5e082015-12-07 13:25:27 -0800217 assertEquals(ITEM_COUNT, seen.cardinality());
Ben Kwab8a5e082015-12-07 13:25:27 -0800218 }
219
Ben Kwa6280de02015-12-16 19:42:08 -0800220 // 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 Kwab8a5e082015-12-07 13:25:27 -0800269
Ben Kwada858bf2015-12-09 14:33:49 -0800270 // 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 Kwab8a5e082015-12-07 13:25:27 -0800293 List<String> ids = model.getModelIds();
Ben Kwada858bf2015-12-09 14:33:49 -0800294 Selection s = new Selection();
295 // Construct a selection of the given positions.
296 for (int p: positions) {
Ben Kwab8a5e082015-12-07 13:25:27 -0800297 s.add(ids.get(p));
Ben Kwada858bf2015-12-09 14:33:49 -0800298 }
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}