blob: 9ba8b5c090d684a0a64e963ca44f3a0be0d49d5a [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;
Tomasz Mikolajewskifd2b4dc2016-12-15 16:39:45 +090022import static com.android.documentsui.base.Shared.ENABLE_OMC_API_FEATURES;
Ben Linf8f06e92017-01-27 17:15:48 -080023import static com.android.documentsui.base.Shared.VERBOSE;
Ben Kwa0497da82015-11-30 23:00:02 -080024
Steve McKay990f76e2016-09-16 12:36:58 -070025import android.annotation.IntDef;
Ben Linf8f06e92017-01-27 17:15:48 -080026import android.app.RecoverableSecurityException;
Ben Kwa0497da82015-11-30 23:00:02 -080027import android.database.Cursor;
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +090028import android.database.MergeCursor;
Steve McKay84769b82016-06-09 10:46:07 -070029import android.net.Uri;
Ben Kwa0497da82015-11-30 23:00:02 -080030import android.os.Bundle;
Ben Kwa0497da82015-11-30 23:00:02 -080031import android.provider.DocumentsContract;
32import android.provider.DocumentsContract.Document;
33import android.support.annotation.Nullable;
34import android.support.annotation.VisibleForTesting;
Ben Kwa0497da82015-11-30 23:00:02 -080035import android.util.Log;
Ben Kwa0497da82015-11-30 23:00:02 -080036
Ben Kwa0497da82015-11-30 23:00:02 -080037import com.android.documentsui.DirectoryResult;
Tomasz Mikolajewskie535c572016-11-25 13:58:35 +090038import com.android.documentsui.archives.ArchivesProvider;
Steve McKayd0805062016-09-15 14:30:38 -070039import com.android.documentsui.base.DocumentInfo;
Steve McKay990f76e2016-09-16 12:36:58 -070040import com.android.documentsui.base.EventListener;
Steve McKayd9caa6a2016-09-15 16:36:45 -070041import com.android.documentsui.roots.RootCursorWrapper;
Steve McKay4f78ba62016-10-04 16:48:49 -070042import com.android.documentsui.selection.Selection;
Ben Kwa0497da82015-11-30 23:00:02 -080043
Steve McKay990f76e2016-09-16 12:36:58 -070044import java.lang.annotation.Retention;
45import java.lang.annotation.RetentionPolicy;
Ben Kwa0497da82015-11-30 23:00:02 -080046import java.util.ArrayList;
Ben Kwa0497da82015-11-30 23:00:02 -080047import java.util.HashMap;
Jon Mann38f14392017-01-25 13:51:55 -080048import java.util.HashSet;
Ben Kwa0497da82015-11-30 23:00:02 -080049import java.util.List;
Ben Kwab8a5e082015-12-07 13:25:27 -080050import java.util.Map;
Jon Mann38f14392017-01-25 13:51:55 -080051import java.util.Set;
Steve McKayd0718952016-10-10 13:43:36 -070052import java.util.function.Predicate;
Ben Kwa0497da82015-11-30 23:00:02 -080053
54/**
55 * The data model for the current loaded directory.
56 */
57@VisibleForTesting
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +090058public class Model {
Steve McKayd0718952016-10-10 13:43:36 -070059
60 /**
Tomasz Mikolajewskifd2b4dc2016-12-15 16:39:45 +090061 * Filter that passes (returns true) for all files which can be shared.
Steve McKayd0718952016-10-10 13:43:36 -070062 */
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +090063 public static final Predicate<Cursor> SHARABLE_FILE_FILTER = (Cursor c) -> {
Tomasz Mikolajewskie535c572016-11-25 13:58:35 +090064 int flags = getCursorInt(c, Document.COLUMN_FLAGS);
65 String authority = getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY);
Tomasz Mikolajewskifd2b4dc2016-12-15 16:39:45 +090066 if (!ENABLE_OMC_API_FEATURES) {
67 return (flags & Document.FLAG_PARTIAL) == 0
68 && (flags & Document.FLAG_VIRTUAL_DOCUMENT) == 0
69 && !ArchivesProvider.AUTHORITY.equals(authority);
70 }
Tomasz Mikolajewski5b127ac2016-10-24 18:24:58 +090071 return (flags & Document.FLAG_PARTIAL) == 0
Tomasz Mikolajewskie535c572016-11-25 13:58:35 +090072 && !ArchivesProvider.AUTHORITY.equals(authority);
Steve McKayd0718952016-10-10 13:43:36 -070073 };
74
Tomasz Mikolajewski5b127ac2016-10-24 18:24:58 +090075 /**
76 * Filter that passes (returns true) only virtual documents.
77 */
78 public static final Predicate<Cursor> VIRTUAL_DOCUMENT_FILTER = (Cursor c) -> {
79 int flags = getCursorInt(c, Document.COLUMN_FLAGS);
80 return (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
81 };
82
Steve McKayd79cc4a2016-10-11 16:34:40 -070083 private static final Predicate<Cursor> ANY_FILE_FILTER = (Cursor c) -> true;
84
Ben Kwa0497da82015-11-30 23:00:02 -080085 private static final String TAG = "Model";
Ben Kwab8a5e082015-12-07 13:25:27 -080086
Jon Mann38f14392017-01-25 13:51:55 -080087 /** Maps Model ID to cursor positions, for looking up items by Model ID. */
88 private final Map<String, Integer> mPositions = new HashMap<>();
89 private final Set<String> mFileNames = new HashSet<>();
90
Ben Kwa0497da82015-11-30 23:00:02 -080091 private boolean mIsLoading;
Steve McKay990f76e2016-09-16 12:36:58 -070092 private List<EventListener<Update>> mUpdateListeners = new ArrayList<>();
Ben Kwa0497da82015-11-30 23:00:02 -080093 @Nullable private Cursor mCursor;
Ben Kwab8a5e082015-12-07 13:25:27 -080094 private int mCursorCount;
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +090095 private String mIds[] = new String[0];
Ben Kwab8a5e082015-12-07 13:25:27 -080096
Ben Kwa0497da82015-11-30 23:00:02 -080097 @Nullable String info;
98 @Nullable String error;
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +090099 @Nullable DocumentInfo doc;
Ben Kwa0497da82015-11-30 23:00:02 -0800100
Steve McKay990f76e2016-09-16 12:36:58 -0700101 public void addUpdateListener(EventListener<Update> listener) {
102 mUpdateListeners.add(listener);
103 }
104
105 public void removeUpdateListener(EventListener<Update> listener) {
106 mUpdateListeners.remove(listener);
107 }
108
Ben Kwad72a1da2015-12-01 19:56:57 -0800109 private void notifyUpdateListeners() {
Steve McKay990f76e2016-09-16 12:36:58 -0700110 for (EventListener<Update> handler: mUpdateListeners) {
111 handler.accept(Update.UPDATE);
Ben Kwad72a1da2015-12-01 19:56:57 -0800112 }
113 }
114
115 private void notifyUpdateListeners(Exception e) {
Steve McKay990f76e2016-09-16 12:36:58 -0700116 Update error = new Update(e);
117 for (EventListener<Update> handler: mUpdateListeners) {
118 handler.accept(error);
Ben Kwad72a1da2015-12-01 19:56:57 -0800119 }
120 }
121
Steve McKay9de0da62016-08-25 15:18:23 -0700122 void onLoaderReset() {
123 if (mIsLoading) {
Steve McKay7c662092016-08-26 12:17:41 -0700124 Log.w(TAG, "Received unexpected loader reset while in loading state for doc: "
125 + DocumentInfo.debugString(doc));
Ben Kwa0497da82015-11-30 23:00:02 -0800126 }
Steve McKay7c662092016-08-26 12:17:41 -0700127
128 reset();
Steve McKay9de0da62016-08-25 15:18:23 -0700129 }
130
131 private void reset() {
132 mCursor = null;
133 mCursorCount = 0;
134 mIds = new String[0];
135 mPositions.clear();
136 info = null;
137 error = null;
138 doc = null;
139 mIsLoading = false;
Jon Mann38f14392017-01-25 13:51:55 -0800140 mFileNames.clear();
Steve McKay9de0da62016-08-25 15:18:23 -0700141 notifyUpdateListeners();
142 }
143
144 void update(DirectoryResult result) {
145 assert(result != null);
146
147 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
Ben Kwa0497da82015-11-30 23:00:02 -0800148
149 if (result.exception != null) {
150 Log.e(TAG, "Error while loading directory contents", result.exception);
Ben Kwad72a1da2015-12-01 19:56:57 -0800151 notifyUpdateListeners(result.exception);
Ben Kwa0497da82015-11-30 23:00:02 -0800152 return;
153 }
154
155 mCursor = result.cursor;
156 mCursorCount = mCursor.getCount();
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900157 doc = result.doc;
Ben Kwa0497da82015-11-30 23:00:02 -0800158
Ben Kwab8a5e082015-12-07 13:25:27 -0800159 updateModelData();
Ben Kwa0497da82015-11-30 23:00:02 -0800160
161 final Bundle extras = mCursor.getExtras();
162 if (extras != null) {
163 info = extras.getString(DocumentsContract.EXTRA_INFO);
164 error = extras.getString(DocumentsContract.EXTRA_ERROR);
165 mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
166 }
167
Ben Kwad72a1da2015-12-01 19:56:57 -0800168 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800169 }
170
Ben Kwad72a1da2015-12-01 19:56:57 -0800171 @VisibleForTesting
Ben Kwa0497da82015-11-30 23:00:02 -0800172 int getItemCount() {
Ben Kwada858bf2015-12-09 14:33:49 -0800173 return mCursorCount;
Ben Kwa0497da82015-11-30 23:00:02 -0800174 }
175
176 /**
Ben Kwab8a5e082015-12-07 13:25:27 -0800177 * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
178 * according to the current sort order.
Ben Kwa0497da82015-11-30 23:00:02 -0800179 */
Ben Kwab8a5e082015-12-07 13:25:27 -0800180 private void updateModelData() {
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900181 mIds = new String[mCursorCount];
Jon Mann38f14392017-01-25 13:51:55 -0800182 mFileNames.clear();
Ben Kwa0497da82015-11-30 23:00:02 -0800183 mCursor.moveToPosition(-1);
184 for (int pos = 0; pos < mCursorCount; ++pos) {
Ben Lin2df30e52016-04-22 16:12:50 -0700185 if (!mCursor.moveToNext()) {
186 Log.e(TAG, "Fail to move cursor to next pos: " + pos);
187 return;
188 }
Tomasz Mikolajewski985df3d2016-03-15 15:38:54 +0900189 // Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
190 // unique string that can be used to identify the document referred to by the cursor.
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +0900191 // If the cursor is a merged cursor over multiple authorities, then prefix the ids
192 // with the authority to avoid collisions.
193 if (mCursor instanceof MergeCursor) {
Garfield Tan2010ff72016-09-30 14:55:32 -0700194 mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY)
195 + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +0900196 } else {
Tomasz Mikolajewski06b036f2016-04-26 11:11:17 +0900197 mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +0900198 }
Jon Mann38f14392017-01-25 13:51:55 -0800199 mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME));
Ben Kwab8a5e082015-12-07 13:25:27 -0800200 }
201
202 // Populate the positions.
203 mPositions.clear();
204 for (int i = 0; i < mCursorCount; ++i) {
Garfield Tan2010ff72016-09-30 14:55:32 -0700205 mPositions.put(mIds[i], i);
Ben Kwab8a5e082015-12-07 13:25:27 -0800206 }
207 }
208
Jon Mann38f14392017-01-25 13:51:55 -0800209 public boolean hasFileWithName(String name) {
210 return mFileNames.contains(name);
211 }
212
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +0900213 public @Nullable Cursor getItem(String modelId) {
Ben Kwa0497da82015-11-30 23:00:02 -0800214 Integer pos = mPositions.get(modelId);
Steve McKay5a22a112016-04-12 11:29:10 -0700215 if (pos == null) {
216 if (DEBUG) Log.d(TAG, "Unabled to find cursor position for modelId: " + modelId);
217 return null;
Ben Kwa0497da82015-11-30 23:00:02 -0800218 }
Steve McKay5a22a112016-04-12 11:29:10 -0700219
220 if (!mCursor.moveToPosition(pos)) {
221 if (DEBUG) Log.d(TAG,
222 "Unabled to move cursor to position " + pos + " for modelId: " + modelId);
223 return null;
224 }
225
226 return mCursor;
Ben Kwa0497da82015-11-30 23:00:02 -0800227 }
228
Steve McKay990f76e2016-09-16 12:36:58 -0700229 public boolean isEmpty() {
Ben Kwa0497da82015-11-30 23:00:02 -0800230 return mCursorCount == 0;
231 }
232
233 boolean isLoading() {
234 return mIsLoading;
235 }
236
Steve McKayc8889af2016-09-23 11:22:41 -0700237 public List<DocumentInfo> getDocuments(Selection selection) {
Steve McKayd79cc4a2016-10-11 16:34:40 -0700238 return loadDocuments(selection, ANY_FILE_FILTER);
Ben Kwa0497da82015-11-30 23:00:02 -0800239 }
240
Steve McKay990f76e2016-09-16 12:36:58 -0700241 public @Nullable DocumentInfo getDocument(String modelId) {
242 final Cursor cursor = getItem(modelId);
243 return (cursor == null)
244 ? null
245 : DocumentInfo.fromDirectoryCursor(cursor);
246 }
247
Steve McKayd0718952016-10-10 13:43:36 -0700248 public List<DocumentInfo> loadDocuments(Selection selection, Predicate<Cursor> filter) {
249 final int size = (selection != null) ? selection.size() : 0;
250
251 final List<DocumentInfo> docs = new ArrayList<>(size);
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900252 DocumentInfo doc;
Steve McKayd0718952016-10-10 13:43:36 -0700253 for (String modelId: selection) {
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900254 doc = loadDocument(modelId, filter);
255 if (doc != null) {
256 docs.add(doc);
257 }
Steve McKayd0718952016-10-10 13:43:36 -0700258 }
259 return docs;
260 }
261
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900262 public boolean hasDocuments(Selection selection, Predicate<Cursor> filter) {
263 for (String modelId: selection) {
264 if (loadDocument(modelId, filter) != null) {
265 return true;
266 }
267 }
268 return false;
269 }
270
Steve McKayd0718952016-10-10 13:43:36 -0700271 /**
Steve McKayd0718952016-10-10 13:43:36 -0700272 * @return DocumentInfo, or null. If filter returns false, null will be returned.
273 */
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900274 private @Nullable DocumentInfo loadDocument(String modelId, Predicate<Cursor> filter) {
Steve McKayd0718952016-10-10 13:43:36 -0700275 final Cursor cursor = getItem(modelId);
276
277 if (cursor == null) {
278 Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900279 return null;
Steve McKayd0718952016-10-10 13:43:36 -0700280 }
281
282 if (filter.test(cursor)) {
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900283 return DocumentInfo.fromDirectoryCursor(cursor);
Steve McKayd0718952016-10-10 13:43:36 -0700284 }
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900285
286 if (VERBOSE) Log.v(TAG, "Filtered out document from results: " + modelId);
287 return null;
Steve McKayd0718952016-10-10 13:43:36 -0700288 }
289
Steve McKay84769b82016-06-09 10:46:07 -0700290 public Uri getItemUri(String modelId) {
291 final Cursor cursor = getItem(modelId);
292 return DocumentInfo.getUri(cursor);
293 }
294
Garfield, Tan11d23482016-08-05 09:33:29 -0700295 /**
296 * @return An ordered array of model IDs representing the documents in the model. It is sorted
297 * according to the current sort order, which was set by the last model update.
298 */
299 public String[] getModelIds() {
300 return mIds;
301 }
302
Steve McKay990f76e2016-09-16 12:36:58 -0700303 public static class Update {
Ben Kwa0497da82015-11-30 23:00:02 -0800304
Steve McKay990f76e2016-09-16 12:36:58 -0700305 public static final Update UPDATE = new Update();
Ben Kwa472103f2016-02-10 15:48:25 -0800306
Steve McKay990f76e2016-09-16 12:36:58 -0700307 @IntDef(value = {
308 TYPE_UPDATE,
Ben Linf8f06e92017-01-27 17:15:48 -0800309 TYPE_UPDATE_EXCEPTION
Steve McKay990f76e2016-09-16 12:36:58 -0700310 })
311 @Retention(RetentionPolicy.SOURCE)
312 public @interface UpdateType {}
313 public static final int TYPE_UPDATE = 0;
Ben Linf8f06e92017-01-27 17:15:48 -0800314 public static final int TYPE_UPDATE_EXCEPTION = 1;
Ben Kwa0497da82015-11-30 23:00:02 -0800315
Ben Linf8f06e92017-01-27 17:15:48 -0800316 private final @UpdateType int mUpdateType;
Steve McKay990f76e2016-09-16 12:36:58 -0700317 private final @Nullable Exception mException;
318
319 private Update() {
Ben Linf8f06e92017-01-27 17:15:48 -0800320 mUpdateType = TYPE_UPDATE;
Steve McKay990f76e2016-09-16 12:36:58 -0700321 mException = null;
322 }
323
324 public Update(Exception exception) {
325 assert(exception != null);
Ben Linf8f06e92017-01-27 17:15:48 -0800326 mUpdateType = TYPE_UPDATE_EXCEPTION;
Steve McKay990f76e2016-09-16 12:36:58 -0700327 mException = exception;
328 }
329
330 public boolean isUpdate() {
Ben Linf8f06e92017-01-27 17:15:48 -0800331 return mUpdateType == TYPE_UPDATE;
Steve McKay990f76e2016-09-16 12:36:58 -0700332 }
333
Ben Linf8f06e92017-01-27 17:15:48 -0800334 public boolean hasException() {
335 return mUpdateType == TYPE_UPDATE_EXCEPTION;
Steve McKay990f76e2016-09-16 12:36:58 -0700336 }
337
Ben Linf8f06e92017-01-27 17:15:48 -0800338 public boolean hasRecoverableException() {
339 return hasException() && mException instanceof RecoverableSecurityException;
340 }
341
342 public @Nullable Exception getException() {
Steve McKay990f76e2016-09-16 12:36:58 -0700343 return mException;
344 }
Ben Kwa0497da82015-11-30 23:00:02 -0800345 }
Ben Kwa0497da82015-11-30 23:00:02 -0800346}