blob: ea51e91ad7abc38c475ee2aa601b0d5c16a534fe [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
Steve McKayd0805062016-09-15 14:30:38 -070019import static com.android.documentsui.base.DocumentInfo.getCursorString;
Steve McKayd9caa6a2016-09-15 16:36:45 -070020import static com.android.documentsui.base.Shared.DEBUG;
Ben Kwa0497da82015-11-30 23:00:02 -080021
Steve McKay990f76e2016-09-16 12:36:58 -070022import android.annotation.IntDef;
Ben Kwa0497da82015-11-30 23:00:02 -080023import android.database.Cursor;
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +090024import android.database.MergeCursor;
Steve McKay84769b82016-06-09 10:46:07 -070025import android.net.Uri;
Ben Kwa0497da82015-11-30 23:00:02 -080026import android.os.Bundle;
Ben Kwa0497da82015-11-30 23:00:02 -080027import android.provider.DocumentsContract;
28import android.provider.DocumentsContract.Document;
29import android.support.annotation.Nullable;
30import android.support.annotation.VisibleForTesting;
Ben Kwa0497da82015-11-30 23:00:02 -080031import android.util.Log;
Ben Kwa0497da82015-11-30 23:00:02 -080032
Ben Kwa0497da82015-11-30 23:00:02 -080033import com.android.documentsui.DirectoryResult;
Steve McKayd0805062016-09-15 14:30:38 -070034import com.android.documentsui.base.DocumentInfo;
Steve McKay990f76e2016-09-16 12:36:58 -070035import com.android.documentsui.base.EventListener;
Steve McKayd9caa6a2016-09-15 16:36:45 -070036import com.android.documentsui.roots.RootCursorWrapper;
Steve McKay4f78ba62016-10-04 16:48:49 -070037import com.android.documentsui.selection.Selection;
Ben Kwa0497da82015-11-30 23:00:02 -080038
Steve McKay990f76e2016-09-16 12:36:58 -070039import java.lang.annotation.Retention;
40import java.lang.annotation.RetentionPolicy;
Ben Kwa0497da82015-11-30 23:00:02 -080041import java.util.ArrayList;
Ben Kwa0497da82015-11-30 23:00:02 -080042import java.util.HashMap;
43import java.util.List;
Ben Kwab8a5e082015-12-07 13:25:27 -080044import java.util.Map;
Steve McKayd0718952016-10-10 13:43:36 -070045import java.util.function.Predicate;
Ben Kwa0497da82015-11-30 23:00:02 -080046
47/**
48 * The data model for the current loaded directory.
49 */
50@VisibleForTesting
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +090051public class Model {
Steve McKayd0718952016-10-10 13:43:36 -070052
53 /**
54 * Filter that passes (returns true) all non-virtual, non-partial files.
55 */
56 public static final Predicate<Cursor> CONCRETE_FILE_FILTER = (Cursor c) -> {
57 int flags = DocumentInfo.getCursorInt(c, Document.COLUMN_FLAGS);
58 return (flags & Document.FLAG_VIRTUAL_DOCUMENT) == 0
59 && (flags & Document.FLAG_PARTIAL) == 0;
60 };
61
Ben Kwa0497da82015-11-30 23:00:02 -080062 private static final String TAG = "Model";
Ben Kwab8a5e082015-12-07 13:25:27 -080063
Ben Kwa0497da82015-11-30 23:00:02 -080064 private boolean mIsLoading;
Steve McKay990f76e2016-09-16 12:36:58 -070065 private List<EventListener<Update>> mUpdateListeners = new ArrayList<>();
Ben Kwa0497da82015-11-30 23:00:02 -080066 @Nullable private Cursor mCursor;
Ben Kwab8a5e082015-12-07 13:25:27 -080067 private int mCursorCount;
68 /** Maps Model ID to cursor positions, for looking up items by Model ID. */
69 private Map<String, Integer> mPositions = new HashMap<>();
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +090070 private String mIds[] = new String[0];
Ben Kwab8a5e082015-12-07 13:25:27 -080071
Ben Kwa0497da82015-11-30 23:00:02 -080072 @Nullable String info;
73 @Nullable String error;
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +090074 @Nullable DocumentInfo doc;
Ben Kwa0497da82015-11-30 23:00:02 -080075
Steve McKay990f76e2016-09-16 12:36:58 -070076 public void addUpdateListener(EventListener<Update> listener) {
77 mUpdateListeners.add(listener);
78 }
79
80 public void removeUpdateListener(EventListener<Update> listener) {
81 mUpdateListeners.remove(listener);
82 }
83
Ben Kwad72a1da2015-12-01 19:56:57 -080084 private void notifyUpdateListeners() {
Steve McKay990f76e2016-09-16 12:36:58 -070085 for (EventListener<Update> handler: mUpdateListeners) {
86 handler.accept(Update.UPDATE);
Ben Kwad72a1da2015-12-01 19:56:57 -080087 }
88 }
89
90 private void notifyUpdateListeners(Exception e) {
Steve McKay990f76e2016-09-16 12:36:58 -070091 Update error = new Update(e);
92 for (EventListener<Update> handler: mUpdateListeners) {
93 handler.accept(error);
Ben Kwad72a1da2015-12-01 19:56:57 -080094 }
95 }
96
Steve McKay9de0da62016-08-25 15:18:23 -070097 void onLoaderReset() {
98 if (mIsLoading) {
Steve McKay7c662092016-08-26 12:17:41 -070099 Log.w(TAG, "Received unexpected loader reset while in loading state for doc: "
100 + DocumentInfo.debugString(doc));
Ben Kwa0497da82015-11-30 23:00:02 -0800101 }
Steve McKay7c662092016-08-26 12:17:41 -0700102
103 reset();
Steve McKay9de0da62016-08-25 15:18:23 -0700104 }
105
106 private void reset() {
107 mCursor = null;
108 mCursorCount = 0;
109 mIds = new String[0];
110 mPositions.clear();
111 info = null;
112 error = null;
113 doc = null;
114 mIsLoading = false;
115 notifyUpdateListeners();
116 }
117
118 void update(DirectoryResult result) {
119 assert(result != null);
120
121 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
Ben Kwa0497da82015-11-30 23:00:02 -0800122
123 if (result.exception != null) {
124 Log.e(TAG, "Error while loading directory contents", result.exception);
Ben Kwad72a1da2015-12-01 19:56:57 -0800125 notifyUpdateListeners(result.exception);
Ben Kwa0497da82015-11-30 23:00:02 -0800126 return;
127 }
128
129 mCursor = result.cursor;
130 mCursorCount = mCursor.getCount();
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900131 doc = result.doc;
Ben Kwa0497da82015-11-30 23:00:02 -0800132
Ben Kwab8a5e082015-12-07 13:25:27 -0800133 updateModelData();
Ben Kwa0497da82015-11-30 23:00:02 -0800134
135 final Bundle extras = mCursor.getExtras();
136 if (extras != null) {
137 info = extras.getString(DocumentsContract.EXTRA_INFO);
138 error = extras.getString(DocumentsContract.EXTRA_ERROR);
139 mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
140 }
141
Ben Kwad72a1da2015-12-01 19:56:57 -0800142 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800143 }
144
Ben Kwad72a1da2015-12-01 19:56:57 -0800145 @VisibleForTesting
Ben Kwa0497da82015-11-30 23:00:02 -0800146 int getItemCount() {
Ben Kwada858bf2015-12-09 14:33:49 -0800147 return mCursorCount;
Ben Kwa0497da82015-11-30 23:00:02 -0800148 }
149
150 /**
Ben Kwab8a5e082015-12-07 13:25:27 -0800151 * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
152 * according to the current sort order.
Ben Kwa0497da82015-11-30 23:00:02 -0800153 */
Ben Kwab8a5e082015-12-07 13:25:27 -0800154 private void updateModelData() {
Tomasz Mikolajewskif27c2742016-03-07 18:01:45 +0900155 mIds = new String[mCursorCount];
Tomasz Mikolajewski61e315d2016-03-15 13:23:52 +0900156
Ben Kwa0497da82015-11-30 23:00:02 -0800157 mCursor.moveToPosition(-1);
158 for (int pos = 0; pos < mCursorCount; ++pos) {
Ben Lin2df30e52016-04-22 16:12:50 -0700159 if (!mCursor.moveToNext()) {
160 Log.e(TAG, "Fail to move cursor to next pos: " + pos);
161 return;
162 }
Tomasz Mikolajewski985df3d2016-03-15 15:38:54 +0900163 // Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
164 // unique string that can be used to identify the document referred to by the cursor.
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +0900165 // If the cursor is a merged cursor over multiple authorities, then prefix the ids
166 // with the authority to avoid collisions.
167 if (mCursor instanceof MergeCursor) {
Garfield Tan2010ff72016-09-30 14:55:32 -0700168 mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY)
169 + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +0900170 } else {
Tomasz Mikolajewski06b036f2016-04-26 11:11:17 +0900171 mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
Tomasz Mikolajewski20c04c52016-03-15 15:55:46 +0900172 }
Ben Kwab8a5e082015-12-07 13:25:27 -0800173 }
174
175 // Populate the positions.
176 mPositions.clear();
177 for (int i = 0; i < mCursorCount; ++i) {
Garfield Tan2010ff72016-09-30 14:55:32 -0700178 mPositions.put(mIds[i], i);
Ben Kwab8a5e082015-12-07 13:25:27 -0800179 }
180 }
181
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +0900182 public @Nullable Cursor getItem(String modelId) {
Ben Kwa0497da82015-11-30 23:00:02 -0800183 Integer pos = mPositions.get(modelId);
Steve McKay5a22a112016-04-12 11:29:10 -0700184 if (pos == null) {
185 if (DEBUG) Log.d(TAG, "Unabled to find cursor position for modelId: " + modelId);
186 return null;
Ben Kwa0497da82015-11-30 23:00:02 -0800187 }
Steve McKay5a22a112016-04-12 11:29:10 -0700188
189 if (!mCursor.moveToPosition(pos)) {
190 if (DEBUG) Log.d(TAG,
191 "Unabled to move cursor to position " + pos + " for modelId: " + modelId);
192 return null;
193 }
194
195 return mCursor;
Ben Kwa0497da82015-11-30 23:00:02 -0800196 }
197
Steve McKay990f76e2016-09-16 12:36:58 -0700198 public boolean isEmpty() {
Ben Kwa0497da82015-11-30 23:00:02 -0800199 return mCursorCount == 0;
200 }
201
202 boolean isLoading() {
203 return mIsLoading;
204 }
205
Steve McKayc8889af2016-09-23 11:22:41 -0700206 public List<DocumentInfo> getDocuments(Selection selection) {
Steve McKay84769b82016-06-09 10:46:07 -0700207 final int size = (selection != null) ? selection.size() : 0;
Ben Kwa0497da82015-11-30 23:00:02 -0800208
209 final List<DocumentInfo> docs = new ArrayList<>(size);
Steve McKay84769b82016-06-09 10:46:07 -0700210 // NOTE: That as this now iterates over only final (non-provisional) selection.
211 for (String modelId: selection) {
Steve McKay990f76e2016-09-16 12:36:58 -0700212 DocumentInfo doc = getDocument(modelId);
213 if (doc == null) {
214 Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
Steve McKay5a22a112016-04-12 11:29:10 -0700215 continue;
216 }
Steve McKay990f76e2016-09-16 12:36:58 -0700217 docs.add(doc);
Ben Kwa0497da82015-11-30 23:00:02 -0800218 }
219 return docs;
220 }
221
Steve McKay990f76e2016-09-16 12:36:58 -0700222 public @Nullable DocumentInfo getDocument(String modelId) {
223 final Cursor cursor = getItem(modelId);
224 return (cursor == null)
225 ? null
226 : DocumentInfo.fromDirectoryCursor(cursor);
227 }
228
Steve McKayd0718952016-10-10 13:43:36 -0700229 public List<DocumentInfo> loadDocuments(Selection selection, Predicate<Cursor> filter) {
230 final int size = (selection != null) ? selection.size() : 0;
231
232 final List<DocumentInfo> docs = new ArrayList<>(size);
233 for (String modelId: selection) {
234 loadDocument(docs, modelId, filter);
235 }
236 return docs;
237 }
238
239 /**
240 * @param docs
241 * @return DocumentInfo, or null. If filter returns false, null will be returned.
242 */
243 private void loadDocument(
244 List<DocumentInfo> docs, String modelId, Predicate<Cursor> filter) {
245 final Cursor cursor = getItem(modelId);
246
247 if (cursor == null) {
248 Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
249 }
250
251 if (filter.test(cursor)) {
252 docs.add(DocumentInfo.fromDirectoryCursor(cursor));
253 } else {
254 if (DEBUG) Log.v(TAG, "Filtered document from results: " + modelId);
255 }
256 }
257
Steve McKay84769b82016-06-09 10:46:07 -0700258 public Uri getItemUri(String modelId) {
259 final Cursor cursor = getItem(modelId);
260 return DocumentInfo.getUri(cursor);
261 }
262
Garfield, Tan11d23482016-08-05 09:33:29 -0700263 /**
264 * @return An ordered array of model IDs representing the documents in the model. It is sorted
265 * according to the current sort order, which was set by the last model update.
266 */
267 public String[] getModelIds() {
268 return mIds;
269 }
270
Steve McKay990f76e2016-09-16 12:36:58 -0700271 public static class Update {
Ben Kwa0497da82015-11-30 23:00:02 -0800272
Steve McKay990f76e2016-09-16 12:36:58 -0700273 public static final Update UPDATE = new Update();
Ben Kwa472103f2016-02-10 15:48:25 -0800274
Steve McKay990f76e2016-09-16 12:36:58 -0700275 @IntDef(value = {
276 TYPE_UPDATE,
277 TYPE_UPDATE_ERROR
278 })
279 @Retention(RetentionPolicy.SOURCE)
280 public @interface UpdateType {}
281 public static final int TYPE_UPDATE = 0;
282 public static final int TYPE_UPDATE_ERROR = 1;
Ben Kwa0497da82015-11-30 23:00:02 -0800283
Steve McKay990f76e2016-09-16 12:36:58 -0700284 private final @UpdateType int mType;
285 private final @Nullable Exception mException;
286
287 private Update() {
288 mType = TYPE_UPDATE;
289 mException = null;
290 }
291
292 public Update(Exception exception) {
293 assert(exception != null);
294 mType = TYPE_UPDATE_ERROR;
295 mException = exception;
296 }
297
298 public boolean isUpdate() {
299 return mType == TYPE_UPDATE;
300 }
301
302 public boolean hasError() {
303 return mType == TYPE_UPDATE_ERROR;
304 }
305
306 public @Nullable Exception getError() {
307 return mException;
308 }
Ben Kwa0497da82015-11-30 23:00:02 -0800309 }
Ben Kwa0497da82015-11-30 23:00:02 -0800310}