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 androidx.annotation.IntDef;
24 import android.app.AuthenticationRequiredException;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.provider.DocumentsContract;
29 import android.provider.DocumentsContract.Document;
30 import android.util.Log;
31 
32 import androidx.annotation.Nullable;
33 import androidx.annotation.VisibleForTesting;
34 import androidx.recyclerview.selection.Selection;
35 
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 
41 import java.lang.annotation.Retention;
42 import java.lang.annotation.RetentionPolicy;
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.HashSet;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Set;
49 import java.util.function.Predicate;
50 
51 /**
52  * The data model for the current loaded directory.
53  */
54 @VisibleForTesting
55 public class Model {
56 
57     private static final String TAG = "Model";
58 
59     public @Nullable String info;
60     public @Nullable String error;
61     public @Nullable DocumentInfo doc;
62 
63     private final Features mFeatures;
64 
65     /** Maps Model ID to cursor positions, for looking up items by Model ID. */
66     private final Map<String, Integer> mPositions = new HashMap<>();
67     private final Set<String> mFileNames = new HashSet<>();
68 
69     private boolean mIsLoading;
70     private List<EventListener<Update>> mUpdateListeners = new ArrayList<>();
71     private @Nullable Cursor mCursor;
72     private int mCursorCount;
73     private String mIds[] = new String[0];
74 
Model(Features features)75     public Model(Features features) {
76         mFeatures = features;
77     }
78 
addUpdateListener(EventListener<Update> listener)79     public void addUpdateListener(EventListener<Update> listener) {
80         mUpdateListeners.add(listener);
81     }
82 
removeUpdateListener(EventListener<Update> listener)83     public void removeUpdateListener(EventListener<Update> listener) {
84         mUpdateListeners.remove(listener);
85     }
86 
notifyUpdateListeners()87     private void notifyUpdateListeners() {
88         for (EventListener<Update> handler: mUpdateListeners) {
89             handler.accept(Update.UPDATE);
90         }
91     }
92 
notifyUpdateListeners(Exception e)93     private void notifyUpdateListeners(Exception e) {
94         Update error = new Update(e, mFeatures.isRemoteActionsEnabled());
95         for (EventListener<Update> handler: mUpdateListeners) {
96             handler.accept(error);
97         }
98     }
99 
reset()100     public void reset() {
101         mCursor = null;
102         mCursorCount = 0;
103         mIds = new String[0];
104         mPositions.clear();
105         info = null;
106         error = null;
107         doc = null;
108         mIsLoading = false;
109         mFileNames.clear();
110         notifyUpdateListeners();
111     }
112 
113     @VisibleForTesting
update(DirectoryResult result)114     public void update(DirectoryResult result) {
115         assert(result != null);
116         if (DEBUG) {
117             Log.i(TAG, "Updating model with new result set.");
118         }
119 
120         if (result.exception != null) {
121             Log.e(TAG, "Error while loading directory contents", result.exception);
122             reset(); // Resets this model to avoid access to old cursors.
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             // Prefix the ids with the authority to avoid collisions.
164             mIds[pos] = ModelId.build(mCursor);
165             mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME));
166         }
167 
168         // Populate the positions.
169         mPositions.clear();
170         for (int i = 0; i < mCursorCount; ++i) {
171             mPositions.put(mIds[i], i);
172         }
173     }
174 
hasFileWithName(String name)175     public boolean hasFileWithName(String name) {
176         return mFileNames.contains(name);
177     }
178 
getItem(String modelId)179     public @Nullable Cursor getItem(String modelId) {
180         Integer pos = mPositions.get(modelId);
181         if (pos == null) {
182             if (DEBUG) {
183                 Log.d(TAG, "Unabled to find cursor position for modelId: " + modelId);
184             }
185             return null;
186         }
187 
188         if (!mCursor.moveToPosition(pos)) {
189             if (DEBUG) {
190                 Log.d(TAG,
191                     "Unabled to move cursor to position " + pos + " for modelId: " + modelId);
192             }
193             return null;
194         }
195 
196         return mCursor;
197     }
198 
isLoading()199     public boolean isLoading() {
200         return mIsLoading;
201     }
202 
getDocuments(Selection<String> selection)203     public List<DocumentInfo> getDocuments(Selection<String> selection) {
204         return loadDocuments(selection, DocumentFilters.ANY);
205     }
206 
getDocument(String modelId)207     public @Nullable DocumentInfo getDocument(String modelId) {
208         final Cursor cursor = getItem(modelId);
209         return (cursor == null)
210                 ? null
211                 : DocumentInfo.fromDirectoryCursor(cursor);
212     }
213 
loadDocuments(Selection<String> selection, Predicate<Cursor> filter)214     public List<DocumentInfo> loadDocuments(Selection<String> selection, Predicate<Cursor> filter) {
215         final int size = (selection != null) ? selection.size() : 0;
216 
217         final List<DocumentInfo> docs =  new ArrayList<>(size);
218         DocumentInfo doc;
219         for (String modelId: selection) {
220             doc = loadDocument(modelId, filter);
221             if (doc != null) {
222                 docs.add(doc);
223             }
224         }
225         return docs;
226     }
227 
hasDocuments(Selection<String> selection, Predicate<Cursor> filter)228     public boolean hasDocuments(Selection<String> selection, Predicate<Cursor> filter) {
229         for (String modelId: selection) {
230             if (loadDocument(modelId, filter) != null) {
231                 return true;
232             }
233         }
234         return false;
235     }
236 
237     /**
238      * @return DocumentInfo, or null. If filter returns false, null will be returned.
239      */
loadDocument(String modelId, Predicate<Cursor> filter)240     private @Nullable DocumentInfo loadDocument(String modelId, Predicate<Cursor> filter) {
241         final Cursor cursor = getItem(modelId);
242 
243         if (cursor == null) {
244             Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
245             return null;
246         }
247 
248         if (filter.test(cursor)) {
249             return DocumentInfo.fromDirectoryCursor(cursor);
250         }
251 
252         if (VERBOSE) Log.v(TAG, "Filtered out document from results: " + modelId);
253         return null;
254     }
255 
getItemUri(String modelId)256     public Uri getItemUri(String modelId) {
257         final Cursor cursor = getItem(modelId);
258         return DocumentInfo.getUri(cursor);
259     }
260 
261     /**
262      * @return An ordered array of model IDs representing the documents in the model. It is sorted
263      *         according to the current sort order, which was set by the last model update.
264      */
getModelIds()265     public String[] getModelIds() {
266         return mIds;
267     }
268 
269     public static class Update {
270 
271         public static final Update UPDATE = new Update();
272 
273         @IntDef(value = {
274                 TYPE_UPDATE,
275                 TYPE_UPDATE_EXCEPTION
276         })
277         @Retention(RetentionPolicy.SOURCE)
278         public @interface UpdateType {}
279         public static final int TYPE_UPDATE = 0;
280         public static final int TYPE_UPDATE_EXCEPTION = 1;
281 
282         private final @UpdateType int mUpdateType;
283         private final @Nullable Exception mException;
284         private final boolean mRemoteActionEnabled;
285 
Update()286         private Update() {
287             mUpdateType = TYPE_UPDATE;
288             mException = null;
289             mRemoteActionEnabled = false;
290         }
291 
Update(Exception exception, boolean remoteActionsEnabled)292         public Update(Exception exception, boolean remoteActionsEnabled) {
293             assert(exception != null);
294             mUpdateType = TYPE_UPDATE_EXCEPTION;
295             mException = exception;
296             mRemoteActionEnabled = remoteActionsEnabled;
297         }
298 
isUpdate()299         public boolean isUpdate() {
300             return mUpdateType == TYPE_UPDATE;
301         }
302 
hasException()303         public boolean hasException() {
304             return mUpdateType == TYPE_UPDATE_EXCEPTION;
305         }
306 
hasAuthenticationException()307         public boolean hasAuthenticationException() {
308             return mRemoteActionEnabled
309                     && hasException()
310                     && mException instanceof AuthenticationRequiredException;
311         }
312 
getException()313         public @Nullable Exception getException() {
314             return mException;
315         }
316     }
317 }
318