blob: 63dcb4b4860e99fcd8617513c56d82b21f720a94 [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
Tomasz Mikolajewskie535c572016-11-25 13:58:35 +090019import static com.android.documentsui.base.DocumentInfo.getCursorInt;
Steve McKayd0805062016-09-15 14:30:38 -070020import static com.android.documentsui.base.DocumentInfo.getCursorString;
Steve McKayd9caa6a2016-09-15 16:36:45 -070021import static com.android.documentsui.base.Shared.DEBUG;
Steve McKay30535bc2016-11-04 14:16:58 -070022import static com.android.documentsui.base.Shared.VERBOSE;
Ben Kwa0497da82015-11-30 23:00:02 -080023
Steve McKay990f76e2016-09-16 12:36:58 -070024import android.annotation.IntDef;
Ben Kwa0497da82015-11-30 23:00:02 -080025import android.database.Cursor;
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +090026import android.database.MergeCursor;
Steve McKay84769b82016-06-09 10:46:07 -070027import android.net.Uri;
Ben Kwa0497da82015-11-30 23:00:02 -080028import android.os.Bundle;
Ben Kwa0497da82015-11-30 23:00:02 -080029import android.provider.DocumentsContract;
30import android.provider.DocumentsContract.Document;
31import android.support.annotation.Nullable;
32import android.support.annotation.VisibleForTesting;
Ben Kwa0497da82015-11-30 23:00:02 -080033import android.util.Log;
Ben Kwa0497da82015-11-30 23:00:02 -080034
Ben Kwa0497da82015-11-30 23:00:02 -080035import com.android.documentsui.DirectoryResult;
Tomasz Mikolajewskie535c572016-11-25 13:58:35 +090036import com.android.documentsui.archives.ArchivesProvider;
Steve McKayd0805062016-09-15 14:30:38 -070037import com.android.documentsui.base.DocumentInfo;
Steve McKay990f76e2016-09-16 12:36:58 -070038import com.android.documentsui.base.EventListener;
Steve McKayd9caa6a2016-09-15 16:36:45 -070039import com.android.documentsui.roots.RootCursorWrapper;
Steve McKay4f78ba62016-10-04 16:48:49 -070040import com.android.documentsui.selection.Selection;
Ben Kwa0497da82015-11-30 23:00:02 -080041
Steve McKay990f76e2016-09-16 12:36:58 -070042import java.lang.annotation.Retention;
43import java.lang.annotation.RetentionPolicy;
Ben Kwa0497da82015-11-30 23:00:02 -080044import java.util.ArrayList;
Ben Kwa0497da82015-11-30 23:00:02 -080045import java.util.HashMap;
46import java.util.List;
Ben Kwab8a5e082015-12-07 13:25:27 -080047import java.util.Map;
Steve McKayd0718952016-10-10 13:43:36 -070048import java.util.function.Predicate;
Ben Kwa0497da82015-11-30 23:00:02 -080049
50/**
51 * The data model for the current loaded directory.
52 */
53@VisibleForTesting
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +090054public class Model {
Steve McKayd0718952016-10-10 13:43:36 -070055
56 /**
Tomasz Mikolajewski5b127ac2016-10-24 18:24:58 +090057 * Filter that passes (returns true) all non-partial files and non-archived files.
Steve McKayd0718952016-10-10 13:43:36 -070058 */
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +090059 public static final Predicate<Cursor> SHARABLE_FILE_FILTER = (Cursor c) -> {
Tomasz Mikolajewskie535c572016-11-25 13:58:35 +090060 int flags = getCursorInt(c, Document.COLUMN_FLAGS);
61 String authority = getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY);
Tomasz Mikolajewski5b127ac2016-10-24 18:24:58 +090062 return (flags & Document.FLAG_PARTIAL) == 0
Tomasz Mikolajewskie535c572016-11-25 13:58:35 +090063 && !ArchivesProvider.AUTHORITY.equals(authority);
Steve McKayd0718952016-10-10 13:43:36 -070064 };
65
Tomasz Mikolajewski5b127ac2016-10-24 18:24:58 +090066 /**
67 * Filter that passes (returns true) only virtual documents.
68 */
69 public static final Predicate<Cursor> VIRTUAL_DOCUMENT_FILTER = (Cursor c) -> {
70 int flags = getCursorInt(c, Document.COLUMN_FLAGS);
71 return (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
72 };
73
Steve McKayd79cc4a2016-10-11 16:34:40 -070074 private static final Predicate<Cursor> ANY_FILE_FILTER = (Cursor c) -> true;
75
Ben Kwa0497da82015-11-30 23:00:02 -080076 private static final String TAG = "Model";
Ben Kwab8a5e082015-12-07 13:25:27 -080077
Ben Kwa0497da82015-11-30 23:00:02 -080078 private boolean mIsLoading;
Steve McKay990f76e2016-09-16 12:36:58 -070079 private List<EventListener<Update>> mUpdateListeners = new ArrayList<>();
Ben Kwa0497da82015-11-30 23:00:02 -080080 @Nullable private Cursor mCursor;
Ben Kwab8a5e082015-12-07 13:25:27 -080081 private int mCursorCount;
82 /** Maps Model ID to cursor positions, for looking up items by Model ID. */
83 private Map<String, Integer> mPositions = new HashMap<>();
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +090084 private String mIds[] = new String[0];
Ben Kwab8a5e082015-12-07 13:25:27 -080085
Ben Kwa0497da82015-11-30 23:00:02 -080086 @Nullable String info;
87 @Nullable String error;
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +090088 @Nullable DocumentInfo doc;
Ben Kwa0497da82015-11-30 23:00:02 -080089
Steve McKay990f76e2016-09-16 12:36:58 -070090 public void addUpdateListener(EventListener<Update> listener) {
91 mUpdateListeners.add(listener);
92 }
93
94 public void removeUpdateListener(EventListener<Update> listener) {
95 mUpdateListeners.remove(listener);
96 }
97
Ben Kwad72a1da2015-12-01 19:56:57 -080098 private void notifyUpdateListeners() {
Steve McKay990f76e2016-09-16 12:36:58 -070099 for (EventListener<Update> handler: mUpdateListeners) {
100 handler.accept(Update.UPDATE);
Ben Kwad72a1da2015-12-01 19:56:57 -0800101 }
102 }
103
104 private void notifyUpdateListeners(Exception e) {
Steve McKay990f76e2016-09-16 12:36:58 -0700105 Update error = new Update(e);
106 for (EventListener<Update> handler: mUpdateListeners) {
107 handler.accept(error);
Ben Kwad72a1da2015-12-01 19:56:57 -0800108 }
109 }
110
Steve McKay9de0da62016-08-25 15:18:23 -0700111 void onLoaderReset() {
112 if (mIsLoading) {
Steve McKay7c662092016-08-26 12:17:41 -0700113 Log.w(TAG, "Received unexpected loader reset while in loading state for doc: "
114 + DocumentInfo.debugString(doc));
Ben Kwa0497da82015-11-30 23:00:02 -0800115 }
Steve McKay7c662092016-08-26 12:17:41 -0700116
117 reset();
Steve McKay9de0da62016-08-25 15:18:23 -0700118 }
119
120 private void reset() {
121 mCursor = null;
122 mCursorCount = 0;
123 mIds = new String[0];
124 mPositions.clear();
125 info = null;
126 error = null;
127 doc = null;
128 mIsLoading = false;
129 notifyUpdateListeners();
130 }
131
132 void update(DirectoryResult result) {
133 assert(result != null);
134
135 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
Ben Kwa0497da82015-11-30 23:00:02 -0800136
137 if (result.exception != null) {
138 Log.e(TAG, "Error while loading directory contents", result.exception);
Ben Kwad72a1da2015-12-01 19:56:57 -0800139 notifyUpdateListeners(result.exception);
Ben Kwa0497da82015-11-30 23:00:02 -0800140 return;
141 }
142
143 mCursor = result.cursor;
144 mCursorCount = mCursor.getCount();
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900145 doc = result.doc;
Ben Kwa0497da82015-11-30 23:00:02 -0800146
Ben Kwab8a5e082015-12-07 13:25:27 -0800147 updateModelData();
Ben Kwa0497da82015-11-30 23:00:02 -0800148
149 final Bundle extras = mCursor.getExtras();
150 if (extras != null) {
151 info = extras.getString(DocumentsContract.EXTRA_INFO);
152 error = extras.getString(DocumentsContract.EXTRA_ERROR);
153 mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
154 }
155
Ben Kwad72a1da2015-12-01 19:56:57 -0800156 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800157 }
158
Ben Kwad72a1da2015-12-01 19:56:57 -0800159 @VisibleForTesting
Ben Kwa0497da82015-11-30 23:00:02 -0800160 int getItemCount() {
Ben Kwada858bf2015-12-09 14:33:49 -0800161 return mCursorCount;
Ben Kwa0497da82015-11-30 23:00:02 -0800162 }
163
164 /**
Ben Kwab8a5e082015-12-07 13:25:27 -0800165 * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
166 * according to the current sort order.
Ben Kwa0497da82015-11-30 23:00:02 -0800167 */
Ben Kwab8a5e082015-12-07 13:25:27 -0800168 private void updateModelData() {
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900169 mIds = new String[mCursorCount];
Tomasz Mikolajewski61e315d2016-03-15 13:23:52 +0900170
Ben Kwa0497da82015-11-30 23:00:02 -0800171 mCursor.moveToPosition(-1);
172 for (int pos = 0; pos < mCursorCount; ++pos) {
Ben Lin2df30e52016-04-22 16:12:50 -0700173 if (!mCursor.moveToNext()) {
174 Log.e(TAG, "Fail to move cursor to next pos: " + pos);
175 return;
176 }
Tomasz Mikolajewski985df3d2016-03-15 15:38:54 +0900177 // Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
178 // unique string that can be used to identify the document referred to by the cursor.
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +0900179 // If the cursor is a merged cursor over multiple authorities, then prefix the ids
180 // with the authority to avoid collisions.
181 if (mCursor instanceof MergeCursor) {
Garfield Tan2010ff72016-09-30 14:55:32 -0700182 mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY)
183 + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +0900184 } else {
Tomasz Mikolajewski06b036f2016-04-26 11:11:17 +0900185 mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +0900186 }
Ben Kwab8a5e082015-12-07 13:25:27 -0800187 }
188
189 // Populate the positions.
190 mPositions.clear();
191 for (int i = 0; i < mCursorCount; ++i) {
Garfield Tan2010ff72016-09-30 14:55:32 -0700192 mPositions.put(mIds[i], i);
Ben Kwab8a5e082015-12-07 13:25:27 -0800193 }
194 }
195
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +0900196 public @Nullable Cursor getItem(String modelId) {
Ben Kwa0497da82015-11-30 23:00:02 -0800197 Integer pos = mPositions.get(modelId);
Steve McKay5a22a112016-04-12 11:29:10 -0700198 if (pos == null) {
199 if (DEBUG) Log.d(TAG, "Unabled to find cursor position for modelId: " + modelId);
200 return null;
Ben Kwa0497da82015-11-30 23:00:02 -0800201 }
Steve McKay5a22a112016-04-12 11:29:10 -0700202
203 if (!mCursor.moveToPosition(pos)) {
204 if (DEBUG) Log.d(TAG,
205 "Unabled to move cursor to position " + pos + " for modelId: " + modelId);
206 return null;
207 }
208
209 return mCursor;
Ben Kwa0497da82015-11-30 23:00:02 -0800210 }
211
Steve McKay990f76e2016-09-16 12:36:58 -0700212 public boolean isEmpty() {
Ben Kwa0497da82015-11-30 23:00:02 -0800213 return mCursorCount == 0;
214 }
215
216 boolean isLoading() {
217 return mIsLoading;
218 }
219
Steve McKayc8889af2016-09-23 11:22:41 -0700220 public List<DocumentInfo> getDocuments(Selection selection) {
Steve McKayd79cc4a2016-10-11 16:34:40 -0700221 return loadDocuments(selection, ANY_FILE_FILTER);
Ben Kwa0497da82015-11-30 23:00:02 -0800222 }
223
Steve McKay990f76e2016-09-16 12:36:58 -0700224 public @Nullable DocumentInfo getDocument(String modelId) {
225 final Cursor cursor = getItem(modelId);
226 return (cursor == null)
227 ? null
228 : DocumentInfo.fromDirectoryCursor(cursor);
229 }
230
Steve McKayd0718952016-10-10 13:43:36 -0700231 public List<DocumentInfo> loadDocuments(Selection selection, Predicate<Cursor> filter) {
232 final int size = (selection != null) ? selection.size() : 0;
233
234 final List<DocumentInfo> docs = new ArrayList<>(size);
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900235 DocumentInfo doc;
Steve McKayd0718952016-10-10 13:43:36 -0700236 for (String modelId: selection) {
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900237 doc = loadDocument(modelId, filter);
238 if (doc != null) {
239 docs.add(doc);
240 }
Steve McKayd0718952016-10-10 13:43:36 -0700241 }
242 return docs;
243 }
244
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900245 public boolean hasDocuments(Selection selection, Predicate<Cursor> filter) {
246 for (String modelId: selection) {
247 if (loadDocument(modelId, filter) != null) {
248 return true;
249 }
250 }
251 return false;
252 }
253
Steve McKayd0718952016-10-10 13:43:36 -0700254 /**
Steve McKayd0718952016-10-10 13:43:36 -0700255 * @return DocumentInfo, or null. If filter returns false, null will be returned.
256 */
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900257 private @Nullable DocumentInfo loadDocument(String modelId, Predicate<Cursor> filter) {
Steve McKayd0718952016-10-10 13:43:36 -0700258 final Cursor cursor = getItem(modelId);
259
260 if (cursor == null) {
261 Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900262 return null;
Steve McKayd0718952016-10-10 13:43:36 -0700263 }
264
265 if (filter.test(cursor)) {
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900266 return DocumentInfo.fromDirectoryCursor(cursor);
Steve McKayd0718952016-10-10 13:43:36 -0700267 }
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900268
269 if (VERBOSE) Log.v(TAG, "Filtered out document from results: " + modelId);
270 return null;
Steve McKayd0718952016-10-10 13:43:36 -0700271 }
272
Steve McKay84769b82016-06-09 10:46:07 -0700273 public Uri getItemUri(String modelId) {
274 final Cursor cursor = getItem(modelId);
275 return DocumentInfo.getUri(cursor);
276 }
277
Garfield, Tan11d23482016-08-05 09:33:29 -0700278 /**
279 * @return An ordered array of model IDs representing the documents in the model. It is sorted
280 * according to the current sort order, which was set by the last model update.
281 */
282 public String[] getModelIds() {
283 return mIds;
284 }
285
Steve McKay990f76e2016-09-16 12:36:58 -0700286 public static class Update {
Ben Kwa0497da82015-11-30 23:00:02 -0800287
Steve McKay990f76e2016-09-16 12:36:58 -0700288 public static final Update UPDATE = new Update();
Ben Kwa472103f2016-02-10 15:48:25 -0800289
Steve McKay990f76e2016-09-16 12:36:58 -0700290 @IntDef(value = {
291 TYPE_UPDATE,
292 TYPE_UPDATE_ERROR
293 })
294 @Retention(RetentionPolicy.SOURCE)
295 public @interface UpdateType {}
296 public static final int TYPE_UPDATE = 0;
297 public static final int TYPE_UPDATE_ERROR = 1;
Ben Kwa0497da82015-11-30 23:00:02 -0800298
Steve McKay990f76e2016-09-16 12:36:58 -0700299 private final @UpdateType int mType;
300 private final @Nullable Exception mException;
301
302 private Update() {
303 mType = TYPE_UPDATE;
304 mException = null;
305 }
306
307 public Update(Exception exception) {
308 assert(exception != null);
309 mType = TYPE_UPDATE_ERROR;
310 mException = exception;
311 }
312
313 public boolean isUpdate() {
314 return mType == TYPE_UPDATE;
315 }
316
317 public boolean hasError() {
318 return mType == TYPE_UPDATE_ERROR;
319 }
320
321 public @Nullable Exception getError() {
322 return mException;
323 }
Ben Kwa0497da82015-11-30 23:00:02 -0800324 }
Ben Kwa0497da82015-11-30 23:00:02 -0800325}