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
17package com.android.documentsui;
18
19import static com.android.documentsui.base.DocumentInfo.getCursorString;
20import static com.android.documentsui.base.SharedMinimal.DEBUG;
21import static com.android.documentsui.base.SharedMinimal.VERBOSE;
22
23import android.annotation.IntDef;
24import android.app.AuthenticationRequiredException;
25import android.database.Cursor;
26import android.database.MergeCursor;
27import android.net.Uri;
28import android.os.Bundle;
29import android.provider.DocumentsContract;
30import android.provider.DocumentsContract.Document;
31import android.support.annotation.Nullable;
32import android.support.annotation.VisibleForTesting;
33import android.util.Log;
34
35import com.android.documentsui.DirectoryResult;
36import com.android.documentsui.base.DocumentFilters;
37import com.android.documentsui.base.DocumentInfo;
38import com.android.documentsui.base.EventListener;
39import com.android.documentsui.base.Features;
40import com.android.documentsui.roots.RootCursorWrapper;
41import com.android.documentsui.selection.Selection;
42
43import java.lang.annotation.Retention;
44import java.lang.annotation.RetentionPolicy;
45import java.util.ArrayList;
46import java.util.HashMap;
47import java.util.HashSet;
48import java.util.List;
49import java.util.Map;
50import java.util.Set;
51import java.util.function.Predicate;
52
53/**
54 * The data model for the current loaded directory.
55 */
56@VisibleForTesting
57public 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
77    public Model(Features features) {
78        mFeatures = features;
79    }
80
81    public void addUpdateListener(EventListener<Update> listener) {
82        mUpdateListeners.add(listener);
83    }
84
85    public void removeUpdateListener(EventListener<Update> listener) {
86        mUpdateListeners.remove(listener);
87    }
88
89    private void notifyUpdateListeners() {
90        for (EventListener<Update> handler: mUpdateListeners) {
91            handler.accept(Update.UPDATE);
92        }
93    }
94
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
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
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
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     */
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
182    public boolean hasFileWithName(String name) {
183        return mFileNames.contains(name);
184    }
185
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
202    public boolean isLoading() {
203        return mIsLoading;
204    }
205
206    public List<DocumentInfo> getDocuments(Selection selection) {
207        return loadDocuments(selection, DocumentFilters.ANY);
208    }
209
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
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
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     */
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
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     */
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
289        private Update() {
290            mUpdateType = TYPE_UPDATE;
291            mException = null;
292            mRemoteActionEnabled = false;
293        }
294
295        public Update(Exception exception, boolean remoteActionsEnabled) {
296            assert(exception != null);
297            mUpdateType = TYPE_UPDATE_EXCEPTION;
298            mException = exception;
299            mRemoteActionEnabled = remoteActionsEnabled;
300        }
301
302        public boolean isUpdate() {
303            return mUpdateType == TYPE_UPDATE;
304        }
305
306        public boolean hasException() {
307            return mUpdateType == TYPE_UPDATE_EXCEPTION;
308        }
309
310        public boolean hasAuthenticationException() {
311            return mRemoteActionEnabled
312                    && hasException()
313                    && mException instanceof AuthenticationRequiredException;
314        }
315
316        public @Nullable Exception getException() {
317            return mException;
318        }
319    }
320}
321