blob: 5e55e1a8b9ea884784790e347ba0a6e62ccd8c3a [file] [log] [blame]
Ben Kwa003a9992015-11-30 23:00:02 -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
19import static com.android.documentsui.Shared.DEBUG;
Ben Kwa862b96412015-12-07 13:25:27 -080020import static com.android.documentsui.State.SORT_ORDER_DISPLAY_NAME;
21import static com.android.documentsui.State.SORT_ORDER_LAST_MODIFIED;
22import static com.android.documentsui.State.SORT_ORDER_SIZE;
23import static com.android.documentsui.model.DocumentInfo.getCursorLong;
Ben Kwa003a9992015-11-30 23:00:02 -080024import static com.android.documentsui.model.DocumentInfo.getCursorString;
Ben Kwa003a9992015-11-30 23:00:02 -080025
Ben Kwa003a9992015-11-30 23:00:02 -080026import android.database.Cursor;
Ben Kwa003a9992015-11-30 23:00:02 -080027import android.os.Bundle;
Ben Kwa003a9992015-11-30 23:00:02 -080028import android.provider.DocumentsContract;
29import android.provider.DocumentsContract.Document;
30import android.support.annotation.Nullable;
31import android.support.annotation.VisibleForTesting;
Ben Kwa003a9992015-11-30 23:00:02 -080032import android.util.Log;
Ben Kwa003a9992015-11-30 23:00:02 -080033
Ben Kwa003a9992015-11-30 23:00:02 -080034import com.android.documentsui.DirectoryResult;
Ben Kwa003a9992015-11-30 23:00:02 -080035import com.android.documentsui.RootCursorWrapper;
Steve McKay008e9482016-02-18 15:32:16 -080036import com.android.documentsui.Shared;
Ben Kwa003a9992015-11-30 23:00:02 -080037import com.android.documentsui.dirlist.MultiSelectManager.Selection;
38import com.android.documentsui.model.DocumentInfo;
Ben Kwa003a9992015-11-30 23:00:02 -080039
40import java.util.ArrayList;
Ben Kwa003a9992015-11-30 23:00:02 -080041import java.util.HashMap;
42import java.util.List;
Ben Kwa862b96412015-12-07 13:25:27 -080043import java.util.Map;
Ben Kwa003a9992015-11-30 23:00:02 -080044
45/**
46 * The data model for the current loaded directory.
47 */
48@VisibleForTesting
Tomasz Mikolajewski3d988a92016-02-16 12:28:43 +090049public class Model {
Ben Kwa003a9992015-11-30 23:00:02 -080050 private static final String TAG = "Model";
Ben Kwa862b96412015-12-07 13:25:27 -080051
Ben Kwa003a9992015-11-30 23:00:02 -080052 private boolean mIsLoading;
Ben Kwa743c7c22015-12-01 19:56:57 -080053 private List<UpdateListener> mUpdateListeners = new ArrayList<>();
Ben Kwa003a9992015-11-30 23:00:02 -080054 @Nullable private Cursor mCursor;
Ben Kwa862b96412015-12-07 13:25:27 -080055 private int mCursorCount;
56 /** Maps Model ID to cursor positions, for looking up items by Model ID. */
57 private Map<String, Integer> mPositions = new HashMap<>();
58 /**
59 * A sorted array of model IDs for the files currently in the Model. Sort order is determined
60 * by {@link #mSortOrder}
61 */
62 private List<String> mIds = new ArrayList<>();
63 private int mSortOrder = SORT_ORDER_DISPLAY_NAME;
64
Ben Kwa003a9992015-11-30 23:00:02 -080065 @Nullable String info;
66 @Nullable String error;
Ben Kwa003a9992015-11-30 23:00:02 -080067
Ben Kwa743c7c22015-12-01 19:56:57 -080068 /**
Ben Kwa862b96412015-12-07 13:25:27 -080069 * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
70 * string that can be used to identify the document referred to by the cursor.
Ben Kwa743c7c22015-12-01 19:56:57 -080071 *
72 * @param c A cursor that refers to a document.
73 */
Ben Kwa862b96412015-12-07 13:25:27 -080074 private static String createModelId(Cursor c) {
75 // TODO: Maybe more efficient to use just the document ID, in cases where there is only one
76 // authority (which should be the majority of cases).
Steve McKay955e46d2016-01-05 12:53:35 -080077 return createModelId(
78 getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY),
79 getCursorString(c, Document.COLUMN_DOCUMENT_ID));
80 }
81
82 /**
83 * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
84 * string that can be used to identify the document referred to by the cursor.
85 *
86 * @param c A cursor that refers to a document.
87 */
88 static String createModelId(String authority, String docId) {
89 return authority + "|" + docId;
Ben Kwa743c7c22015-12-01 19:56:57 -080090 }
91
Ben Kwa743c7c22015-12-01 19:56:57 -080092 private void notifyUpdateListeners() {
93 for (UpdateListener listener: mUpdateListeners) {
94 listener.onModelUpdate(this);
95 }
96 }
97
98 private void notifyUpdateListeners(Exception e) {
99 for (UpdateListener listener: mUpdateListeners) {
100 listener.onModelUpdateFailed(e);
101 }
102 }
103
Ben Kwa003a9992015-11-30 23:00:02 -0800104 void update(DirectoryResult result) {
105 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
106
107 if (result == null) {
108 mCursor = null;
109 mCursorCount = 0;
Ben Kwa862b96412015-12-07 13:25:27 -0800110 mIds.clear();
111 mPositions.clear();
Ben Kwa003a9992015-11-30 23:00:02 -0800112 info = null;
113 error = null;
114 mIsLoading = false;
Ben Kwa743c7c22015-12-01 19:56:57 -0800115 notifyUpdateListeners();
Ben Kwa003a9992015-11-30 23:00:02 -0800116 return;
117 }
118
119 if (result.exception != null) {
120 Log.e(TAG, "Error while loading directory contents", result.exception);
Ben Kwa743c7c22015-12-01 19:56:57 -0800121 notifyUpdateListeners(result.exception);
Ben Kwa003a9992015-11-30 23:00:02 -0800122 return;
123 }
124
125 mCursor = result.cursor;
126 mCursorCount = mCursor.getCount();
Ben Kwa862b96412015-12-07 13:25:27 -0800127 mSortOrder = result.sortOrder;
Ben Kwa003a9992015-11-30 23:00:02 -0800128
Ben Kwa862b96412015-12-07 13:25:27 -0800129 updateModelData();
Ben Kwa003a9992015-11-30 23:00:02 -0800130
131 final Bundle extras = mCursor.getExtras();
132 if (extras != null) {
133 info = extras.getString(DocumentsContract.EXTRA_INFO);
134 error = extras.getString(DocumentsContract.EXTRA_ERROR);
135 mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
136 }
137
Ben Kwa743c7c22015-12-01 19:56:57 -0800138 notifyUpdateListeners();
Ben Kwa003a9992015-11-30 23:00:02 -0800139 }
140
Ben Kwa743c7c22015-12-01 19:56:57 -0800141 @VisibleForTesting
Ben Kwa003a9992015-11-30 23:00:02 -0800142 int getItemCount() {
Ben Kwadb65cd52015-12-09 14:33:49 -0800143 return mCursorCount;
Ben Kwa003a9992015-11-30 23:00:02 -0800144 }
145
146 /**
Ben Kwa862b96412015-12-07 13:25:27 -0800147 * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
148 * according to the current sort order.
Ben Kwa003a9992015-11-30 23:00:02 -0800149 */
Ben Kwa862b96412015-12-07 13:25:27 -0800150 private void updateModelData() {
151 int[] positions = new int[mCursorCount];
152 mIds.clear();
Ben Kwac72a2cb2015-12-16 19:42:08 -0800153 String[] stringValues = new String[mCursorCount];
154 long[] longValues = null;
Ben Kwa862b96412015-12-07 13:25:27 -0800155
Ben Kwac72a2cb2015-12-16 19:42:08 -0800156 if (mSortOrder == SORT_ORDER_LAST_MODIFIED || mSortOrder == SORT_ORDER_SIZE) {
157 longValues = new long[mCursorCount];
Ben Kwa862b96412015-12-07 13:25:27 -0800158 }
159
Ben Kwa003a9992015-11-30 23:00:02 -0800160 mCursor.moveToPosition(-1);
161 for (int pos = 0; pos < mCursorCount; ++pos) {
162 mCursor.moveToNext();
Ben Kwa862b96412015-12-07 13:25:27 -0800163 positions[pos] = pos;
164 mIds.add(createModelId(mCursor));
165
166 switch(mSortOrder) {
167 case SORT_ORDER_DISPLAY_NAME:
168 final String mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
169 final String displayName = getCursorString(
170 mCursor, Document.COLUMN_DISPLAY_NAME);
171 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
Steve McKay008e9482016-02-18 15:32:16 -0800172 stringValues[pos] = Shared.DIR_PREFIX + displayName;
Ben Kwa862b96412015-12-07 13:25:27 -0800173 } else {
Ben Kwac72a2cb2015-12-16 19:42:08 -0800174 stringValues[pos] = displayName;
Ben Kwa862b96412015-12-07 13:25:27 -0800175 }
176 break;
177 case SORT_ORDER_LAST_MODIFIED:
Ben Kwae2564f92016-01-08 15:34:47 -0800178 longValues[pos] = getLastModified(mCursor);
Ben Kwac72a2cb2015-12-16 19:42:08 -0800179 stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
Ben Kwa862b96412015-12-07 13:25:27 -0800180 break;
181 case SORT_ORDER_SIZE:
Ben Kwac72a2cb2015-12-16 19:42:08 -0800182 longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
183 stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
Ben Kwa862b96412015-12-07 13:25:27 -0800184 break;
185 }
186 }
187
188 switch (mSortOrder) {
189 case SORT_ORDER_DISPLAY_NAME:
Ben Kwac72a2cb2015-12-16 19:42:08 -0800190 binarySort(stringValues, positions, mIds);
Ben Kwa862b96412015-12-07 13:25:27 -0800191 break;
192 case SORT_ORDER_LAST_MODIFIED:
193 case SORT_ORDER_SIZE:
Ben Kwac72a2cb2015-12-16 19:42:08 -0800194 binarySort(longValues, stringValues, positions, mIds);
Ben Kwa862b96412015-12-07 13:25:27 -0800195 break;
196 }
197
198 // Populate the positions.
199 mPositions.clear();
200 for (int i = 0; i < mCursorCount; ++i) {
201 mPositions.put(mIds.get(i), positions[i]);
202 }
203 }
204
205 /**
Ben Kwac72a2cb2015-12-16 19:42:08 -0800206 * Sorts model data. Takes three columns of index-corresponded data. The first column is the
207 * sort key. Rows are sorted in ascending alphabetical order on the sort key. This code is based
208 * on TimSort.binarySort().
209 *
210 * @param sortKey Data is sorted in ascending alphabetical order.
211 * @param positions Cursor positions to be sorted.
212 * @param ids Model IDs to be sorted.
Ben Kwa862b96412015-12-07 13:25:27 -0800213 */
Ben Kwac72a2cb2015-12-16 19:42:08 -0800214 private static void binarySort(String[] sortKey, int[] positions, List<String> ids) {
Ben Kwa862b96412015-12-07 13:25:27 -0800215 final int count = positions.length;
216 for (int start = 1; start < count; start++) {
217 final int pivotPosition = positions[start];
Ben Kwac72a2cb2015-12-16 19:42:08 -0800218 final String pivotValue = sortKey[start];
Ben Kwa862b96412015-12-07 13:25:27 -0800219 final String pivotId = ids.get(start);
220
221 int left = 0;
222 int right = start;
223
224 while (left < right) {
225 int mid = (left + right) >>> 1;
226
227 final String lhs = pivotValue;
Ben Kwac72a2cb2015-12-16 19:42:08 -0800228 final String rhs = sortKey[mid];
Steve McKay008e9482016-02-18 15:32:16 -0800229 final int compare = Shared.compareToIgnoreCaseNullable(lhs, rhs);
Ben Kwa862b96412015-12-07 13:25:27 -0800230
231 if (compare < 0) {
232 right = mid;
233 } else {
234 left = mid + 1;
235 }
236 }
237
238 int n = start - left;
239 switch (n) {
240 case 2:
241 positions[left + 2] = positions[left + 1];
Ben Kwac72a2cb2015-12-16 19:42:08 -0800242 sortKey[left + 2] = sortKey[left + 1];
Ben Kwa862b96412015-12-07 13:25:27 -0800243 ids.set(left + 2, ids.get(left + 1));
244 case 1:
245 positions[left + 1] = positions[left];
Ben Kwac72a2cb2015-12-16 19:42:08 -0800246 sortKey[left + 1] = sortKey[left];
Ben Kwa862b96412015-12-07 13:25:27 -0800247 ids.set(left + 1, ids.get(left));
248 break;
249 default:
250 System.arraycopy(positions, left, positions, left + 1, n);
Ben Kwac72a2cb2015-12-16 19:42:08 -0800251 System.arraycopy(sortKey, left, sortKey, left + 1, n);
Ben Kwa862b96412015-12-07 13:25:27 -0800252 for (int i = n; i >= 1; --i) {
253 ids.set(left + i, ids.get(left + i - 1));
254 }
255 }
256
257 positions[left] = pivotPosition;
Ben Kwac72a2cb2015-12-16 19:42:08 -0800258 sortKey[left] = pivotValue;
Ben Kwa862b96412015-12-07 13:25:27 -0800259 ids.set(left, pivotId);
260 }
261 }
262
263 /**
Ben Kwac72a2cb2015-12-16 19:42:08 -0800264 * Sorts model data. Takes four columns of index-corresponded data. The first column is the sort
265 * key, and the second is an array of mime types. The rows are first bucketed by mime type
266 * (directories vs documents) and then each bucket is sorted independently in descending
267 * numerical order on the sort key. This code is based on TimSort.binarySort().
268 *
269 * @param sortKey Data is sorted in descending numerical order.
270 * @param mimeTypes Corresponding mime types. Directories will be sorted ahead of documents.
271 * @param positions Cursor positions to be sorted.
272 * @param ids Model IDs to be sorted.
Ben Kwa862b96412015-12-07 13:25:27 -0800273 */
Ben Kwac72a2cb2015-12-16 19:42:08 -0800274 private static void binarySort(
275 long[] sortKey, String[] mimeTypes, int[] positions, List<String> ids) {
Ben Kwa862b96412015-12-07 13:25:27 -0800276 final int count = positions.length;
277 for (int start = 1; start < count; start++) {
278 final int pivotPosition = positions[start];
Ben Kwac72a2cb2015-12-16 19:42:08 -0800279 final long pivotValue = sortKey[start];
280 final String pivotMime = mimeTypes[start];
Ben Kwa862b96412015-12-07 13:25:27 -0800281 final String pivotId = ids.get(start);
282
283 int left = 0;
284 int right = start;
285
286 while (left < right) {
Ben Kwac72a2cb2015-12-16 19:42:08 -0800287 int mid = ((left + right) >>> 1);
Ben Kwa862b96412015-12-07 13:25:27 -0800288
Ben Kwac72a2cb2015-12-16 19:42:08 -0800289 // First bucket by mime type. Directories always go in front.
290 int compare = 0;
291 final boolean lhsIsDir = Document.MIME_TYPE_DIR.equals(pivotMime);
292 final boolean rhsIsDir = Document.MIME_TYPE_DIR.equals(mimeTypes[mid]);
293 if (lhsIsDir && !rhsIsDir) {
294 compare = -1;
295 } else if (!lhsIsDir && rhsIsDir) {
296 compare = 1;
297 } else {
298 final long lhs = pivotValue;
299 final long rhs = sortKey[mid];
Ben Kwae2564f92016-01-08 15:34:47 -0800300 // Sort in descending numerical order. This matches legacy behaviour, which
301 // yields largest or most recent items on top.
Ben Kwac72a2cb2015-12-16 19:42:08 -0800302 compare = -Long.compare(lhs, rhs);
303 }
Ben Kwa862b96412015-12-07 13:25:27 -0800304
Ben Kwae2564f92016-01-08 15:34:47 -0800305 // If numerical comparison yields a tie, use document ID as a tie breaker. This
306 // will yield stable results even if incoming items are continually shuffling and
307 // have identical numerical sort keys. One common example of this scenario is seen
308 // when sorting a set of active downloads by mod time.
309 if (compare == 0) {
310 compare = pivotId.compareTo(ids.get(mid));
311 }
312
Ben Kwa862b96412015-12-07 13:25:27 -0800313 if (compare < 0) {
314 right = mid;
315 } else {
316 left = mid + 1;
317 }
318 }
319
320 int n = start - left;
321 switch (n) {
322 case 2:
323 positions[left + 2] = positions[left + 1];
Ben Kwac72a2cb2015-12-16 19:42:08 -0800324 sortKey[left + 2] = sortKey[left + 1];
325 mimeTypes[left + 2] = mimeTypes[left + 1];
Ben Kwa862b96412015-12-07 13:25:27 -0800326 ids.set(left + 2, ids.get(left + 1));
327 case 1:
328 positions[left + 1] = positions[left];
Ben Kwac72a2cb2015-12-16 19:42:08 -0800329 sortKey[left + 1] = sortKey[left];
330 mimeTypes[left + 1] = mimeTypes[left];
Ben Kwa862b96412015-12-07 13:25:27 -0800331 ids.set(left + 1, ids.get(left));
332 break;
333 default:
334 System.arraycopy(positions, left, positions, left + 1, n);
Ben Kwac72a2cb2015-12-16 19:42:08 -0800335 System.arraycopy(sortKey, left, sortKey, left + 1, n);
336 System.arraycopy(mimeTypes, left, mimeTypes, left + 1, n);
Ben Kwa862b96412015-12-07 13:25:27 -0800337 for (int i = n; i >= 1; --i) {
338 ids.set(left + i, ids.get(left + i - 1));
339 }
340 }
341
342 positions[left] = pivotPosition;
Ben Kwac72a2cb2015-12-16 19:42:08 -0800343 sortKey[left] = pivotValue;
344 mimeTypes[left] = pivotMime;
Ben Kwa862b96412015-12-07 13:25:27 -0800345 ids.set(left, pivotId);
Ben Kwa003a9992015-11-30 23:00:02 -0800346 }
347 }
348
Ben Kwae2564f92016-01-08 15:34:47 -0800349 /**
350 * @return Timestamp for the given document. Some docs (e.g. active downloads) have a null
351 * timestamp - these will be replaced with MAX_LONG so that such files get sorted to the top
352 * when sorting by date.
353 */
354 long getLastModified(Cursor cursor) {
355 long l = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
356 return (l == -1) ? Long.MAX_VALUE : l;
357 }
358
Tomasz Mikolajewski3d988a92016-02-16 12:28:43 +0900359 public @Nullable Cursor getItem(String modelId) {
Ben Kwa003a9992015-11-30 23:00:02 -0800360 Integer pos = mPositions.get(modelId);
361 if (pos != null) {
362 mCursor.moveToPosition(pos);
363 return mCursor;
364 }
365 return null;
366 }
367
Ben Kwa003a9992015-11-30 23:00:02 -0800368 boolean isEmpty() {
369 return mCursorCount == 0;
370 }
371
372 boolean isLoading() {
373 return mIsLoading;
374 }
375
376 List<DocumentInfo> getDocuments(Selection items) {
377 final int size = (items != null) ? items.size() : 0;
378
379 final List<DocumentInfo> docs = new ArrayList<>(size);
Ben Kwa743c7c22015-12-01 19:56:57 -0800380 for (String modelId: items.getAll()) {
381 final Cursor cursor = getItem(modelId);
Steve McKaya1f76802016-02-25 13:34:03 -0800382 assert(cursor != null);
383
384 docs.add(DocumentInfo.fromDirectoryCursor(cursor));
Ben Kwa003a9992015-11-30 23:00:02 -0800385 }
386 return docs;
387 }
388
Ben Kwa003a9992015-11-30 23:00:02 -0800389 void addUpdateListener(UpdateListener listener) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800390 mUpdateListeners.add(listener);
Ben Kwa003a9992015-11-30 23:00:02 -0800391 }
392
Ben Kwaa4acc902016-02-10 15:48:25 -0800393 void removeUpdateListener(UpdateListener listener) {
394 mUpdateListeners.remove(listener);
395 }
396
Ben Kwa743c7c22015-12-01 19:56:57 -0800397 static interface UpdateListener {
Ben Kwa003a9992015-11-30 23:00:02 -0800398 /**
399 * Called when a successful update has occurred.
400 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800401 void onModelUpdate(Model model);
Ben Kwa003a9992015-11-30 23:00:02 -0800402
403 /**
404 * Called when an update has been attempted but failed.
405 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800406 void onModelUpdateFailed(Exception e);
Ben Kwa003a9992015-11-30 23:00:02 -0800407 }
Ben Kwa862b96412015-12-07 13:25:27 -0800408
409 /**
410 * @return An ordered array of model IDs representing the documents in the model. It is sorted
411 * according to the current sort order, which was set by the last model update.
412 */
413 public List<String> getModelIds() {
414 return mIds;
415 }
Ben Kwa003a9992015-11-30 23:00:02 -0800416}