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.Shared.DEBUG;
21 import static com.android.documentsui.base.Shared.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             notifyUpdateListeners(result.exception);
124             return;
125         }
126 
127         mCursor = result.cursor;
128         mCursorCount = mCursor.getCount();
129         doc = result.doc;
130 
131         updateModelData();
132 
133         final Bundle extras = mCursor.getExtras();
134         if (extras != null) {
135             info = extras.getString(DocumentsContract.EXTRA_INFO);
136             error = extras.getString(DocumentsContract.EXTRA_ERROR);
137             mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
138         }
139 
140         notifyUpdateListeners();
141     }
142 
143     @VisibleForTesting
getItemCount()144     public int getItemCount() {
145         return mCursorCount;
146     }
147 
148     /**
149      * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
150      * according to the current sort order.
151      */
updateModelData()152     private void updateModelData() {
153         mIds = new String[mCursorCount];
154         mFileNames.clear();
155         mCursor.moveToPosition(-1);
156         for (int pos = 0; pos < mCursorCount; ++pos) {
157             if (!mCursor.moveToNext()) {
158                 Log.e(TAG, "Fail to move cursor to next pos: " + pos);
159                 return;
160             }
161             // Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
162             // unique string that can be used to identify the document referred to by the cursor.
163             // If the cursor is a merged cursor over multiple authorities, then prefix the ids
164             // with the authority to avoid collisions.
165             if (mCursor instanceof MergeCursor) {
166                 mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY)
167                         + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
168             } else {
169                 mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
170             }
171             mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME));
172         }
173 
174         // Populate the positions.
175         mPositions.clear();
176         for (int i = 0; i < mCursorCount; ++i) {
177             mPositions.put(mIds[i], i);
178         }
179     }
180 
hasFileWithName(String name)181     public boolean hasFileWithName(String name) {
182         return mFileNames.contains(name);
183     }
184 
getItem(String modelId)185     public @Nullable Cursor getItem(String modelId) {
186         Integer pos = mPositions.get(modelId);
187         if (pos == null) {
188             if (DEBUG) Log.d(TAG, "Unabled to find cursor position for modelId: " + modelId);
189             return null;
190         }
191 
192         if (!mCursor.moveToPosition(pos)) {
193             if (DEBUG) Log.d(TAG,
194                     "Unabled to move cursor to position " + pos + " for modelId: " + modelId);
195             return null;
196         }
197 
198         return mCursor;
199     }
200 
isEmpty()201     public boolean isEmpty() {
202         return mCursorCount == 0;
203     }
204 
isLoading()205     public boolean isLoading() {
206         return mIsLoading;
207     }
208 
getDocuments(Selection selection)209     public List<DocumentInfo> getDocuments(Selection selection) {
210         return loadDocuments(selection, DocumentFilters.ANY);
211     }
212 
getDocument(String modelId)213     public @Nullable DocumentInfo getDocument(String modelId) {
214         final Cursor cursor = getItem(modelId);
215         return (cursor == null)
216                 ? null
217                 : DocumentInfo.fromDirectoryCursor(cursor);
218     }
219 
loadDocuments(Selection selection, Predicate<Cursor> filter)220     public List<DocumentInfo> loadDocuments(Selection selection, Predicate<Cursor> filter) {
221         final int size = (selection != null) ? selection.size() : 0;
222 
223         final List<DocumentInfo> docs =  new ArrayList<>(size);
224         DocumentInfo doc;
225         for (String modelId: selection) {
226             doc = loadDocument(modelId, filter);
227             if (doc != null) {
228                 docs.add(doc);
229             }
230         }
231         return docs;
232     }
233 
hasDocuments(Selection selection, Predicate<Cursor> filter)234     public boolean hasDocuments(Selection selection, Predicate<Cursor> filter) {
235         for (String modelId: selection) {
236             if (loadDocument(modelId, filter) != null) {
237                 return true;
238             }
239         }
240         return false;
241     }
242 
243     /**
244      * @return DocumentInfo, or null. If filter returns false, null will be returned.
245      */
loadDocument(String modelId, Predicate<Cursor> filter)246     private @Nullable DocumentInfo loadDocument(String modelId, Predicate<Cursor> filter) {
247         final Cursor cursor = getItem(modelId);
248 
249         if (cursor == null) {
250             Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
251             return null;
252         }
253 
254         if (filter.test(cursor)) {
255             return DocumentInfo.fromDirectoryCursor(cursor);
256         }
257 
258         if (VERBOSE) Log.v(TAG, "Filtered out document from results: " + modelId);
259         return null;
260     }
261 
getItemUri(String modelId)262     public Uri getItemUri(String modelId) {
263         final Cursor cursor = getItem(modelId);
264         return DocumentInfo.getUri(cursor);
265     }
266 
267     /**
268      * @return An ordered array of model IDs representing the documents in the model. It is sorted
269      *         according to the current sort order, which was set by the last model update.
270      */
getModelIds()271     public String[] getModelIds() {
272         return mIds;
273     }
274 
275     public static class Update {
276 
277         public static final Update UPDATE = new Update();
278 
279         @IntDef(value = {
280                 TYPE_UPDATE,
281                 TYPE_UPDATE_EXCEPTION
282         })
283         @Retention(RetentionPolicy.SOURCE)
284         public @interface UpdateType {}
285         public static final int TYPE_UPDATE = 0;
286         public static final int TYPE_UPDATE_EXCEPTION = 1;
287 
288         private final @UpdateType int mUpdateType;
289         private final @Nullable Exception mException;
290         private final boolean mRemoteActionEnabled;
291 
Update()292         private Update() {
293             mUpdateType = TYPE_UPDATE;
294             mException = null;
295             mRemoteActionEnabled = false;
296         }
297 
Update(Exception exception, boolean remoteActionsEnabled)298         public Update(Exception exception, boolean remoteActionsEnabled) {
299             assert(exception != null);
300             mUpdateType = TYPE_UPDATE_EXCEPTION;
301             mException = exception;
302             mRemoteActionEnabled = remoteActionsEnabled;
303         }
304 
isUpdate()305         public boolean isUpdate() {
306             return mUpdateType == TYPE_UPDATE;
307         }
308 
hasException()309         public boolean hasException() {
310             return mUpdateType == TYPE_UPDATE_EXCEPTION;
311         }
312 
hasAuthenticationException()313         public boolean hasAuthenticationException() {
314             return mRemoteActionEnabled
315                     && hasException()
316                     && mException instanceof AuthenticationRequiredException;
317         }
318 
getException()319         public @Nullable Exception getException() {
320             return mException;
321         }
322     }
323 }
324