blob: fce16c63979b2de65d3505a3c159507a7b4b921f [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;
Ben Kwa0497da82015-11-30 23:00:02 -080022
23import android.content.ContentProviderClient;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.database.Cursor;
27import android.os.AsyncTask;
28import android.os.Bundle;
29import android.os.Looper;
30import android.provider.DocumentsContract;
31import android.provider.DocumentsContract.Document;
32import android.support.annotation.Nullable;
33import android.support.annotation.VisibleForTesting;
34import android.support.v7.widget.RecyclerView;
35import android.util.Log;
Ben Kwa0497da82015-11-30 23:00:02 -080036
37import com.android.documentsui.BaseActivity.DocumentContext;
38import com.android.documentsui.DirectoryResult;
39import com.android.documentsui.DocumentsApplication;
40import com.android.documentsui.RootCursorWrapper;
41import com.android.documentsui.dirlist.MultiSelectManager.Selection;
42import com.android.documentsui.model.DocumentInfo;
Ben Kwa0497da82015-11-30 23:00:02 -080043
44import java.util.ArrayList;
Ben Kwa0497da82015-11-30 23:00:02 -080045import java.util.HashMap;
46import java.util.List;
Ben Kwad72a1da2015-12-01 19:56:57 -080047import java.util.Set;
Ben Kwa0497da82015-11-30 23:00:02 -080048
49/**
50 * The data model for the current loaded directory.
51 */
52@VisibleForTesting
53public class Model implements DocumentContext {
54 private static final String TAG = "Model";
Ben Kwa0497da82015-11-30 23:00:02 -080055 private Context mContext;
56 private int mCursorCount;
57 private boolean mIsLoading;
Ben Kwad72a1da2015-12-01 19:56:57 -080058 private List<UpdateListener> mUpdateListeners = new ArrayList<>();
Ben Kwa0497da82015-11-30 23:00:02 -080059 @Nullable private Cursor mCursor;
60 @Nullable String info;
61 @Nullable String error;
62 private HashMap<String, Integer> mPositions = new HashMap<>();
63
64 Model(Context context, RecyclerView.Adapter<?> viewAdapter) {
65 mContext = context;
Ben Kwa0497da82015-11-30 23:00:02 -080066 }
67
Ben Kwad72a1da2015-12-01 19:56:57 -080068 /**
69 * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
70 * unique string that can be used to identify the document referred to by the cursor.
71 *
72 * @param c A cursor that refers to a document.
73 */
74 public static String createId(Cursor c) {
75 return getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY) +
76 "|" + getCursorString(c, Document.COLUMN_DOCUMENT_ID);
77 }
78
79 /**
80 * @return Model IDs for all known items in the model. Note that this will include items
81 * pending deletion.
82 */
83 public Set<String> getIds() {
84 return mPositions.keySet();
85 }
86
87 private void notifyUpdateListeners() {
88 for (UpdateListener listener: mUpdateListeners) {
89 listener.onModelUpdate(this);
90 }
91 }
92
93 private void notifyUpdateListeners(Exception e) {
94 for (UpdateListener listener: mUpdateListeners) {
95 listener.onModelUpdateFailed(e);
96 }
97 }
98
Ben Kwa0497da82015-11-30 23:00:02 -080099 void update(DirectoryResult result) {
100 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
101
102 if (result == null) {
103 mCursor = null;
104 mCursorCount = 0;
105 info = null;
106 error = null;
107 mIsLoading = false;
Ben Kwad72a1da2015-12-01 19:56:57 -0800108 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800109 return;
110 }
111
112 if (result.exception != null) {
113 Log.e(TAG, "Error while loading directory contents", result.exception);
Ben Kwad72a1da2015-12-01 19:56:57 -0800114 notifyUpdateListeners(result.exception);
Ben Kwa0497da82015-11-30 23:00:02 -0800115 return;
116 }
117
118 mCursor = result.cursor;
119 mCursorCount = mCursor.getCount();
120
121 updatePositions();
122
123 final Bundle extras = mCursor.getExtras();
124 if (extras != null) {
125 info = extras.getString(DocumentsContract.EXTRA_INFO);
126 error = extras.getString(DocumentsContract.EXTRA_ERROR);
127 mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
128 }
129
Ben Kwad72a1da2015-12-01 19:56:57 -0800130 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800131 }
132
Ben Kwad72a1da2015-12-01 19:56:57 -0800133 @VisibleForTesting
Ben Kwa0497da82015-11-30 23:00:02 -0800134 int getItemCount() {
Ben Kwada858bf2015-12-09 14:33:49 -0800135 return mCursorCount;
Ben Kwa0497da82015-11-30 23:00:02 -0800136 }
137
138 /**
139 * Update the ModelId-position map.
140 */
141 private void updatePositions() {
142 mPositions.clear();
143 mCursor.moveToPosition(-1);
144 for (int pos = 0; pos < mCursorCount; ++pos) {
145 mCursor.moveToNext();
Ben Kwad72a1da2015-12-01 19:56:57 -0800146 mPositions.put(Model.createId(mCursor), pos);
Ben Kwa0497da82015-11-30 23:00:02 -0800147 }
148 }
149
150 @Nullable Cursor getItem(String modelId) {
151 Integer pos = mPositions.get(modelId);
152 if (pos != null) {
153 mCursor.moveToPosition(pos);
154 return mCursor;
155 }
156 return null;
157 }
158
Ben Kwa0497da82015-11-30 23:00:02 -0800159 boolean isEmpty() {
160 return mCursorCount == 0;
161 }
162
163 boolean isLoading() {
164 return mIsLoading;
165 }
166
167 List<DocumentInfo> getDocuments(Selection items) {
168 final int size = (items != null) ? items.size() : 0;
169
170 final List<DocumentInfo> docs = new ArrayList<>(size);
Ben Kwad72a1da2015-12-01 19:56:57 -0800171 for (String modelId: items.getAll()) {
172 final Cursor cursor = getItem(modelId);
Ben Kwa0497da82015-11-30 23:00:02 -0800173 checkNotNull(cursor, "Cursor cannot be null.");
174 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
175 docs.add(doc);
176 }
177 return docs;
178 }
179
180 @Override
181 public Cursor getCursor() {
182 if (Looper.myLooper() != Looper.getMainLooper()) {
183 throw new IllegalStateException("Can't call getCursor from non-main thread.");
184 }
185 return mCursor;
186 }
187
Ben Kwada858bf2015-12-09 14:33:49 -0800188 public void delete(Selection selected, DeletionListener listener) {
189 final ContentResolver resolver = mContext.getContentResolver();
190 new DeleteFilesTask(resolver, listener).execute(selected);
Ben Kwa0497da82015-11-30 23:00:02 -0800191 }
192
193 /**
194 * A Task which collects the DocumentInfo for documents that have been marked for deletion,
195 * and actually deletes them.
196 */
Ben Kwada858bf2015-12-09 14:33:49 -0800197 private class DeleteFilesTask extends AsyncTask<Selection, Void, Void> {
Ben Kwa0497da82015-11-30 23:00:02 -0800198 private ContentResolver mResolver;
199 private DeletionListener mListener;
Ben Kwada858bf2015-12-09 14:33:49 -0800200 private boolean mHadTrouble;
Ben Kwa0497da82015-11-30 23:00:02 -0800201
202 /**
203 * @param resolver A ContentResolver for performing the actual file deletions.
204 * @param errorCallback A Runnable that is executed in the event that one or more errors
Ben Kwada858bf2015-12-09 14:33:49 -0800205 * occurred while copying files. Execution will occur on the UI thread.
Ben Kwa0497da82015-11-30 23:00:02 -0800206 */
207 public DeleteFilesTask(ContentResolver resolver, DeletionListener listener) {
208 mResolver = resolver;
209 mListener = listener;
210 }
211
212 @Override
Ben Kwada858bf2015-12-09 14:33:49 -0800213 protected Void doInBackground(Selection... selected) {
214 List<DocumentInfo> toDelete = getDocuments(selected[0]);
215 mHadTrouble = false;
Ben Kwa0497da82015-11-30 23:00:02 -0800216
Ben Kwada858bf2015-12-09 14:33:49 -0800217 for (DocumentInfo doc : toDelete) {
Ben Kwa0497da82015-11-30 23:00:02 -0800218 if (!doc.isDeleteSupported()) {
219 Log.w(TAG, doc + " could not be deleted. Skipping...");
Ben Kwada858bf2015-12-09 14:33:49 -0800220 mHadTrouble = true;
Ben Kwa0497da82015-11-30 23:00:02 -0800221 continue;
222 }
223
224 ContentProviderClient client = null;
225 try {
226 if (DEBUG) Log.d(TAG, "Deleting: " + doc.displayName);
227 client = DocumentsApplication.acquireUnstableProviderOrThrow(
228 mResolver, doc.derivedUri.getAuthority());
229 DocumentsContract.deleteDocument(client, doc.derivedUri);
230 } catch (Exception e) {
Ben Kwada858bf2015-12-09 14:33:49 -0800231 Log.w(TAG, "Failed to delete " + doc, e);
232 mHadTrouble = true;
Ben Kwa0497da82015-11-30 23:00:02 -0800233 } finally {
234 ContentProviderClient.releaseQuietly(client);
235 }
236 }
237
Ben Kwada858bf2015-12-09 14:33:49 -0800238 return null;
239 }
240
241 @Override
242 protected void onPostExecute(Void _) {
243 if (mHadTrouble) {
Ben Kwa0497da82015-11-30 23:00:02 -0800244 // TODO show which files failed? b/23720103
245 mListener.onError();
246 if (DEBUG) Log.d(TAG, "Deletion task completed. Some deletions failed.");
247 } else {
248 if (DEBUG) Log.d(TAG, "Deletion task completed successfully.");
249 }
Ben Kwa0497da82015-11-30 23:00:02 -0800250
251 mListener.onCompletion();
252 }
253 }
254
255 static class DeletionListener {
256 /**
257 * Called when deletion has completed (regardless of whether an error occurred).
258 */
259 void onCompletion() {}
260
261 /**
262 * Called at the end of a deletion operation that produced one or more errors.
263 */
264 void onError() {}
265 }
266
267 void addUpdateListener(UpdateListener listener) {
Ben Kwad72a1da2015-12-01 19:56:57 -0800268 mUpdateListeners.add(listener);
Ben Kwa0497da82015-11-30 23:00:02 -0800269 }
270
Ben Kwad72a1da2015-12-01 19:56:57 -0800271 static interface UpdateListener {
Ben Kwa0497da82015-11-30 23:00:02 -0800272 /**
273 * Called when a successful update has occurred.
274 */
Ben Kwad72a1da2015-12-01 19:56:57 -0800275 void onModelUpdate(Model model);
Ben Kwa0497da82015-11-30 23:00:02 -0800276
277 /**
278 * Called when an update has been attempted but failed.
279 */
Ben Kwad72a1da2015-12-01 19:56:57 -0800280 void onModelUpdateFailed(Exception e);
Ben Kwa0497da82015-11-30 23:00:02 -0800281 }
282}