Model.java revision 3d988a969f39ca34ef89c449a0675164499c9a3a
1/*
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
19import static com.android.documentsui.Shared.DEBUG;
20import static com.android.documentsui.State.SORT_ORDER_DISPLAY_NAME;
21import static com.android.documentsui.State.SORT_ORDER_LAST_MODIFIED;
22import static com.android.documentsui.State.SORT_ORDER_SIZE;
23import static com.android.documentsui.model.DocumentInfo.getCursorLong;
24import static com.android.documentsui.model.DocumentInfo.getCursorString;
25import static com.android.internal.util.Preconditions.checkNotNull;
26
27import android.database.Cursor;
28import android.os.Bundle;
29import android.os.Looper;
30import android.provider.DocumentsContract;
31import android.provider.DocumentsContract.Document;
32import android.support.annotation.Nullable;
33import android.support.annotation.VisibleForTesting;
34import android.util.Log;
35
36import com.android.documentsui.DirectoryResult;
37import com.android.documentsui.RootCursorWrapper;
38import com.android.documentsui.dirlist.MultiSelectManager.Selection;
39import com.android.documentsui.model.DocumentInfo;
40
41import java.util.ArrayList;
42import java.util.HashMap;
43import java.util.List;
44import java.util.Map;
45
46/**
47 * The data model for the current loaded directory.
48 */
49@VisibleForTesting
50public class Model {
51    private static final String TAG = "Model";
52
53    private boolean mIsLoading;
54    private List<UpdateListener> mUpdateListeners = new ArrayList<>();
55    @Nullable private Cursor mCursor;
56    private int mCursorCount;
57    /** Maps Model ID to cursor positions, for looking up items by Model ID. */
58    private Map<String, Integer> mPositions = new HashMap<>();
59    /**
60     * A sorted array of model IDs for the files currently in the Model.  Sort order is determined
61     * by {@link #mSortOrder}
62     */
63    private List<String> mIds = new ArrayList<>();
64    private int mSortOrder = SORT_ORDER_DISPLAY_NAME;
65
66    @Nullable String info;
67    @Nullable String error;
68
69    /**
70     * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
71     * string that can be used to identify the document referred to by the cursor.
72     *
73     * @param c A cursor that refers to a document.
74     */
75    private static String createModelId(Cursor c) {
76        // TODO: Maybe more efficient to use just the document ID, in cases where there is only one
77        // authority (which should be the majority of cases).
78        return createModelId(
79                getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY),
80                getCursorString(c, Document.COLUMN_DOCUMENT_ID));
81    }
82
83    /**
84     * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
85     * string that can be used to identify the document referred to by the cursor.
86     *
87     * @param c A cursor that refers to a document.
88     */
89    static String createModelId(String authority, String docId) {
90        return authority + "|" + docId;
91    }
92
93    private void notifyUpdateListeners() {
94        for (UpdateListener listener: mUpdateListeners) {
95            listener.onModelUpdate(this);
96        }
97    }
98
99    private void notifyUpdateListeners(Exception e) {
100        for (UpdateListener listener: mUpdateListeners) {
101            listener.onModelUpdateFailed(e);
102        }
103    }
104
105    void update(DirectoryResult result) {
106        if (DEBUG) Log.i(TAG, "Updating model with new result set.");
107
108        if (result == null) {
109            mCursor = null;
110            mCursorCount = 0;
111            mIds.clear();
112            mPositions.clear();
113            info = null;
114            error = null;
115            mIsLoading = false;
116            notifyUpdateListeners();
117            return;
118        }
119
120        if (result.exception != null) {
121            Log.e(TAG, "Error while loading directory contents", result.exception);
122            notifyUpdateListeners(result.exception);
123            return;
124        }
125
126        mCursor = result.cursor;
127        mCursorCount = mCursor.getCount();
128        mSortOrder = result.sortOrder;
129
130        updateModelData();
131
132        final Bundle extras = mCursor.getExtras();
133        if (extras != null) {
134            info = extras.getString(DocumentsContract.EXTRA_INFO);
135            error = extras.getString(DocumentsContract.EXTRA_ERROR);
136            mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
137        }
138
139        notifyUpdateListeners();
140    }
141
142    @VisibleForTesting
143    int getItemCount() {
144        return mCursorCount;
145    }
146
147    /**
148     * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
149     * according to the current sort order.
150     */
151    private void updateModelData() {
152        int[] positions = new int[mCursorCount];
153        mIds.clear();
154        String[] stringValues = new String[mCursorCount];
155        long[] longValues = null;
156
157        if (mSortOrder == SORT_ORDER_LAST_MODIFIED || mSortOrder == SORT_ORDER_SIZE) {
158            longValues = new long[mCursorCount];
159        }
160
161        mCursor.moveToPosition(-1);
162        for (int pos = 0; pos < mCursorCount; ++pos) {
163            mCursor.moveToNext();
164            positions[pos] = pos;
165            mIds.add(createModelId(mCursor));
166
167            switch(mSortOrder) {
168                case SORT_ORDER_DISPLAY_NAME:
169                    final String mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
170                    final String displayName = getCursorString(
171                            mCursor, Document.COLUMN_DISPLAY_NAME);
172                    if (Document.MIME_TYPE_DIR.equals(mimeType)) {
173                        stringValues[pos] = DocumentInfo.DIR_PREFIX + displayName;
174                    } else {
175                        stringValues[pos] = displayName;
176                    }
177                    break;
178                case SORT_ORDER_LAST_MODIFIED:
179                    longValues[pos] = getLastModified(mCursor);
180                    stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
181                    break;
182                case SORT_ORDER_SIZE:
183                    longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
184                    stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
185                    break;
186            }
187        }
188
189        switch (mSortOrder) {
190            case SORT_ORDER_DISPLAY_NAME:
191                binarySort(stringValues, positions, mIds);
192                break;
193            case SORT_ORDER_LAST_MODIFIED:
194            case SORT_ORDER_SIZE:
195                binarySort(longValues, stringValues, positions, mIds);
196                break;
197        }
198
199        // Populate the positions.
200        mPositions.clear();
201        for (int i = 0; i < mCursorCount; ++i) {
202            mPositions.put(mIds.get(i), positions[i]);
203        }
204    }
205
206    /**
207     * Sorts model data. Takes three columns of index-corresponded data. The first column is the
208     * sort key. Rows are sorted in ascending alphabetical order on the sort key. This code is based
209     * on TimSort.binarySort().
210     *
211     * @param sortKey Data is sorted in ascending alphabetical order.
212     * @param positions Cursor positions to be sorted.
213     * @param ids Model IDs to be sorted.
214     */
215    private static void binarySort(String[] sortKey, int[] positions, List<String> ids) {
216        final int count = positions.length;
217        for (int start = 1; start < count; start++) {
218            final int pivotPosition = positions[start];
219            final String pivotValue = sortKey[start];
220            final String pivotId = ids.get(start);
221
222            int left = 0;
223            int right = start;
224
225            while (left < right) {
226                int mid = (left + right) >>> 1;
227
228                final String lhs = pivotValue;
229                final String rhs = sortKey[mid];
230                final int compare = DocumentInfo.compareToIgnoreCaseNullable(lhs, rhs);
231
232                if (compare < 0) {
233                    right = mid;
234                } else {
235                    left = mid + 1;
236                }
237            }
238
239            int n = start - left;
240            switch (n) {
241                case 2:
242                    positions[left + 2] = positions[left + 1];
243                    sortKey[left + 2] = sortKey[left + 1];
244                    ids.set(left + 2, ids.get(left + 1));
245                case 1:
246                    positions[left + 1] = positions[left];
247                    sortKey[left + 1] = sortKey[left];
248                    ids.set(left + 1, ids.get(left));
249                    break;
250                default:
251                    System.arraycopy(positions, left, positions, left + 1, n);
252                    System.arraycopy(sortKey, left, sortKey, left + 1, n);
253                    for (int i = n; i >= 1; --i) {
254                        ids.set(left + i, ids.get(left + i - 1));
255                    }
256            }
257
258            positions[left] = pivotPosition;
259            sortKey[left] = pivotValue;
260            ids.set(left, pivotId);
261        }
262    }
263
264    /**
265     * Sorts model data. Takes four columns of index-corresponded data. The first column is the sort
266     * key, and the second is an array of mime types. The rows are first bucketed by mime type
267     * (directories vs documents) and then each bucket is sorted independently in descending
268     * numerical order on the sort key. This code is based on TimSort.binarySort().
269     *
270     * @param sortKey Data is sorted in descending numerical order.
271     * @param mimeTypes Corresponding mime types. Directories will be sorted ahead of documents.
272     * @param positions Cursor positions to be sorted.
273     * @param ids Model IDs to be sorted.
274     */
275    private static void binarySort(
276            long[] sortKey, String[] mimeTypes, int[] positions, List<String> ids) {
277        final int count = positions.length;
278        for (int start = 1; start < count; start++) {
279            final int pivotPosition = positions[start];
280            final long pivotValue = sortKey[start];
281            final String pivotMime = mimeTypes[start];
282            final String pivotId = ids.get(start);
283
284            int left = 0;
285            int right = start;
286
287            while (left < right) {
288                int mid = ((left + right) >>> 1);
289
290                // First bucket by mime type.  Directories always go in front.
291                int compare = 0;
292                final boolean lhsIsDir = Document.MIME_TYPE_DIR.equals(pivotMime);
293                final boolean rhsIsDir = Document.MIME_TYPE_DIR.equals(mimeTypes[mid]);
294                if (lhsIsDir && !rhsIsDir) {
295                    compare = -1;
296                } else if (!lhsIsDir && rhsIsDir) {
297                    compare = 1;
298                } else {
299                    final long lhs = pivotValue;
300                    final long rhs = sortKey[mid];
301                    // Sort in descending numerical order. This matches legacy behaviour, which
302                    // yields largest or most recent items on top.
303                    compare = -Long.compare(lhs, rhs);
304                }
305
306                // If numerical comparison yields a tie, use document ID as a tie breaker.  This
307                // will yield stable results even if incoming items are continually shuffling and
308                // have identical numerical sort keys.  One common example of this scenario is seen
309                // when sorting a set of active downloads by mod time.
310                if (compare == 0) {
311                    compare = pivotId.compareTo(ids.get(mid));
312                }
313
314                if (compare < 0) {
315                    right = mid;
316                } else {
317                    left = mid + 1;
318                }
319            }
320
321            int n = start - left;
322            switch (n) {
323                case 2:
324                    positions[left + 2] = positions[left + 1];
325                    sortKey[left + 2] = sortKey[left + 1];
326                    mimeTypes[left + 2] = mimeTypes[left + 1];
327                    ids.set(left + 2, ids.get(left + 1));
328                case 1:
329                    positions[left + 1] = positions[left];
330                    sortKey[left + 1] = sortKey[left];
331                    mimeTypes[left + 1] = mimeTypes[left];
332                    ids.set(left + 1, ids.get(left));
333                    break;
334                default:
335                    System.arraycopy(positions, left, positions, left + 1, n);
336                    System.arraycopy(sortKey, left, sortKey, left + 1, n);
337                    System.arraycopy(mimeTypes, left, mimeTypes, left + 1, n);
338                    for (int i = n; i >= 1; --i) {
339                        ids.set(left + i, ids.get(left + i - 1));
340                    }
341            }
342
343            positions[left] = pivotPosition;
344            sortKey[left] = pivotValue;
345            mimeTypes[left] = pivotMime;
346            ids.set(left, pivotId);
347        }
348    }
349
350    /**
351     * @return Timestamp for the given document. Some docs (e.g. active downloads) have a null
352     * timestamp - these will be replaced with MAX_LONG so that such files get sorted to the top
353     * when sorting by date.
354     */
355    long getLastModified(Cursor cursor) {
356        long l = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
357        return (l == -1) ? Long.MAX_VALUE : l;
358    }
359
360    public @Nullable Cursor getItem(String modelId) {
361        Integer pos = mPositions.get(modelId);
362        if (pos != null) {
363            mCursor.moveToPosition(pos);
364            return mCursor;
365        }
366        return null;
367    }
368
369    boolean isEmpty() {
370        return mCursorCount == 0;
371    }
372
373    boolean isLoading() {
374        return mIsLoading;
375    }
376
377    List<DocumentInfo> getDocuments(Selection items) {
378        final int size = (items != null) ? items.size() : 0;
379
380        final List<DocumentInfo> docs =  new ArrayList<>(size);
381        for (String modelId: items.getAll()) {
382            final Cursor cursor = getItem(modelId);
383            checkNotNull(cursor, "Cursor cannot be null.");
384            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
385            docs.add(doc);
386        }
387        return docs;
388    }
389
390    void addUpdateListener(UpdateListener listener) {
391        mUpdateListeners.add(listener);
392    }
393
394    static interface UpdateListener {
395        /**
396         * Called when a successful update has occurred.
397         */
398        void onModelUpdate(Model model);
399
400        /**
401         * Called when an update has been attempted but failed.
402         */
403        void onModelUpdateFailed(Exception e);
404    }
405
406    /**
407     * @return An ordered array of model IDs representing the documents in the model. It is sorted
408     *         according to the current sort order, which was set by the last model update.
409     */
410    public List<String> getModelIds() {
411        return mIds;
412    }
413}
414