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, |
| 57 | Document.COLUMN_SIZE |
Ben Kwa | da858bf | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 58 | }; |
| 59 | private static Cursor cursor; |
| 60 | |
| 61 | private Context context; |
| 62 | private Model model; |
| 63 | private TestContentProvider provider; |
Ben Kwa | b8a5e08 | 2015-12-07 13:25:27 -0800 | [diff] [blame^] | 64 | private static final String[] NAMES = new String[] { |
| 65 | "4", |
| 66 | "foo", |
| 67 | "1", |
| 68 | "bar", |
| 69 | "*(Ljifl;a", |
| 70 | "0", |
| 71 | "baz", |
| 72 | "2", |
| 73 | "3", |
| 74 | "%$%VD" |
| 75 | }; |
Ben Kwa | da858bf | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 76 | |
| 77 | public void setUp() { |
| 78 | setupTestContext(); |
| 79 | |
Ben Kwa | b8a5e08 | 2015-12-07 13:25:27 -0800 | [diff] [blame^] | 80 | Random rand = new Random(); |
| 81 | |
Ben Kwa | da858bf | 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 | b8a5e08 | 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 | da858bf | 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. |
| 99 | model = new Model(context, new DummyAdapter()); |
| 100 | model.addUpdateListener(new DummyListener()); |
| 101 | model.update(r); |
| 102 | } |
| 103 | |
Ben Kwa | b8a5e08 | 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 | da858bf | 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 | b8a5e08 | 2015-12-07 13:25:27 -0800 | [diff] [blame^] | 120 | MatrixCursor cIn = new MatrixCursor(COLUMNS); |
Ben Kwa | da858bf | 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 | b8a5e08 | 2015-12-07 13:25:27 -0800 | [diff] [blame^] | 126 | MatrixCursor.RowBuilder row0 = cIn.newRow(); |
Ben Kwa | da858bf | 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 | b8a5e08 | 2015-12-07 13:25:27 -0800 | [diff] [blame^] | 130 | MatrixCursor.RowBuilder row1 = cIn.newRow(); |
Ben Kwa | da858bf | 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 | b8a5e08 | 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 | da858bf | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 160 | } |
Ben Kwa | b8a5e08 | 2015-12-07 13:25:27 -0800 | [diff] [blame^] | 161 | |
| 162 | assertEquals(ITEM_COUNT, b0.cardinality()); |
| 163 | assertEquals(ITEM_COUNT, b1.cardinality()); |
Ben Kwa | da858bf | 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 | b8a5e08 | 2015-12-07 13:25:27 -0800 | [diff] [blame^] | 168 | List<String> ids = model.getModelIds(); |
| 169 | assertEquals(ITEM_COUNT, ids.size()); |
Ben Kwa | da858bf | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 170 | for (int i = 0; i < ITEM_COUNT; ++i) { |
Ben Kwa | b8a5e08 | 2015-12-07 13:25:27 -0800 | [diff] [blame^] | 171 | Cursor c = model.getItem(ids.get(i)); |
Ben Kwa | da858bf | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 172 | assertEquals(i, c.getPosition()); |
| 173 | } |
| 174 | } |
| 175 | |
Ben Kwa | b8a5e08 | 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() { |
| 200 | BitSet seen = new BitSet(ITEM_COUNT); |
| 201 | List<Integer> sizes = new ArrayList<>(); |
| 202 | |
| 203 | DirectoryResult r = new DirectoryResult(); |
| 204 | r.cursor = cursor; |
| 205 | r.sortOrder = State.SORT_ORDER_SIZE; |
| 206 | model.update(r); |
| 207 | |
| 208 | for (String id: model.getModelIds()) { |
| 209 | Cursor c = model.getItem(id); |
| 210 | seen.set(c.getPosition()); |
| 211 | sizes.add(DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE)); |
| 212 | } |
| 213 | |
| 214 | assertEquals(ITEM_COUNT, seen.cardinality()); |
| 215 | for (int i = 0; i < sizes.size()-1; ++i) { |
| 216 | // Note: sizes are sorted descending. |
| 217 | assertTrue(sizes.get(i) >= sizes.get(i+1)); |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | |
Ben Kwa | da858bf | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 222 | // Tests that Model.delete works correctly. |
| 223 | public void testDelete() throws Exception { |
| 224 | // Simulate deleting 2 files. |
| 225 | List<DocumentInfo> docsBefore = getDocumentInfo(2, 3); |
| 226 | delete(2, 3); |
| 227 | |
| 228 | provider.assertWasDeleted(docsBefore.get(0)); |
| 229 | provider.assertWasDeleted(docsBefore.get(1)); |
| 230 | } |
| 231 | |
| 232 | private void setupTestContext() { |
| 233 | final MockContentResolver resolver = new MockContentResolver(); |
| 234 | context = new ContextWrapper(getContext()) { |
| 235 | @Override |
| 236 | public ContentResolver getContentResolver() { |
| 237 | return resolver; |
| 238 | } |
| 239 | }; |
| 240 | provider = new TestContentProvider(); |
| 241 | resolver.addProvider(AUTHORITY, provider); |
| 242 | } |
| 243 | |
| 244 | private Selection positionToSelection(int... positions) { |
Ben Kwa | b8a5e08 | 2015-12-07 13:25:27 -0800 | [diff] [blame^] | 245 | List<String> ids = model.getModelIds(); |
Ben Kwa | da858bf | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 246 | Selection s = new Selection(); |
| 247 | // Construct a selection of the given positions. |
| 248 | for (int p: positions) { |
Ben Kwa | b8a5e08 | 2015-12-07 13:25:27 -0800 | [diff] [blame^] | 249 | s.add(ids.get(p)); |
Ben Kwa | da858bf | 2015-12-09 14:33:49 -0800 | [diff] [blame] | 250 | } |
| 251 | return s; |
| 252 | } |
| 253 | |
| 254 | private void delete(int... positions) throws InterruptedException { |
| 255 | Selection s = positionToSelection(positions); |
| 256 | final CountDownLatch latch = new CountDownLatch(1); |
| 257 | |
| 258 | model.delete( |
| 259 | s, |
| 260 | new Model.DeletionListener() { |
| 261 | @Override |
| 262 | public void onError() { |
| 263 | latch.countDown(); |
| 264 | } |
| 265 | @Override |
| 266 | void onCompletion() { |
| 267 | latch.countDown(); |
| 268 | } |
| 269 | }); |
| 270 | latch.await(); |
| 271 | } |
| 272 | |
| 273 | private List<DocumentInfo> getDocumentInfo(int... positions) { |
| 274 | return model.getDocuments(positionToSelection(positions)); |
| 275 | } |
| 276 | |
| 277 | private static class DummyListener implements Model.UpdateListener { |
| 278 | public void onModelUpdate(Model model) {} |
| 279 | public void onModelUpdateFailed(Exception e) {} |
| 280 | } |
| 281 | |
| 282 | private static class DummyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { |
| 283 | public int getItemCount() { return 0; } |
| 284 | public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {} |
| 285 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
| 286 | return null; |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | private static class TestContentProvider extends MockContentProvider { |
| 291 | List<Uri> mDeleted = new ArrayList<>(); |
| 292 | |
| 293 | @Override |
| 294 | public Bundle call(String method, String arg, Bundle extras) { |
| 295 | // Intercept and log delete method calls. |
| 296 | if (DocumentsContract.METHOD_DELETE_DOCUMENT.equals(method)) { |
| 297 | final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); |
| 298 | mDeleted.add(documentUri); |
| 299 | return new Bundle(); |
| 300 | } else { |
| 301 | return super.call(method, arg, extras); |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | public void assertWasDeleted(DocumentInfo doc) { |
| 306 | assertTrue(mDeleted.contains(doc.derivedUri)); |
| 307 | } |
| 308 | } |
| 309 | } |