blob: 0d4d37e9ef4f9f551c946232d263de34cdca12d7 [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;
25import static com.android.internal.util.Preconditions.checkNotNull;
Ben Kwa0497da82015-11-30 23:00:02 -080026
27import android.content.ContentProviderClient;
28import android.content.ContentResolver;
29import android.content.Context;
30import android.database.Cursor;
31import android.os.AsyncTask;
32import android.os.Bundle;
33import android.os.Looper;
34import android.provider.DocumentsContract;
35import android.provider.DocumentsContract.Document;
36import android.support.annotation.Nullable;
37import android.support.annotation.VisibleForTesting;
38import android.support.v7.widget.RecyclerView;
39import android.util.Log;
Ben Kwa0497da82015-11-30 23:00:02 -080040
Ben Kwab8a5e082015-12-07 13:25:27 -080041import com.android.documentsui.BaseActivity.SiblingProvider;
Ben Kwa0497da82015-11-30 23:00:02 -080042import com.android.documentsui.DirectoryResult;
43import com.android.documentsui.DocumentsApplication;
44import com.android.documentsui.RootCursorWrapper;
45import com.android.documentsui.dirlist.MultiSelectManager.Selection;
46import com.android.documentsui.model.DocumentInfo;
Ben Kwa0497da82015-11-30 23:00:02 -080047
48import java.util.ArrayList;
Ben Kwa0497da82015-11-30 23:00:02 -080049import java.util.HashMap;
50import java.util.List;
Ben Kwab8a5e082015-12-07 13:25:27 -080051import java.util.Map;
Ben Kwa0497da82015-11-30 23:00:02 -080052
53/**
54 * The data model for the current loaded directory.
55 */
56@VisibleForTesting
Ben Kwab8a5e082015-12-07 13:25:27 -080057public class Model implements SiblingProvider {
Ben Kwa0497da82015-11-30 23:00:02 -080058 private static final String TAG = "Model";
Ben Kwab8a5e082015-12-07 13:25:27 -080059
Ben Kwa0497da82015-11-30 23:00:02 -080060 private Context mContext;
Ben Kwa0497da82015-11-30 23:00:02 -080061 private boolean mIsLoading;
Ben Kwad72a1da2015-12-01 19:56:57 -080062 private List<UpdateListener> mUpdateListeners = new ArrayList<>();
Ben Kwa0497da82015-11-30 23:00:02 -080063 @Nullable private Cursor mCursor;
Ben Kwab8a5e082015-12-07 13:25:27 -080064 private int mCursorCount;
65 /** Maps Model ID to cursor positions, for looking up items by Model ID. */
66 private Map<String, Integer> mPositions = new HashMap<>();
67 /**
68 * A sorted array of model IDs for the files currently in the Model. Sort order is determined
69 * by {@link #mSortOrder}
70 */
71 private List<String> mIds = new ArrayList<>();
72 private int mSortOrder = SORT_ORDER_DISPLAY_NAME;
73
Ben Kwa0497da82015-11-30 23:00:02 -080074 @Nullable String info;
75 @Nullable String error;
Ben Kwa0497da82015-11-30 23:00:02 -080076
77 Model(Context context, RecyclerView.Adapter<?> viewAdapter) {
78 mContext = context;
Ben Kwa0497da82015-11-30 23:00:02 -080079 }
80
Ben Kwad72a1da2015-12-01 19:56:57 -080081 /**
Ben Kwab8a5e082015-12-07 13:25:27 -080082 * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
83 * string that can be used to identify the document referred to by the cursor.
Ben Kwad72a1da2015-12-01 19:56:57 -080084 *
85 * @param c A cursor that refers to a document.
86 */
Ben Kwab8a5e082015-12-07 13:25:27 -080087 private static String createModelId(Cursor c) {
88 // TODO: Maybe more efficient to use just the document ID, in cases where there is only one
89 // authority (which should be the majority of cases).
Ben Kwad72a1da2015-12-01 19:56:57 -080090 return getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY) +
91 "|" + getCursorString(c, Document.COLUMN_DOCUMENT_ID);
92 }
93
Ben Kwad72a1da2015-12-01 19:56:57 -080094 private void notifyUpdateListeners() {
95 for (UpdateListener listener: mUpdateListeners) {
96 listener.onModelUpdate(this);
97 }
98 }
99
100 private void notifyUpdateListeners(Exception e) {
101 for (UpdateListener listener: mUpdateListeners) {
102 listener.onModelUpdateFailed(e);
103 }
104 }
105
Ben Kwa0497da82015-11-30 23:00:02 -0800106 void update(DirectoryResult result) {
107 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
108
109 if (result == null) {
110 mCursor = null;
111 mCursorCount = 0;
Ben Kwab8a5e082015-12-07 13:25:27 -0800112 mIds.clear();
113 mPositions.clear();
Ben Kwa0497da82015-11-30 23:00:02 -0800114 info = null;
115 error = null;
116 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;
Ben Kwa0497da82015-11-30 23:00:02 -0800130
Ben Kwab8a5e082015-12-07 13:25:27 -0800131 updateModelData();
Ben Kwa0497da82015-11-30 23:00:02 -0800132
133 final Bundle extras = mCursor.getExtras();
134 if (extras != null) {
135 info = extras.getString(DocumentsContract.EXTRA_INFO);
136 error = extras.getString(DocumentsContract.EXTRA_ERROR);
137 mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
138 }
139
Ben Kwad72a1da2015-12-01 19:56:57 -0800140 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800141 }
142
Ben Kwad72a1da2015-12-01 19:56:57 -0800143 @VisibleForTesting
Ben Kwa0497da82015-11-30 23:00:02 -0800144 int getItemCount() {
Ben Kwada858bf2015-12-09 14:33:49 -0800145 return mCursorCount;
Ben Kwa0497da82015-11-30 23:00:02 -0800146 }
147
148 /**
Ben Kwab8a5e082015-12-07 13:25:27 -0800149 * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
150 * according to the current sort order.
Ben Kwa0497da82015-11-30 23:00:02 -0800151 */
Ben Kwab8a5e082015-12-07 13:25:27 -0800152 private void updateModelData() {
153 int[] positions = new int[mCursorCount];
154 mIds.clear();
155 String[] strings = null;
156 long[] longs = null;
157
158 switch (mSortOrder) {
159 case SORT_ORDER_DISPLAY_NAME:
160 strings = new String[mCursorCount];
161 break;
162 case SORT_ORDER_LAST_MODIFIED:
163 case SORT_ORDER_SIZE:
164 longs = new long[mCursorCount];
165 break;
166 }
167
Ben Kwa0497da82015-11-30 23:00:02 -0800168 mCursor.moveToPosition(-1);
169 for (int pos = 0; pos < mCursorCount; ++pos) {
170 mCursor.moveToNext();
Ben Kwab8a5e082015-12-07 13:25:27 -0800171 positions[pos] = pos;
172 mIds.add(createModelId(mCursor));
173
174 switch(mSortOrder) {
175 case SORT_ORDER_DISPLAY_NAME:
176 final String mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
177 final String displayName = getCursorString(
178 mCursor, Document.COLUMN_DISPLAY_NAME);
179 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
180 strings[pos] = DocumentInfo.DIR_PREFIX + displayName;
181 } else {
182 strings[pos] = displayName;
183 }
184 break;
185 case SORT_ORDER_LAST_MODIFIED:
186 longs[pos] = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
187 break;
188 case SORT_ORDER_SIZE:
189 longs[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
190 break;
191 }
192 }
193
194 switch (mSortOrder) {
195 case SORT_ORDER_DISPLAY_NAME:
196 binarySort(positions, strings, mIds);
197 break;
198 case SORT_ORDER_LAST_MODIFIED:
199 case SORT_ORDER_SIZE:
200 binarySort(positions, longs, mIds);
201 break;
202 }
203
204 // Populate the positions.
205 mPositions.clear();
206 for (int i = 0; i < mCursorCount; ++i) {
207 mPositions.put(mIds.get(i), positions[i]);
208 }
209 }
210
211 /**
212 * Borrowed from TimSort.binarySort(), but modified to sort three-column data set.
213 */
214 private static void binarySort(int[] positions, String[] strings, List<String> ids) {
215 final int count = positions.length;
216 for (int start = 1; start < count; start++) {
217 final int pivotPosition = positions[start];
218 final String pivotValue = strings[start];
219 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;
228 final String rhs = strings[mid];
229 final int compare = DocumentInfo.compareToIgnoreCaseNullable(lhs, rhs);
230
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];
242 strings[left + 2] = strings[left + 1];
243 ids.set(left + 2, ids.get(left + 1));
244 case 1:
245 positions[left + 1] = positions[left];
246 strings[left + 1] = strings[left];
247 ids.set(left + 1, ids.get(left));
248 break;
249 default:
250 System.arraycopy(positions, left, positions, left + 1, n);
251 System.arraycopy(strings, left, strings, left + 1, n);
252 for (int i = n; i >= 1; --i) {
253 ids.set(left + i, ids.get(left + i - 1));
254 }
255 }
256
257 positions[left] = pivotPosition;
258 strings[left] = pivotValue;
259 ids.set(left, pivotId);
260 }
261 }
262
263 /**
264 * Borrowed from TimSort.binarySort(), but modified to sort three-column data set.
265 */
266 private static void binarySort(int[] positions, long[] longs, List<String> ids) {
267 final int count = positions.length;
268 for (int start = 1; start < count; start++) {
269 final int pivotPosition = positions[start];
270 final long pivotValue = longs[start];
271 final String pivotId = ids.get(start);
272
273 int left = 0;
274 int right = start;
275
276 while (left < right) {
277 int mid = (left + right) >>> 1;
278
279 final long lhs = pivotValue;
280 final long rhs = longs[mid];
281 // Sort in descending numerical order. This matches legacy behaviour, which yields
282 // largest or most recent items on top.
283 final int compare = -Long.compare(lhs, rhs);
284
285 if (compare < 0) {
286 right = mid;
287 } else {
288 left = mid + 1;
289 }
290 }
291
292 int n = start - left;
293 switch (n) {
294 case 2:
295 positions[left + 2] = positions[left + 1];
296 longs[left + 2] = longs[left + 1];
297 ids.set(left + 2, ids.get(left + 1));
298 case 1:
299 positions[left + 1] = positions[left];
300 longs[left + 1] = longs[left];
301 ids.set(left + 1, ids.get(left));
302 break;
303 default:
304 System.arraycopy(positions, left, positions, left + 1, n);
305 System.arraycopy(longs, left, longs, left + 1, n);
306 for (int i = n; i >= 1; --i) {
307 ids.set(left + i, ids.get(left + i - 1));
308 }
309 }
310
311 positions[left] = pivotPosition;
312 longs[left] = pivotValue;
313 ids.set(left, pivotId);
Ben Kwa0497da82015-11-30 23:00:02 -0800314 }
315 }
316
317 @Nullable Cursor getItem(String modelId) {
318 Integer pos = mPositions.get(modelId);
319 if (pos != null) {
320 mCursor.moveToPosition(pos);
321 return mCursor;
322 }
323 return null;
324 }
325
Ben Kwa0497da82015-11-30 23:00:02 -0800326 boolean isEmpty() {
327 return mCursorCount == 0;
328 }
329
330 boolean isLoading() {
331 return mIsLoading;
332 }
333
334 List<DocumentInfo> getDocuments(Selection items) {
335 final int size = (items != null) ? items.size() : 0;
336
337 final List<DocumentInfo> docs = new ArrayList<>(size);
Ben Kwad72a1da2015-12-01 19:56:57 -0800338 for (String modelId: items.getAll()) {
339 final Cursor cursor = getItem(modelId);
Ben Kwa0497da82015-11-30 23:00:02 -0800340 checkNotNull(cursor, "Cursor cannot be null.");
341 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
342 docs.add(doc);
343 }
344 return docs;
345 }
346
347 @Override
348 public Cursor getCursor() {
349 if (Looper.myLooper() != Looper.getMainLooper()) {
350 throw new IllegalStateException("Can't call getCursor from non-main thread.");
351 }
352 return mCursor;
353 }
354
Ben Kwada858bf2015-12-09 14:33:49 -0800355 public void delete(Selection selected, DeletionListener listener) {
356 final ContentResolver resolver = mContext.getContentResolver();
357 new DeleteFilesTask(resolver, listener).execute(selected);
Ben Kwa0497da82015-11-30 23:00:02 -0800358 }
359
360 /**
361 * A Task which collects the DocumentInfo for documents that have been marked for deletion,
362 * and actually deletes them.
363 */
Ben Kwada858bf2015-12-09 14:33:49 -0800364 private class DeleteFilesTask extends AsyncTask<Selection, Void, Void> {
Ben Kwa0497da82015-11-30 23:00:02 -0800365 private ContentResolver mResolver;
366 private DeletionListener mListener;
Ben Kwada858bf2015-12-09 14:33:49 -0800367 private boolean mHadTrouble;
Ben Kwa0497da82015-11-30 23:00:02 -0800368
369 /**
370 * @param resolver A ContentResolver for performing the actual file deletions.
371 * @param errorCallback A Runnable that is executed in the event that one or more errors
Ben Kwada858bf2015-12-09 14:33:49 -0800372 * occurred while copying files. Execution will occur on the UI thread.
Ben Kwa0497da82015-11-30 23:00:02 -0800373 */
374 public DeleteFilesTask(ContentResolver resolver, DeletionListener listener) {
375 mResolver = resolver;
376 mListener = listener;
377 }
378
379 @Override
Ben Kwada858bf2015-12-09 14:33:49 -0800380 protected Void doInBackground(Selection... selected) {
381 List<DocumentInfo> toDelete = getDocuments(selected[0]);
382 mHadTrouble = false;
Ben Kwa0497da82015-11-30 23:00:02 -0800383
Ben Kwada858bf2015-12-09 14:33:49 -0800384 for (DocumentInfo doc : toDelete) {
Ben Kwa0497da82015-11-30 23:00:02 -0800385 if (!doc.isDeleteSupported()) {
386 Log.w(TAG, doc + " could not be deleted. Skipping...");
Ben Kwada858bf2015-12-09 14:33:49 -0800387 mHadTrouble = true;
Ben Kwa0497da82015-11-30 23:00:02 -0800388 continue;
389 }
390
391 ContentProviderClient client = null;
392 try {
393 if (DEBUG) Log.d(TAG, "Deleting: " + doc.displayName);
394 client = DocumentsApplication.acquireUnstableProviderOrThrow(
395 mResolver, doc.derivedUri.getAuthority());
396 DocumentsContract.deleteDocument(client, doc.derivedUri);
397 } catch (Exception e) {
Ben Kwada858bf2015-12-09 14:33:49 -0800398 Log.w(TAG, "Failed to delete " + doc, e);
399 mHadTrouble = true;
Ben Kwa0497da82015-11-30 23:00:02 -0800400 } finally {
401 ContentProviderClient.releaseQuietly(client);
402 }
403 }
404
Ben Kwada858bf2015-12-09 14:33:49 -0800405 return null;
406 }
407
408 @Override
409 protected void onPostExecute(Void _) {
410 if (mHadTrouble) {
Ben Kwa0497da82015-11-30 23:00:02 -0800411 // TODO show which files failed? b/23720103
412 mListener.onError();
413 if (DEBUG) Log.d(TAG, "Deletion task completed. Some deletions failed.");
414 } else {
415 if (DEBUG) Log.d(TAG, "Deletion task completed successfully.");
416 }
Ben Kwa0497da82015-11-30 23:00:02 -0800417
418 mListener.onCompletion();
419 }
420 }
421
422 static class DeletionListener {
423 /**
424 * Called when deletion has completed (regardless of whether an error occurred).
425 */
426 void onCompletion() {}
427
428 /**
429 * Called at the end of a deletion operation that produced one or more errors.
430 */
431 void onError() {}
432 }
433
434 void addUpdateListener(UpdateListener listener) {
Ben Kwad72a1da2015-12-01 19:56:57 -0800435 mUpdateListeners.add(listener);
Ben Kwa0497da82015-11-30 23:00:02 -0800436 }
437
Ben Kwad72a1da2015-12-01 19:56:57 -0800438 static interface UpdateListener {
Ben Kwa0497da82015-11-30 23:00:02 -0800439 /**
440 * Called when a successful update has occurred.
441 */
Ben Kwad72a1da2015-12-01 19:56:57 -0800442 void onModelUpdate(Model model);
Ben Kwa0497da82015-11-30 23:00:02 -0800443
444 /**
445 * Called when an update has been attempted but failed.
446 */
Ben Kwad72a1da2015-12-01 19:56:57 -0800447 void onModelUpdateFailed(Exception e);
Ben Kwa0497da82015-11-30 23:00:02 -0800448 }
Ben Kwab8a5e082015-12-07 13:25:27 -0800449
450 /**
451 * @return An ordered array of model IDs representing the documents in the model. It is sorted
452 * according to the current sort order, which was set by the last model update.
453 */
454 public List<String> getModelIds() {
455 return mIds;
456 }
Ben Kwa0497da82015-11-30 23:00:02 -0800457}