blob: ab4f5c4f90ad3d874ce5ed6099d3e136fa985b16 [file] [log] [blame]
Ben Kwa0497da82015-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 Kwab8a5e082015-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 Kwa0497da82015-11-30 23:00:02 -080024import static com.android.documentsui.model.DocumentInfo.getCursorString;
Ben Kwa0497da82015-11-30 23:00:02 -080025
Ben Kwa0497da82015-11-30 23:00:02 -080026import android.database.Cursor;
Ben Kwa0497da82015-11-30 23:00:02 -080027import android.os.Bundle;
Ben Kwa0497da82015-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 Kwa0497da82015-11-30 23:00:02 -080032import android.util.Log;
Ben Kwa0497da82015-11-30 23:00:02 -080033
Ben Kwa0497da82015-11-30 23:00:02 -080034import com.android.documentsui.DirectoryResult;
Ben Kwa0497da82015-11-30 23:00:02 -080035import com.android.documentsui.RootCursorWrapper;
Steve McKay55c00e72016-02-18 15:32:16 -080036import com.android.documentsui.Shared;
Ben Kwa0497da82015-11-30 23:00:02 -080037import com.android.documentsui.dirlist.MultiSelectManager.Selection;
38import com.android.documentsui.model.DocumentInfo;
Ben Kwa0497da82015-11-30 23:00:02 -080039
40import java.util.ArrayList;
Ben Kwa0497da82015-11-30 23:00:02 -080041import java.util.HashMap;
42import java.util.List;
Ben Kwab8a5e082015-12-07 13:25:27 -080043import java.util.Map;
Ben Kwa0497da82015-11-30 23:00:02 -080044
45/**
46 * The data model for the current loaded directory.
47 */
48@VisibleForTesting
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +090049public class Model {
Ben Kwa0497da82015-11-30 23:00:02 -080050 private static final String TAG = "Model";
Ben Kwab8a5e082015-12-07 13:25:27 -080051
Ben Kwa0497da82015-11-30 23:00:02 -080052 private boolean mIsLoading;
Ben Kwad72a1da2015-12-01 19:56:57 -080053 private List<UpdateListener> mUpdateListeners = new ArrayList<>();
Ben Kwa0497da82015-11-30 23:00:02 -080054 @Nullable private Cursor mCursor;
Ben Kwab8a5e082015-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 */
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +090062 private String mIds[] = new String[0];
Ben Kwab8a5e082015-12-07 13:25:27 -080063 private int mSortOrder = SORT_ORDER_DISPLAY_NAME;
64
Ben Kwa0497da82015-11-30 23:00:02 -080065 @Nullable String info;
66 @Nullable String error;
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +090067 @Nullable DocumentInfo doc;
Ben Kwa0497da82015-11-30 23:00:02 -080068
Ben Kwad72a1da2015-12-01 19:56:57 -080069 /**
Ben Kwab8a5e082015-12-07 13:25:27 -080070 * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
71 * string that can be used to identify the document referred to by the cursor.
Ben Kwad72a1da2015-12-01 19:56:57 -080072 *
73 * @param c A cursor that refers to a document.
74 */
Ben Kwab8a5e082015-12-07 13:25:27 -080075 private static String createModelId(Cursor c) {
76 // TODO: Maybe more efficient to use just the document ID, in cases where there is only one
77 // authority (which should be the majority of cases).
Steve McKaye9104032016-01-05 12:53:35 -080078 return createModelId(
79 getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY),
80 getCursorString(c, Document.COLUMN_DOCUMENT_ID));
81 }
82
83 /**
84 * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
85 * string that can be used to identify the document referred to by the cursor.
86 *
87 * @param c A cursor that refers to a document.
88 */
89 static String createModelId(String authority, String docId) {
90 return authority + "|" + docId;
Ben Kwad72a1da2015-12-01 19:56:57 -080091 }
92
Ben Kwad72a1da2015-12-01 19:56:57 -080093 private void notifyUpdateListeners() {
94 for (UpdateListener listener: mUpdateListeners) {
95 listener.onModelUpdate(this);
96 }
97 }
98
99 private void notifyUpdateListeners(Exception e) {
100 for (UpdateListener listener: mUpdateListeners) {
101 listener.onModelUpdateFailed(e);
102 }
103 }
104
Ben Kwa0497da82015-11-30 23:00:02 -0800105 void update(DirectoryResult result) {
106 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
107
108 if (result == null) {
109 mCursor = null;
110 mCursorCount = 0;
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900111 mIds = new String[0];
Ben Kwab8a5e082015-12-07 13:25:27 -0800112 mPositions.clear();
Ben Kwa0497da82015-11-30 23:00:02 -0800113 info = null;
114 error = null;
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900115 doc = null;
Ben Kwa0497da82015-11-30 23:00:02 -0800116 mIsLoading = false;
Ben Kwad72a1da2015-12-01 19:56:57 -0800117 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800118 return;
119 }
120
121 if (result.exception != null) {
122 Log.e(TAG, "Error while loading directory contents", result.exception);
Ben Kwad72a1da2015-12-01 19:56:57 -0800123 notifyUpdateListeners(result.exception);
Ben Kwa0497da82015-11-30 23:00:02 -0800124 return;
125 }
126
127 mCursor = result.cursor;
128 mCursorCount = mCursor.getCount();
Ben Kwab8a5e082015-12-07 13:25:27 -0800129 mSortOrder = result.sortOrder;
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900130 doc = result.doc;
Ben Kwa0497da82015-11-30 23:00:02 -0800131
Ben Kwab8a5e082015-12-07 13:25:27 -0800132 updateModelData();
Ben Kwa0497da82015-11-30 23:00:02 -0800133
134 final Bundle extras = mCursor.getExtras();
135 if (extras != null) {
136 info = extras.getString(DocumentsContract.EXTRA_INFO);
137 error = extras.getString(DocumentsContract.EXTRA_ERROR);
138 mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
139 }
140
Ben Kwad72a1da2015-12-01 19:56:57 -0800141 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800142 }
143
Ben Kwad72a1da2015-12-01 19:56:57 -0800144 @VisibleForTesting
Ben Kwa0497da82015-11-30 23:00:02 -0800145 int getItemCount() {
Ben Kwada858bf2015-12-09 14:33:49 -0800146 return mCursorCount;
Ben Kwa0497da82015-11-30 23:00:02 -0800147 }
148
149 /**
Ben Kwab8a5e082015-12-07 13:25:27 -0800150 * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
151 * according to the current sort order.
Ben Kwa0497da82015-11-30 23:00:02 -0800152 */
Ben Kwab8a5e082015-12-07 13:25:27 -0800153 private void updateModelData() {
154 int[] positions = new int[mCursorCount];
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900155 mIds = new String[mCursorCount];
Ben Kwa6280de02015-12-16 19:42:08 -0800156 String[] stringValues = new String[mCursorCount];
157 long[] longValues = null;
Ben Kwab8a5e082015-12-07 13:25:27 -0800158
Ben Kwa6280de02015-12-16 19:42:08 -0800159 if (mSortOrder == SORT_ORDER_LAST_MODIFIED || mSortOrder == SORT_ORDER_SIZE) {
160 longValues = new long[mCursorCount];
Ben Kwab8a5e082015-12-07 13:25:27 -0800161 }
162
Ben Kwa0497da82015-11-30 23:00:02 -0800163 mCursor.moveToPosition(-1);
164 for (int pos = 0; pos < mCursorCount; ++pos) {
165 mCursor.moveToNext();
Ben Kwab8a5e082015-12-07 13:25:27 -0800166 positions[pos] = pos;
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900167 mIds[pos] = createModelId(mCursor);
Ben Kwab8a5e082015-12-07 13:25:27 -0800168
169 switch(mSortOrder) {
170 case SORT_ORDER_DISPLAY_NAME:
171 final String mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
172 final String displayName = getCursorString(
173 mCursor, Document.COLUMN_DISPLAY_NAME);
174 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
Steve McKay55c00e72016-02-18 15:32:16 -0800175 stringValues[pos] = Shared.DIR_PREFIX + displayName;
Ben Kwab8a5e082015-12-07 13:25:27 -0800176 } else {
Ben Kwa6280de02015-12-16 19:42:08 -0800177 stringValues[pos] = displayName;
Ben Kwab8a5e082015-12-07 13:25:27 -0800178 }
179 break;
180 case SORT_ORDER_LAST_MODIFIED:
Ben Kwae4338342016-01-08 15:34:47 -0800181 longValues[pos] = getLastModified(mCursor);
Ben Kwa6280de02015-12-16 19:42:08 -0800182 stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
Ben Kwab8a5e082015-12-07 13:25:27 -0800183 break;
184 case SORT_ORDER_SIZE:
Ben Kwa6280de02015-12-16 19:42:08 -0800185 longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
186 stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
Ben Kwab8a5e082015-12-07 13:25:27 -0800187 break;
188 }
189 }
190
191 switch (mSortOrder) {
192 case SORT_ORDER_DISPLAY_NAME:
Ben Kwa6280de02015-12-16 19:42:08 -0800193 binarySort(stringValues, positions, mIds);
Ben Kwab8a5e082015-12-07 13:25:27 -0800194 break;
195 case SORT_ORDER_LAST_MODIFIED:
196 case SORT_ORDER_SIZE:
Ben Kwa6280de02015-12-16 19:42:08 -0800197 binarySort(longValues, stringValues, positions, mIds);
Ben Kwab8a5e082015-12-07 13:25:27 -0800198 break;
199 }
200
201 // Populate the positions.
202 mPositions.clear();
203 for (int i = 0; i < mCursorCount; ++i) {
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900204 mPositions.put(mIds[i], positions[i]);
Ben Kwab8a5e082015-12-07 13:25:27 -0800205 }
206 }
207
208 /**
Ben Kwa6280de02015-12-16 19:42:08 -0800209 * Sorts model data. Takes three columns of index-corresponded data. The first column is the
210 * sort key. Rows are sorted in ascending alphabetical order on the sort key. This code is based
211 * on TimSort.binarySort().
212 *
213 * @param sortKey Data is sorted in ascending alphabetical order.
214 * @param positions Cursor positions to be sorted.
215 * @param ids Model IDs to be sorted.
Ben Kwab8a5e082015-12-07 13:25:27 -0800216 */
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900217 private static void binarySort(String[] sortKey, int[] positions, String[] ids) {
Ben Kwab8a5e082015-12-07 13:25:27 -0800218 final int count = positions.length;
219 for (int start = 1; start < count; start++) {
220 final int pivotPosition = positions[start];
Ben Kwa6280de02015-12-16 19:42:08 -0800221 final String pivotValue = sortKey[start];
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900222 final String pivotId = ids[start];
Ben Kwab8a5e082015-12-07 13:25:27 -0800223
224 int left = 0;
225 int right = start;
226
227 while (left < right) {
228 int mid = (left + right) >>> 1;
229
230 final String lhs = pivotValue;
Ben Kwa6280de02015-12-16 19:42:08 -0800231 final String rhs = sortKey[mid];
Steve McKay55c00e72016-02-18 15:32:16 -0800232 final int compare = Shared.compareToIgnoreCaseNullable(lhs, rhs);
Ben Kwab8a5e082015-12-07 13:25:27 -0800233
234 if (compare < 0) {
235 right = mid;
236 } else {
237 left = mid + 1;
238 }
239 }
240
241 int n = start - left;
242 switch (n) {
243 case 2:
244 positions[left + 2] = positions[left + 1];
Ben Kwa6280de02015-12-16 19:42:08 -0800245 sortKey[left + 2] = sortKey[left + 1];
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900246 ids[left + 2] = ids[left + 1];
Ben Kwab8a5e082015-12-07 13:25:27 -0800247 case 1:
248 positions[left + 1] = positions[left];
Ben Kwa6280de02015-12-16 19:42:08 -0800249 sortKey[left + 1] = sortKey[left];
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900250 ids[left + 1] = ids[left];
Ben Kwab8a5e082015-12-07 13:25:27 -0800251 break;
252 default:
253 System.arraycopy(positions, left, positions, left + 1, n);
Ben Kwa6280de02015-12-16 19:42:08 -0800254 System.arraycopy(sortKey, left, sortKey, left + 1, n);
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900255 System.arraycopy(ids, left, ids, left + 1, n);
Ben Kwab8a5e082015-12-07 13:25:27 -0800256 }
257
258 positions[left] = pivotPosition;
Ben Kwa6280de02015-12-16 19:42:08 -0800259 sortKey[left] = pivotValue;
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900260 ids[left] = pivotId;
Ben Kwab8a5e082015-12-07 13:25:27 -0800261 }
262 }
263
264 /**
Ben Kwa6280de02015-12-16 19:42:08 -0800265 * Sorts model data. Takes four columns of index-corresponded data. The first column is the sort
266 * key, and the second is an array of mime types. The rows are first bucketed by mime type
267 * (directories vs documents) and then each bucket is sorted independently in descending
268 * numerical order on the sort key. This code is based on TimSort.binarySort().
269 *
270 * @param sortKey Data is sorted in descending numerical order.
271 * @param mimeTypes Corresponding mime types. Directories will be sorted ahead of documents.
272 * @param positions Cursor positions to be sorted.
273 * @param ids Model IDs to be sorted.
Ben Kwab8a5e082015-12-07 13:25:27 -0800274 */
Ben Kwa6280de02015-12-16 19:42:08 -0800275 private static void binarySort(
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900276 long[] sortKey, String[] mimeTypes, int[] positions, String[] ids) {
Ben Kwab8a5e082015-12-07 13:25:27 -0800277 final int count = positions.length;
278 for (int start = 1; start < count; start++) {
279 final int pivotPosition = positions[start];
Ben Kwa6280de02015-12-16 19:42:08 -0800280 final long pivotValue = sortKey[start];
281 final String pivotMime = mimeTypes[start];
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900282 final String pivotId = ids[start];
Ben Kwab8a5e082015-12-07 13:25:27 -0800283
284 int left = 0;
285 int right = start;
286
287 while (left < right) {
Ben Kwa6280de02015-12-16 19:42:08 -0800288 int mid = ((left + right) >>> 1);
Ben Kwab8a5e082015-12-07 13:25:27 -0800289
Ben Kwa6280de02015-12-16 19:42:08 -0800290 // First bucket by mime type. Directories always go in front.
291 int compare = 0;
292 final boolean lhsIsDir = Document.MIME_TYPE_DIR.equals(pivotMime);
293 final boolean rhsIsDir = Document.MIME_TYPE_DIR.equals(mimeTypes[mid]);
294 if (lhsIsDir && !rhsIsDir) {
295 compare = -1;
296 } else if (!lhsIsDir && rhsIsDir) {
297 compare = 1;
298 } else {
299 final long lhs = pivotValue;
300 final long rhs = sortKey[mid];
Ben Kwae4338342016-01-08 15:34:47 -0800301 // Sort in descending numerical order. This matches legacy behaviour, which
302 // yields largest or most recent items on top.
Ben Kwa6280de02015-12-16 19:42:08 -0800303 compare = -Long.compare(lhs, rhs);
304 }
Ben Kwab8a5e082015-12-07 13:25:27 -0800305
Ben Kwae4338342016-01-08 15:34:47 -0800306 // If numerical comparison yields a tie, use document ID as a tie breaker. This
307 // will yield stable results even if incoming items are continually shuffling and
308 // have identical numerical sort keys. One common example of this scenario is seen
309 // when sorting a set of active downloads by mod time.
310 if (compare == 0) {
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900311 compare = pivotId.compareTo(ids[mid]);
Ben Kwae4338342016-01-08 15:34:47 -0800312 }
313
Ben Kwab8a5e082015-12-07 13:25:27 -0800314 if (compare < 0) {
315 right = mid;
316 } else {
317 left = mid + 1;
318 }
319 }
320
321 int n = start - left;
322 switch (n) {
323 case 2:
324 positions[left + 2] = positions[left + 1];
Ben Kwa6280de02015-12-16 19:42:08 -0800325 sortKey[left + 2] = sortKey[left + 1];
326 mimeTypes[left + 2] = mimeTypes[left + 1];
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900327 ids[left + 2] = ids[left + 1];
Ben Kwab8a5e082015-12-07 13:25:27 -0800328 case 1:
329 positions[left + 1] = positions[left];
Ben Kwa6280de02015-12-16 19:42:08 -0800330 sortKey[left + 1] = sortKey[left];
331 mimeTypes[left + 1] = mimeTypes[left];
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900332 ids[left + 1] = ids[left];
Ben Kwab8a5e082015-12-07 13:25:27 -0800333 break;
334 default:
335 System.arraycopy(positions, left, positions, left + 1, n);
Ben Kwa6280de02015-12-16 19:42:08 -0800336 System.arraycopy(sortKey, left, sortKey, left + 1, n);
337 System.arraycopy(mimeTypes, left, mimeTypes, left + 1, n);
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900338 System.arraycopy(ids, left, ids, left + 1, n);
Ben Kwab8a5e082015-12-07 13:25:27 -0800339 }
340
341 positions[left] = pivotPosition;
Ben Kwa6280de02015-12-16 19:42:08 -0800342 sortKey[left] = pivotValue;
343 mimeTypes[left] = pivotMime;
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900344 ids[left] = pivotId;
Ben Kwa0497da82015-11-30 23:00:02 -0800345 }
346 }
347
Ben Kwae4338342016-01-08 15:34:47 -0800348 /**
349 * @return Timestamp for the given document. Some docs (e.g. active downloads) have a null
350 * timestamp - these will be replaced with MAX_LONG so that such files get sorted to the top
351 * when sorting by date.
352 */
353 long getLastModified(Cursor cursor) {
354 long l = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
355 return (l == -1) ? Long.MAX_VALUE : l;
356 }
357
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +0900358 public @Nullable Cursor getItem(String modelId) {
Ben Kwa0497da82015-11-30 23:00:02 -0800359 Integer pos = mPositions.get(modelId);
360 if (pos != null) {
361 mCursor.moveToPosition(pos);
362 return mCursor;
363 }
364 return null;
365 }
366
Ben Kwa0497da82015-11-30 23:00:02 -0800367 boolean isEmpty() {
368 return mCursorCount == 0;
369 }
370
371 boolean isLoading() {
372 return mIsLoading;
373 }
374
375 List<DocumentInfo> getDocuments(Selection items) {
376 final int size = (items != null) ? items.size() : 0;
377
378 final List<DocumentInfo> docs = new ArrayList<>(size);
Ben Kwad72a1da2015-12-01 19:56:57 -0800379 for (String modelId: items.getAll()) {
380 final Cursor cursor = getItem(modelId);
Steve McKay0af8afd2016-02-25 13:34:03 -0800381 assert(cursor != null);
382
383 docs.add(DocumentInfo.fromDirectoryCursor(cursor));
Ben Kwa0497da82015-11-30 23:00:02 -0800384 }
385 return docs;
386 }
387
Ben Kwa0497da82015-11-30 23:00:02 -0800388 void addUpdateListener(UpdateListener listener) {
Ben Kwad72a1da2015-12-01 19:56:57 -0800389 mUpdateListeners.add(listener);
Ben Kwa0497da82015-11-30 23:00:02 -0800390 }
391
Ben Kwa472103f2016-02-10 15:48:25 -0800392 void removeUpdateListener(UpdateListener listener) {
393 mUpdateListeners.remove(listener);
394 }
395
Ben Kwad72a1da2015-12-01 19:56:57 -0800396 static interface UpdateListener {
Ben Kwa0497da82015-11-30 23:00:02 -0800397 /**
398 * Called when a successful update has occurred.
399 */
Ben Kwad72a1da2015-12-01 19:56:57 -0800400 void onModelUpdate(Model model);
Ben Kwa0497da82015-11-30 23:00:02 -0800401
402 /**
403 * Called when an update has been attempted but failed.
404 */
Ben Kwad72a1da2015-12-01 19:56:57 -0800405 void onModelUpdateFailed(Exception e);
Ben Kwa0497da82015-11-30 23:00:02 -0800406 }
Ben Kwab8a5e082015-12-07 13:25:27 -0800407
408 /**
409 * @return An ordered array of model IDs representing the documents in the model. It is sorted
410 * according to the current sort order, which was set by the last model update.
411 */
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900412 public String[] getModelIds() {
Ben Kwab8a5e082015-12-07 13:25:27 -0800413 return mIds;
414 }
Ben Kwa0497da82015-11-30 23:00:02 -0800415}