• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 
17 package com.android.documentsui;
18 
19 import static com.android.documentsui.base.DocumentInfo.getCursorString;
20 import static com.android.documentsui.base.SharedMinimal.DEBUG;
21 import static com.android.documentsui.base.SharedMinimal.VERBOSE;
22 
23 import android.annotation.IntDef;
24 import android.app.AuthenticationRequiredException;
25 import android.database.Cursor;
26 import android.database.MergeCursor;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.provider.DocumentsContract;
30 import android.provider.DocumentsContract.Document;
31 import android.support.annotation.Nullable;
32 import android.support.annotation.VisibleForTesting;
33 import android.util.Log;
34 
35 import com.android.documentsui.DirectoryResult;
36 import com.android.documentsui.base.DocumentFilters;
37 import com.android.documentsui.base.DocumentInfo;
38 import com.android.documentsui.base.EventListener;
39 import com.android.documentsui.base.Features;
40 import com.android.documentsui.roots.RootCursorWrapper;
41 import com.android.documentsui.selection.Selection;
42 
43 import java.lang.annotation.Retention;
44 import java.lang.annotation.RetentionPolicy;
45 import java.util.ArrayList;
46 import java.util.HashMap;
47 import java.util.HashSet;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Set;
51 import java.util.function.Predicate;
52 
53 /**
54  * The data model for the current loaded directory.
55  */
56 @VisibleForTesting
57 public class Model {
58 
59     private static final String TAG = "Model";
60 
61     public @Nullable String info;
62     public @Nullable String error;
63     public @Nullable DocumentInfo doc;
64 
65     private final Features mFeatures;
66 
67     /** Maps Model ID to cursor positions, for looking up items by Model ID. */
68     private final Map<String, Integer> mPositions = new HashMap<>();
69     private final Set<String> mFileNames = new HashSet<>();
70 
71     private boolean mIsLoading;
72     private List<EventListener<Update>> mUpdateListeners = new ArrayList<>();
73     private @Nullable Cursor mCursor;
74     private int mCursorCount;
75     private String mIds[] = new String[0];
76 
Model(Features features)77     public Model(Features features) {
78         mFeatures = features;
79     }
80 
addUpdateListener(EventListener<Update> listener)81     public void addUpdateListener(EventListener<Update> listener) {
82         mUpdateListeners.add(listener);
83     }
84 
removeUpdateListener(EventListener<Update> listener)85     public void removeUpdateListener(EventListener<Update> listener) {
86         mUpdateListeners.remove(listener);
87     }
88 
notifyUpdateListeners()89     private void notifyUpdateListeners() {
90         for (EventListener<Update> handler: mUpdateListeners) {
91             handler.accept(Update.UPDATE);
92         }
93     }
94 
notifyUpdateListeners(Exception e)95     private void notifyUpdateListeners(Exception e) {
96         Update error = new Update(e, mFeatures.isRemoteActionsEnabled());
97         for (EventListener<Update> handler: mUpdateListeners) {
98             handler.accept(error);
99         }
100     }
101 
reset()102     public void reset() {
103         mCursor = null;
104         mCursorCount = 0;
105         mIds = new String[0];
106         mPositions.clear();
107         info = null;
108         error = null;
109         doc = null;
110         mIsLoading = false;
111         mFileNames.clear();
112         notifyUpdateListeners();
113     }
114 
115     @VisibleForTesting
update(DirectoryResult result)116     protected void update(DirectoryResult result) {
117         assert(result != null);
118 
119         if (DEBUG) Log.i(TAG, "Updating model with new result set.");
120 
121         if (result.exception != null) {
122             Log.e(TAG, "Error while loading directory contents", result.exception);
123             reset(); // Resets this model to avoid access to old cursors.
124             notifyUpdateListeners(result.exception);
125             return;
126         }
127 
128         mCursor = result.cursor;
129         mCursorCount = mCursor.getCount();
130         doc = result.doc;
131 
132         updateModelData();
133 
134         final Bundle extras = mCursor.getExtras();
135         if (extras != null) {
136             info = extras.getString(DocumentsContract.EXTRA_INFO);
137             error = extras.getString(DocumentsContract.EXTRA_ERROR);
138             mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
139         }
140 
141         notifyUpdateListeners();
142     }
143 
144     @VisibleForTesting
getItemCount()145     public int getItemCount() {
146         return mCursorCount;
147     }
148 
149     /**
150      * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
151      * according to the current sort order.
152      */
updateModelData()153     private void updateModelData() {
154         mIds = new String[mCursorCount];
155         mFileNames.clear();
156         mCursor.moveToPosition(-1);
157         for (int pos = 0; pos < mCursorCount; ++pos) {
158             if (!mCursor.moveToNext()) {
159                 Log.e(TAG, "Fail to move cursor to next pos: " + pos);
160                 return;
161             }
162             // Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
163             // unique string that can be used to identify the document referred to by the cursor.
164             // If the cursor is a merged cursor over multiple authorities, then prefix the ids
165             // with the authority to avoid collisions.
166             if (mCursor instanceof MergeCursor) {
167                 mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY)
168                         + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
169             } else {
170                 mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
171             }
172             mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME));
173         }
174 
175         // Populate the positions.
176         mPositions.clear();
177         for (int i = 0; i < mCursorCount; ++i) {
178             mPositions.put(mIds[i], i);
179         }
180     }
181 
hasFileWithName(String name)182     public boolean hasFileWithName(String name) {
183         return mFileNames.contains(name);
184     }
185 
getItem(String modelId)186     public @Nullable Cursor getItem(String modelId) {
187         Integer pos = mPositions.get(modelId);
188         if (pos == null) {
189             if (DEBUG) Log.d(TAG, "Unabled to find cursor position for modelId: " + modelId);
190             return null;
191         }
192 
193         if (!mCursor.moveToPosition(pos)) {
194             if (DEBUG) Log.d(TAG,
195                     "Unabled to move cursor to position " + pos + " for modelId: " + modelId);
196             return null;
197         }
198 
199         return mCursor;
200     }
201 
isLoading()202     public boolean isLoading() {
203         return mIsLoading;
204     }
205 
getDocuments(Selection selection)206     public List<DocumentInfo> getDocuments(Selection selection) {
207         return loadDocuments(selection, DocumentFilters.ANY);
208     }
209 
getDocument(String modelId)210     public @Nullable DocumentInfo getDocument(String modelId) {
211         final Cursor cursor = getItem(modelId);
212         return (cursor == null)
213                 ? null
214                 : DocumentInfo.fromDirectoryCursor(cursor);
215     }
216 
loadDocuments(Selection selection, Predicate<Cursor> filter)217     public List<DocumentInfo> loadDocuments(Selection selection, Predicate<Cursor> filter) {
218         final int size = (selection != null) ? selection.size() : 0;
219 
220         final List<DocumentInfo> docs =  new ArrayList<>(size);
221         DocumentInfo doc;
222         for (String modelId: selection) {
223             doc = loadDocument(modelId, filter);
224             if (doc != null) {
225                 docs.add(doc);
226             }
227         }
228         return docs;
229     }
230 
hasDocuments(Selection selection, Predicate<Cursor> filter)231     public boolean hasDocuments(Selection selection, Predicate<Cursor> filter) {
232         for (String modelId: selection) {
233             if (loadDocument(modelId, filter) != null) {
234                 return true;
235             }
236         }
237         return false;
238     }
239 
240     /**
241      * @return DocumentInfo, or null. If filter returns false, null will be returned.
242      */
loadDocument(String modelId, Predicate<Cursor> filter)243     private @Nullable DocumentInfo loadDocument(String modelId, Predicate<Cursor> filter) {
244         final Cursor cursor = getItem(modelId);
245 
246         if (cursor == null) {
247             Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
248             return null;
249         }
250 
251         if (filter.test(cursor)) {
252             return DocumentInfo.fromDirectoryCursor(cursor);
253         }
254 
255         if (VERBOSE) Log.v(TAG, "Filtered out document from results: " + modelId);
256         return null;
257     }
258 
getItemUri(String modelId)259     public Uri getItemUri(String modelId) {
260         final Cursor cursor = getItem(modelId);
261         return DocumentInfo.getUri(cursor);
262     }
263 
264     /**
265      * @return An ordered array of model IDs representing the documents in the model. It is sorted
266      *         according to the current sort order, which was set by the last model update.
267      */
getModelIds()268     public String[] getModelIds() {
269         return mIds;
270     }
271 
272     public static class Update {
273 
274         public static final Update UPDATE = new Update();
275 
276         @IntDef(value = {
277                 TYPE_UPDATE,
278                 TYPE_UPDATE_EXCEPTION
279         })
280         @Retention(RetentionPolicy.SOURCE)
281         public @interface UpdateType {}
282         public static final int TYPE_UPDATE = 0;
283         public static final int TYPE_UPDATE_EXCEPTION = 1;
284 
285         private final @UpdateType int mUpdateType;
286         private final @Nullable Exception mException;
287         private final boolean mRemoteActionEnabled;
288 
Update()289         private Update() {
290             mUpdateType = TYPE_UPDATE;
291             mException = null;
292             mRemoteActionEnabled = false;
293         }
294 
Update(Exception exception, boolean remoteActionsEnabled)295         public Update(Exception exception, boolean remoteActionsEnabled) {
296             assert(exception != null);
297             mUpdateType = TYPE_UPDATE_EXCEPTION;
298             mException = exception;
299             mRemoteActionEnabled = remoteActionsEnabled;
300         }
301 
isUpdate()302         public boolean isUpdate() {
303             return mUpdateType == TYPE_UPDATE;
304         }
305 
hasException()306         public boolean hasException() {
307             return mUpdateType == TYPE_UPDATE_EXCEPTION;
308         }
309 
hasAuthenticationException()310         public boolean hasAuthenticationException() {
311             return mRemoteActionEnabled
312                     && hasException()
313                     && mException instanceof AuthenticationRequiredException;
314         }
315 
getException()316         public @Nullable Exception getException() {
317             return mException;
318         }
319     }
320 }
321