blob: 49691d416b220ec3746f207f799b4f7caa2aa9d0 [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;
20import static com.android.documentsui.model.DocumentInfo.getCursorString;
21import static com.android.internal.util.Preconditions.checkNotNull;
22import static com.android.internal.util.Preconditions.checkState;
23
24import android.content.ContentProviderClient;
25import android.content.ContentResolver;
26import android.content.Context;
27import android.database.Cursor;
28import android.os.AsyncTask;
29import android.os.Bundle;
30import android.os.Looper;
31import android.provider.DocumentsContract;
32import android.provider.DocumentsContract.Document;
33import android.support.annotation.Nullable;
34import android.support.annotation.VisibleForTesting;
35import android.support.v7.widget.RecyclerView;
36import android.util.Log;
Ben Kwad72a1da2015-12-01 19:56:57 -080037import android.util.SparseArray;
Ben Kwa0497da82015-11-30 23:00:02 -080038
39import com.android.documentsui.BaseActivity.DocumentContext;
40import com.android.documentsui.DirectoryResult;
41import com.android.documentsui.DocumentsApplication;
42import com.android.documentsui.RootCursorWrapper;
43import com.android.documentsui.dirlist.MultiSelectManager.Selection;
44import com.android.documentsui.model.DocumentInfo;
45import com.android.internal.annotations.GuardedBy;
46
47import java.util.ArrayList;
Ben Kwa0497da82015-11-30 23:00:02 -080048import java.util.HashMap;
Ben Kwad72a1da2015-12-01 19:56:57 -080049import java.util.HashSet;
Ben Kwa0497da82015-11-30 23:00:02 -080050import java.util.List;
Ben Kwad72a1da2015-12-01 19:56:57 -080051import java.util.Set;
Ben Kwa0497da82015-11-30 23:00:02 -080052
53/**
54 * The data model for the current loaded directory.
55 */
56@VisibleForTesting
57public class Model implements DocumentContext {
58 private static final String TAG = "Model";
59 private RecyclerView.Adapter<?> mViewAdapter;
60 private Context mContext;
61 private int mCursorCount;
62 private boolean mIsLoading;
63 @GuardedBy("mPendingDelete")
64 private Boolean mPendingDelete = false;
65 @GuardedBy("mPendingDelete")
Ben Kwad72a1da2015-12-01 19:56:57 -080066 private Set<String> mMarkedForDeletion = new HashSet<>();
67 private List<UpdateListener> mUpdateListeners = new ArrayList<>();
Ben Kwa0497da82015-11-30 23:00:02 -080068 @Nullable private Cursor mCursor;
69 @Nullable String info;
70 @Nullable String error;
71 private HashMap<String, Integer> mPositions = new HashMap<>();
72
73 Model(Context context, RecyclerView.Adapter<?> viewAdapter) {
74 mContext = context;
75 mViewAdapter = viewAdapter;
76 }
77
Ben Kwad72a1da2015-12-01 19:56:57 -080078 /**
79 * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
80 * unique string that can be used to identify the document referred to by the cursor.
81 *
82 * @param c A cursor that refers to a document.
83 */
84 public static String createId(Cursor c) {
85 return getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY) +
86 "|" + getCursorString(c, Document.COLUMN_DOCUMENT_ID);
87 }
88
89 /**
90 * @return Model IDs for all known items in the model. Note that this will include items
91 * pending deletion.
92 */
93 public Set<String> getIds() {
94 return mPositions.keySet();
95 }
96
97 private void notifyUpdateListeners() {
98 for (UpdateListener listener: mUpdateListeners) {
99 listener.onModelUpdate(this);
100 }
101 }
102
103 private void notifyUpdateListeners(Exception e) {
104 for (UpdateListener listener: mUpdateListeners) {
105 listener.onModelUpdateFailed(e);
106 }
107 }
108
Ben Kwa0497da82015-11-30 23:00:02 -0800109 void update(DirectoryResult result) {
110 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
111
112 if (result == null) {
113 mCursor = null;
114 mCursorCount = 0;
115 info = null;
116 error = null;
117 mIsLoading = false;
Ben Kwad72a1da2015-12-01 19:56:57 -0800118 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800119 return;
120 }
121
122 if (result.exception != null) {
123 Log.e(TAG, "Error while loading directory contents", result.exception);
Ben Kwad72a1da2015-12-01 19:56:57 -0800124 notifyUpdateListeners(result.exception);
Ben Kwa0497da82015-11-30 23:00:02 -0800125 return;
126 }
127
128 mCursor = result.cursor;
129 mCursorCount = mCursor.getCount();
130
131 updatePositions();
132
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() {
145 synchronized(mPendingDelete) {
146 return mCursorCount - mMarkedForDeletion.size();
147 }
148 }
149
150 /**
151 * Update the ModelId-position map.
152 */
153 private void updatePositions() {
154 mPositions.clear();
155 mCursor.moveToPosition(-1);
156 for (int pos = 0; pos < mCursorCount; ++pos) {
157 mCursor.moveToNext();
Ben Kwad72a1da2015-12-01 19:56:57 -0800158 mPositions.put(Model.createId(mCursor), pos);
Ben Kwa0497da82015-11-30 23:00:02 -0800159 }
160 }
161
162 @Nullable Cursor getItem(String modelId) {
163 Integer pos = mPositions.get(modelId);
164 if (pos != null) {
165 mCursor.moveToPosition(pos);
166 return mCursor;
167 }
168 return null;
169 }
170
Ben Kwa0497da82015-11-30 23:00:02 -0800171 boolean isEmpty() {
172 return mCursorCount == 0;
173 }
174
175 boolean isLoading() {
176 return mIsLoading;
177 }
178
179 List<DocumentInfo> getDocuments(Selection items) {
180 final int size = (items != null) ? items.size() : 0;
181
182 final List<DocumentInfo> docs = new ArrayList<>(size);
Ben Kwad72a1da2015-12-01 19:56:57 -0800183 for (String modelId: items.getAll()) {
184 final Cursor cursor = getItem(modelId);
Ben Kwa0497da82015-11-30 23:00:02 -0800185 checkNotNull(cursor, "Cursor cannot be null.");
186 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
187 docs.add(doc);
188 }
189 return docs;
190 }
191
192 @Override
193 public Cursor getCursor() {
194 if (Looper.myLooper() != Looper.getMainLooper()) {
195 throw new IllegalStateException("Can't call getCursor from non-main thread.");
196 }
197 return mCursor;
198 }
199
200 List<DocumentInfo> getDocumentsMarkedForDeletion() {
Ben Kwad72a1da2015-12-01 19:56:57 -0800201 // TODO(stable-id): This could be just a plain old selection now.
Ben Kwa0497da82015-11-30 23:00:02 -0800202 synchronized (mPendingDelete) {
203 final int size = mMarkedForDeletion.size();
204 List<DocumentInfo> docs = new ArrayList<>(size);
205
Ben Kwad72a1da2015-12-01 19:56:57 -0800206 for (String id: mMarkedForDeletion) {
207 Integer position = mPositions.get(id);
208 checkState(position != null);
Ben Kwa0497da82015-11-30 23:00:02 -0800209 mCursor.moveToPosition(position);
210 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(mCursor);
211 docs.add(doc);
212 }
213 return docs;
214 }
215 }
216
217 /**
218 * Marks the given files for deletion. This will remove them from the UI. Clients must then
219 * call either {@link #undoDeletion()} or {@link #finalizeDeletion()} to cancel or confirm
220 * the deletion, respectively. Only one deletion operation is allowed at a time.
221 *
222 * @param selected A selection representing the files to delete.
223 */
224 void markForDeletion(Selection selected) {
225 synchronized (mPendingDelete) {
226 mPendingDelete = true;
227 // Only one deletion operation at a time.
228 checkState(mMarkedForDeletion.size() == 0);
229 // There should never be more to delete than what exists.
230 checkState(mCursorCount >= selected.size());
231
Ben Kwad72a1da2015-12-01 19:56:57 -0800232 // Adapter notifications must be sent in reverse order of adapter position. This is
233 // because each removal causes subsequent item adapter positions to change.
234 SparseArray<String> ids = new SparseArray<>();
235 for (int i = ids.size() - 1; i >= 0; i--) {
236 int pos = ids.keyAt(i);
237 mMarkedForDeletion.add(ids.get(pos));
238 mViewAdapter.notifyItemRemoved(pos);
239 if (DEBUG) Log.d(TAG, "Scheduled " + pos + " for delete.");
Ben Kwa0497da82015-11-30 23:00:02 -0800240 }
241 }
242 }
243
244 /**
245 * Cancels an ongoing deletion operation. All files currently marked for deletion will be
246 * unmarked, and restored in the UI. See {@link #markForDeletion(Selection)}.
247 */
248 void undoDeletion() {
249 synchronized (mPendingDelete) {
250 // Iterate over deleted items, temporarily marking them false in the deletion list, and
251 // re-adding them to the UI.
Ben Kwad72a1da2015-12-01 19:56:57 -0800252 for (String id: mMarkedForDeletion) {
253 Integer pos= mPositions.get(id);
254 checkNotNull(pos);
255 mMarkedForDeletion.remove(id);
256 mViewAdapter.notifyItemInserted(pos);
Ben Kwa0497da82015-11-30 23:00:02 -0800257 }
258 resetDeleteData();
259 }
260 }
261
262 private void resetDeleteData() {
263 synchronized (mPendingDelete) {
264 mPendingDelete = false;
265 mMarkedForDeletion.clear();
266 }
267 }
268
269 /**
270 * Finalizes an ongoing deletion operation. All files currently marked for deletion will be
271 * deleted. See {@link #markForDeletion(Selection)}.
272 *
273 * @param view The view which will be used to interact with the user (e.g. surfacing
274 * snackbars) for errors, info, etc.
275 */
276 void finalizeDeletion(DeletionListener listener) {
277 synchronized (mPendingDelete) {
278 if (mPendingDelete) {
279 // Necessary to avoid b/25072545. Even when that's resolved, this
280 // is a nice safe thing to day.
281 mPendingDelete = false;
282 final ContentResolver resolver = mContext.getContentResolver();
283 DeleteFilesTask task = new DeleteFilesTask(resolver, listener);
284 task.execute();
285 }
286 }
287 }
288
289 /**
290 * A Task which collects the DocumentInfo for documents that have been marked for deletion,
291 * and actually deletes them.
292 */
293 private class DeleteFilesTask extends AsyncTask<Void, Void, List<DocumentInfo>> {
294 private ContentResolver mResolver;
295 private DeletionListener mListener;
296
297 /**
298 * @param resolver A ContentResolver for performing the actual file deletions.
299 * @param errorCallback A Runnable that is executed in the event that one or more errors
300 * occured while copying files. Execution will occur on the UI thread.
301 */
302 public DeleteFilesTask(ContentResolver resolver, DeletionListener listener) {
303 mResolver = resolver;
304 mListener = listener;
305 }
306
307 @Override
308 protected List<DocumentInfo> doInBackground(Void... params) {
309 return getDocumentsMarkedForDeletion();
310 }
311
312 @Override
313 protected void onPostExecute(List<DocumentInfo> docs) {
314 boolean hadTrouble = false;
315 for (DocumentInfo doc : docs) {
316 if (!doc.isDeleteSupported()) {
317 Log.w(TAG, doc + " could not be deleted. Skipping...");
318 hadTrouble = true;
319 continue;
320 }
321
322 ContentProviderClient client = null;
323 try {
324 if (DEBUG) Log.d(TAG, "Deleting: " + doc.displayName);
325 client = DocumentsApplication.acquireUnstableProviderOrThrow(
326 mResolver, doc.derivedUri.getAuthority());
327 DocumentsContract.deleteDocument(client, doc.derivedUri);
328 } catch (Exception e) {
329 Log.w(TAG, "Failed to delete " + doc);
330 hadTrouble = true;
331 } finally {
332 ContentProviderClient.releaseQuietly(client);
333 }
334 }
335
336 if (hadTrouble) {
337 // TODO show which files failed? b/23720103
338 mListener.onError();
339 if (DEBUG) Log.d(TAG, "Deletion task completed. Some deletions failed.");
340 } else {
341 if (DEBUG) Log.d(TAG, "Deletion task completed successfully.");
342 }
343 resetDeleteData();
344
345 mListener.onCompletion();
346 }
347 }
348
349 static class DeletionListener {
350 /**
351 * Called when deletion has completed (regardless of whether an error occurred).
352 */
353 void onCompletion() {}
354
355 /**
356 * Called at the end of a deletion operation that produced one or more errors.
357 */
358 void onError() {}
359 }
360
361 void addUpdateListener(UpdateListener listener) {
Ben Kwad72a1da2015-12-01 19:56:57 -0800362 mUpdateListeners.add(listener);
Ben Kwa0497da82015-11-30 23:00:02 -0800363 }
364
Ben Kwad72a1da2015-12-01 19:56:57 -0800365 static interface UpdateListener {
Ben Kwa0497da82015-11-30 23:00:02 -0800366 /**
367 * Called when a successful update has occurred.
368 */
Ben Kwad72a1da2015-12-01 19:56:57 -0800369 void onModelUpdate(Model model);
Ben Kwa0497da82015-11-30 23:00:02 -0800370
371 /**
372 * Called when an update has been attempted but failed.
373 */
Ben Kwad72a1da2015-12-01 19:56:57 -0800374 void onModelUpdateFailed(Exception e);
Ben Kwa0497da82015-11-30 23:00:02 -0800375 }
376}