blob: bea38c64ae684229953df887418d6281cbb225fe [file] [log] [blame]
Ben Kwa0497da82015-11-30 23:00:02 -08001/*
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;
Ben Kwab8a5e082015-12-07 13:25:27 -080020import 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;
Ben Kwa0497da82015-11-30 23:00:02 -080024import static com.android.documentsui.model.DocumentInfo.getCursorString;
25import static com.android.internal.util.Preconditions.checkNotNull;
Ben Kwa0497da82015-11-30 23:00:02 -080026
27import android.content.ContentProviderClient;
28import android.content.ContentResolver;
29import android.content.Context;
30import android.database.Cursor;
31import android.os.AsyncTask;
32import android.os.Bundle;
33import android.os.Looper;
34import android.provider.DocumentsContract;
35import android.provider.DocumentsContract.Document;
36import android.support.annotation.Nullable;
37import android.support.annotation.VisibleForTesting;
38import android.support.v7.widget.RecyclerView;
39import android.util.Log;
Ben Kwa0497da82015-11-30 23:00:02 -080040
Ben Kwab8a5e082015-12-07 13:25:27 -080041import com.android.documentsui.BaseActivity.SiblingProvider;
Ben Kwa0497da82015-11-30 23:00:02 -080042import com.android.documentsui.DirectoryResult;
43import com.android.documentsui.DocumentsApplication;
44import com.android.documentsui.RootCursorWrapper;
45import com.android.documentsui.dirlist.MultiSelectManager.Selection;
46import com.android.documentsui.model.DocumentInfo;
Ben Kwa0497da82015-11-30 23:00:02 -080047
48import java.util.ArrayList;
Ben Kwa0497da82015-11-30 23:00:02 -080049import java.util.HashMap;
50import java.util.List;
Ben Kwab8a5e082015-12-07 13:25:27 -080051import java.util.Map;
Ben Kwa0497da82015-11-30 23:00:02 -080052
53/**
54 * The data model for the current loaded directory.
55 */
56@VisibleForTesting
Ben Kwab8a5e082015-12-07 13:25:27 -080057public class Model implements SiblingProvider {
Ben Kwa0497da82015-11-30 23:00:02 -080058 private static final String TAG = "Model";
Ben Kwab8a5e082015-12-07 13:25:27 -080059
Ben Kwa0497da82015-11-30 23:00:02 -080060 private Context mContext;
Ben Kwa0497da82015-11-30 23:00:02 -080061 private boolean mIsLoading;
Ben Kwad72a1da2015-12-01 19:56:57 -080062 private List<UpdateListener> mUpdateListeners = new ArrayList<>();
Ben Kwa0497da82015-11-30 23:00:02 -080063 @Nullable private Cursor mCursor;
Ben Kwab8a5e082015-12-07 13:25:27 -080064 private int mCursorCount;
65 /** Maps Model ID to cursor positions, for looking up items by Model ID. */
66 private Map<String, Integer> mPositions = new HashMap<>();
67 /**
68 * A sorted array of model IDs for the files currently in the Model. Sort order is determined
69 * by {@link #mSortOrder}
70 */
71 private List<String> mIds = new ArrayList<>();
72 private int mSortOrder = SORT_ORDER_DISPLAY_NAME;
73
Ben Kwa0497da82015-11-30 23:00:02 -080074 @Nullable String info;
75 @Nullable String error;
Ben Kwa0497da82015-11-30 23:00:02 -080076
77 Model(Context context, RecyclerView.Adapter<?> viewAdapter) {
78 mContext = context;
Ben Kwa0497da82015-11-30 23:00:02 -080079 }
80
Ben Kwad72a1da2015-12-01 19:56:57 -080081 /**
Ben Kwab8a5e082015-12-07 13:25:27 -080082 * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
83 * string that can be used to identify the document referred to by the cursor.
Ben Kwad72a1da2015-12-01 19:56:57 -080084 *
85 * @param c A cursor that refers to a document.
86 */
Ben Kwab8a5e082015-12-07 13:25:27 -080087 private static String createModelId(Cursor c) {
88 // TODO: Maybe more efficient to use just the document ID, in cases where there is only one
89 // authority (which should be the majority of cases).
Ben Kwad72a1da2015-12-01 19:56:57 -080090 return getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY) +
91 "|" + getCursorString(c, Document.COLUMN_DOCUMENT_ID);
92 }
93
Ben Kwad72a1da2015-12-01 19:56:57 -080094 private void notifyUpdateListeners() {
95 for (UpdateListener listener: mUpdateListeners) {
96 listener.onModelUpdate(this);
97 }
98 }
99
100 private void notifyUpdateListeners(Exception e) {
101 for (UpdateListener listener: mUpdateListeners) {
102 listener.onModelUpdateFailed(e);
103 }
104 }
105
Ben Kwa0497da82015-11-30 23:00:02 -0800106 void update(DirectoryResult result) {
107 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
108
109 if (result == null) {
110 mCursor = null;
111 mCursorCount = 0;
Ben Kwab8a5e082015-12-07 13:25:27 -0800112 mIds.clear();
113 mPositions.clear();
Ben Kwa0497da82015-11-30 23:00:02 -0800114 info = null;
115 error = null;
116 mIsLoading = false;
Ben Kwad72a1da2015-12-01 19:56:57 -0800117 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800118 return;
119 }
120
121 if (result.exception != null) {
122 Log.e(TAG, "Error while loading directory contents", result.exception);
Ben Kwad72a1da2015-12-01 19:56:57 -0800123 notifyUpdateListeners(result.exception);
Ben Kwa0497da82015-11-30 23:00:02 -0800124 return;
125 }
126
127 mCursor = result.cursor;
128 mCursorCount = mCursor.getCount();
Ben Kwab8a5e082015-12-07 13:25:27 -0800129 mSortOrder = result.sortOrder;
Ben Kwa0497da82015-11-30 23:00:02 -0800130
Ben Kwab8a5e082015-12-07 13:25:27 -0800131 updateModelData();
Ben Kwa0497da82015-11-30 23:00:02 -0800132
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
Ben Kwad72a1da2015-12-01 19:56:57 -0800140 notifyUpdateListeners();
Ben Kwa0497da82015-11-30 23:00:02 -0800141 }
142
Ben Kwad72a1da2015-12-01 19:56:57 -0800143 @VisibleForTesting
Ben Kwa0497da82015-11-30 23:00:02 -0800144 int getItemCount() {
Ben Kwada858bf2015-12-09 14:33:49 -0800145 return mCursorCount;
Ben Kwa0497da82015-11-30 23:00:02 -0800146 }
147
148 /**
Ben Kwab8a5e082015-12-07 13:25:27 -0800149 * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
150 * according to the current sort order.
Ben Kwa0497da82015-11-30 23:00:02 -0800151 */
Ben Kwab8a5e082015-12-07 13:25:27 -0800152 private void updateModelData() {
153 int[] positions = new int[mCursorCount];
154 mIds.clear();
Ben Kwa6280de02015-12-16 19:42:08 -0800155 String[] stringValues = new String[mCursorCount];
156 long[] longValues = null;
Ben Kwab8a5e082015-12-07 13:25:27 -0800157
Ben Kwa6280de02015-12-16 19:42:08 -0800158 if (mSortOrder == SORT_ORDER_LAST_MODIFIED || mSortOrder == SORT_ORDER_SIZE) {
159 longValues = new long[mCursorCount];
Ben Kwab8a5e082015-12-07 13:25:27 -0800160 }
161
Ben Kwa0497da82015-11-30 23:00:02 -0800162 mCursor.moveToPosition(-1);
163 for (int pos = 0; pos < mCursorCount; ++pos) {
164 mCursor.moveToNext();
Ben Kwab8a5e082015-12-07 13:25:27 -0800165 positions[pos] = pos;
166 mIds.add(createModelId(mCursor));
167
168 switch(mSortOrder) {
169 case SORT_ORDER_DISPLAY_NAME:
170 final String mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
171 final String displayName = getCursorString(
172 mCursor, Document.COLUMN_DISPLAY_NAME);
173 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
Ben Kwa6280de02015-12-16 19:42:08 -0800174 stringValues[pos] = DocumentInfo.DIR_PREFIX + displayName;
Ben Kwab8a5e082015-12-07 13:25:27 -0800175 } else {
Ben Kwa6280de02015-12-16 19:42:08 -0800176 stringValues[pos] = displayName;
Ben Kwab8a5e082015-12-07 13:25:27 -0800177 }
178 break;
179 case SORT_ORDER_LAST_MODIFIED:
Ben Kwa6280de02015-12-16 19:42:08 -0800180 longValues[pos] = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
181 stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
Ben Kwab8a5e082015-12-07 13:25:27 -0800182 break;
183 case SORT_ORDER_SIZE:
Ben Kwa6280de02015-12-16 19:42:08 -0800184 longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
185 stringValues[pos] = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
Ben Kwab8a5e082015-12-07 13:25:27 -0800186 break;
187 }
188 }
189
190 switch (mSortOrder) {
191 case SORT_ORDER_DISPLAY_NAME:
Ben Kwa6280de02015-12-16 19:42:08 -0800192 binarySort(stringValues, positions, mIds);
Ben Kwab8a5e082015-12-07 13:25:27 -0800193 break;
194 case SORT_ORDER_LAST_MODIFIED:
195 case SORT_ORDER_SIZE:
Ben Kwa6280de02015-12-16 19:42:08 -0800196 binarySort(longValues, stringValues, positions, mIds);
Ben Kwab8a5e082015-12-07 13:25:27 -0800197 break;
198 }
199
200 // Populate the positions.
201 mPositions.clear();
202 for (int i = 0; i < mCursorCount; ++i) {
203 mPositions.put(mIds.get(i), positions[i]);
204 }
205 }
206
207 /**
Ben Kwa6280de02015-12-16 19:42:08 -0800208 * Sorts model data. Takes three columns of index-corresponded data. The first column is the
209 * sort key. Rows are sorted in ascending alphabetical order on the sort key. This code is based
210 * on TimSort.binarySort().
211 *
212 * @param sortKey Data is sorted in ascending alphabetical order.
213 * @param positions Cursor positions to be sorted.
214 * @param ids Model IDs to be sorted.
Ben Kwab8a5e082015-12-07 13:25:27 -0800215 */
Ben Kwa6280de02015-12-16 19:42:08 -0800216 private static void binarySort(String[] sortKey, int[] positions, List<String> ids) {
Ben Kwab8a5e082015-12-07 13:25:27 -0800217 final int count = positions.length;
218 for (int start = 1; start < count; start++) {
219 final int pivotPosition = positions[start];
Ben Kwa6280de02015-12-16 19:42:08 -0800220 final String pivotValue = sortKey[start];
Ben Kwab8a5e082015-12-07 13:25:27 -0800221 final String pivotId = ids.get(start);
222
223 int left = 0;
224 int right = start;
225
226 while (left < right) {
227 int mid = (left + right) >>> 1;
228
229 final String lhs = pivotValue;
Ben Kwa6280de02015-12-16 19:42:08 -0800230 final String rhs = sortKey[mid];
Ben Kwab8a5e082015-12-07 13:25:27 -0800231 final int compare = DocumentInfo.compareToIgnoreCaseNullable(lhs, rhs);
232
233 if (compare < 0) {
234 right = mid;
235 } else {
236 left = mid + 1;
237 }
238 }
239
240 int n = start - left;
241 switch (n) {
242 case 2:
243 positions[left + 2] = positions[left + 1];
Ben Kwa6280de02015-12-16 19:42:08 -0800244 sortKey[left + 2] = sortKey[left + 1];
Ben Kwab8a5e082015-12-07 13:25:27 -0800245 ids.set(left + 2, ids.get(left + 1));
246 case 1:
247 positions[left + 1] = positions[left];
Ben Kwa6280de02015-12-16 19:42:08 -0800248 sortKey[left + 1] = sortKey[left];
Ben Kwab8a5e082015-12-07 13:25:27 -0800249 ids.set(left + 1, ids.get(left));
250 break;
251 default:
252 System.arraycopy(positions, left, positions, left + 1, n);
Ben Kwa6280de02015-12-16 19:42:08 -0800253 System.arraycopy(sortKey, left, sortKey, left + 1, n);
Ben Kwab8a5e082015-12-07 13:25:27 -0800254 for (int i = n; i >= 1; --i) {
255 ids.set(left + i, ids.get(left + i - 1));
256 }
257 }
258
259 positions[left] = pivotPosition;
Ben Kwa6280de02015-12-16 19:42:08 -0800260 sortKey[left] = pivotValue;
Ben Kwab8a5e082015-12-07 13:25:27 -0800261 ids.set(left, pivotId);
262 }
263 }
264
265 /**
Ben Kwa6280de02015-12-16 19:42:08 -0800266 * Sorts model data. Takes four columns of index-corresponded data. The first column is the sort
267 * key, and the second is an array of mime types. The rows are first bucketed by mime type
268 * (directories vs documents) and then each bucket is sorted independently in descending
269 * numerical order on the sort key. This code is based on TimSort.binarySort().
270 *
271 * @param sortKey Data is sorted in descending numerical order.
272 * @param mimeTypes Corresponding mime types. Directories will be sorted ahead of documents.
273 * @param positions Cursor positions to be sorted.
274 * @param ids Model IDs to be sorted.
Ben Kwab8a5e082015-12-07 13:25:27 -0800275 */
Ben Kwa6280de02015-12-16 19:42:08 -0800276 private static void binarySort(
277 long[] sortKey, String[] mimeTypes, int[] positions, List<String> ids) {
Ben Kwab8a5e082015-12-07 13:25:27 -0800278 final int count = positions.length;
279 for (int start = 1; start < count; start++) {
280 final int pivotPosition = positions[start];
Ben Kwa6280de02015-12-16 19:42:08 -0800281 final long pivotValue = sortKey[start];
282 final String pivotMime = mimeTypes[start];
Ben Kwab8a5e082015-12-07 13:25:27 -0800283 final String pivotId = ids.get(start);
284
285 int left = 0;
286 int right = start;
287
288 while (left < right) {
Ben Kwa6280de02015-12-16 19:42:08 -0800289 int mid = ((left + right) >>> 1);
Ben Kwab8a5e082015-12-07 13:25:27 -0800290
Ben Kwa6280de02015-12-16 19:42:08 -0800291 // First bucket by mime type. Directories always go in front.
292 int compare = 0;
293 final boolean lhsIsDir = Document.MIME_TYPE_DIR.equals(pivotMime);
294 final boolean rhsIsDir = Document.MIME_TYPE_DIR.equals(mimeTypes[mid]);
295 if (lhsIsDir && !rhsIsDir) {
296 compare = -1;
297 } else if (!lhsIsDir && rhsIsDir) {
298 compare = 1;
299 } else {
300 final long lhs = pivotValue;
301 final long rhs = sortKey[mid];
302 // Sort in descending numerical order. This matches legacy behaviour, which yields
303 // largest or most recent items on top.
304 compare = -Long.compare(lhs, rhs);
305 }
Ben Kwab8a5e082015-12-07 13:25:27 -0800306
307 if (compare < 0) {
308 right = mid;
309 } else {
310 left = mid + 1;
311 }
312 }
313
314 int n = start - left;
315 switch (n) {
316 case 2:
317 positions[left + 2] = positions[left + 1];
Ben Kwa6280de02015-12-16 19:42:08 -0800318 sortKey[left + 2] = sortKey[left + 1];
319 mimeTypes[left + 2] = mimeTypes[left + 1];
Ben Kwab8a5e082015-12-07 13:25:27 -0800320 ids.set(left + 2, ids.get(left + 1));
321 case 1:
322 positions[left + 1] = positions[left];
Ben Kwa6280de02015-12-16 19:42:08 -0800323 sortKey[left + 1] = sortKey[left];
324 mimeTypes[left + 1] = mimeTypes[left];
Ben Kwab8a5e082015-12-07 13:25:27 -0800325 ids.set(left + 1, ids.get(left));
326 break;
327 default:
328 System.arraycopy(positions, left, positions, left + 1, n);
Ben Kwa6280de02015-12-16 19:42:08 -0800329 System.arraycopy(sortKey, left, sortKey, left + 1, n);
330 System.arraycopy(mimeTypes, left, mimeTypes, left + 1, n);
Ben Kwab8a5e082015-12-07 13:25:27 -0800331 for (int i = n; i >= 1; --i) {
332 ids.set(left + i, ids.get(left + i - 1));
333 }
334 }
335
336 positions[left] = pivotPosition;
Ben Kwa6280de02015-12-16 19:42:08 -0800337 sortKey[left] = pivotValue;
338 mimeTypes[left] = pivotMime;
Ben Kwab8a5e082015-12-07 13:25:27 -0800339 ids.set(left, pivotId);
Ben Kwa0497da82015-11-30 23:00:02 -0800340 }
341 }
342
343 @Nullable Cursor getItem(String modelId) {
344 Integer pos = mPositions.get(modelId);
345 if (pos != null) {
346 mCursor.moveToPosition(pos);
347 return mCursor;
348 }
349 return null;
350 }
351
Ben Kwa0497da82015-11-30 23:00:02 -0800352 boolean isEmpty() {
353 return mCursorCount == 0;
354 }
355
356 boolean isLoading() {
357 return mIsLoading;
358 }
359
360 List<DocumentInfo> getDocuments(Selection items) {
361 final int size = (items != null) ? items.size() : 0;
362
363 final List<DocumentInfo> docs = new ArrayList<>(size);
Ben Kwad72a1da2015-12-01 19:56:57 -0800364 for (String modelId: items.getAll()) {
365 final Cursor cursor = getItem(modelId);
Ben Kwa0497da82015-11-30 23:00:02 -0800366 checkNotNull(cursor, "Cursor cannot be null.");
367 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
368 docs.add(doc);
369 }
370 return docs;
371 }
372
373 @Override
374 public Cursor getCursor() {
375 if (Looper.myLooper() != Looper.getMainLooper()) {
376 throw new IllegalStateException("Can't call getCursor from non-main thread.");
377 }
378 return mCursor;
379 }
380
Ben Kwada858bf2015-12-09 14:33:49 -0800381 public void delete(Selection selected, DeletionListener listener) {
382 final ContentResolver resolver = mContext.getContentResolver();
383 new DeleteFilesTask(resolver, listener).execute(selected);
Ben Kwa0497da82015-11-30 23:00:02 -0800384 }
385
386 /**
387 * A Task which collects the DocumentInfo for documents that have been marked for deletion,
388 * and actually deletes them.
389 */
Ben Kwada858bf2015-12-09 14:33:49 -0800390 private class DeleteFilesTask extends AsyncTask<Selection, Void, Void> {
Ben Kwa0497da82015-11-30 23:00:02 -0800391 private ContentResolver mResolver;
392 private DeletionListener mListener;
Ben Kwada858bf2015-12-09 14:33:49 -0800393 private boolean mHadTrouble;
Ben Kwa0497da82015-11-30 23:00:02 -0800394
395 /**
396 * @param resolver A ContentResolver for performing the actual file deletions.
397 * @param errorCallback A Runnable that is executed in the event that one or more errors
Ben Kwada858bf2015-12-09 14:33:49 -0800398 * occurred while copying files. Execution will occur on the UI thread.
Ben Kwa0497da82015-11-30 23:00:02 -0800399 */
400 public DeleteFilesTask(ContentResolver resolver, DeletionListener listener) {
401 mResolver = resolver;
402 mListener = listener;
403 }
404
405 @Override
Ben Kwada858bf2015-12-09 14:33:49 -0800406 protected Void doInBackground(Selection... selected) {
407 List<DocumentInfo> toDelete = getDocuments(selected[0]);
408 mHadTrouble = false;
Ben Kwa0497da82015-11-30 23:00:02 -0800409
Ben Kwada858bf2015-12-09 14:33:49 -0800410 for (DocumentInfo doc : toDelete) {
Ben Kwa0497da82015-11-30 23:00:02 -0800411 if (!doc.isDeleteSupported()) {
412 Log.w(TAG, doc + " could not be deleted. Skipping...");
Ben Kwada858bf2015-12-09 14:33:49 -0800413 mHadTrouble = true;
Ben Kwa0497da82015-11-30 23:00:02 -0800414 continue;
415 }
416
417 ContentProviderClient client = null;
418 try {
419 if (DEBUG) Log.d(TAG, "Deleting: " + doc.displayName);
420 client = DocumentsApplication.acquireUnstableProviderOrThrow(
421 mResolver, doc.derivedUri.getAuthority());
422 DocumentsContract.deleteDocument(client, doc.derivedUri);
423 } catch (Exception e) {
Ben Kwada858bf2015-12-09 14:33:49 -0800424 Log.w(TAG, "Failed to delete " + doc, e);
425 mHadTrouble = true;
Ben Kwa0497da82015-11-30 23:00:02 -0800426 } finally {
427 ContentProviderClient.releaseQuietly(client);
428 }
429 }
430
Ben Kwada858bf2015-12-09 14:33:49 -0800431 return null;
432 }
433
434 @Override
435 protected void onPostExecute(Void _) {
436 if (mHadTrouble) {
Ben Kwa0497da82015-11-30 23:00:02 -0800437 // TODO show which files failed? b/23720103
438 mListener.onError();
439 if (DEBUG) Log.d(TAG, "Deletion task completed. Some deletions failed.");
440 } else {
441 if (DEBUG) Log.d(TAG, "Deletion task completed successfully.");
442 }
Ben Kwa0497da82015-11-30 23:00:02 -0800443
444 mListener.onCompletion();
445 }
446 }
447
448 static class DeletionListener {
449 /**
450 * Called when deletion has completed (regardless of whether an error occurred).
451 */
452 void onCompletion() {}
453
454 /**
455 * Called at the end of a deletion operation that produced one or more errors.
456 */
457 void onError() {}
458 }
459
460 void addUpdateListener(UpdateListener listener) {
Ben Kwad72a1da2015-12-01 19:56:57 -0800461 mUpdateListeners.add(listener);
Ben Kwa0497da82015-11-30 23:00:02 -0800462 }
463
Ben Kwad72a1da2015-12-01 19:56:57 -0800464 static interface UpdateListener {
Ben Kwa0497da82015-11-30 23:00:02 -0800465 /**
466 * Called when a successful update has occurred.
467 */
Ben Kwad72a1da2015-12-01 19:56:57 -0800468 void onModelUpdate(Model model);
Ben Kwa0497da82015-11-30 23:00:02 -0800469
470 /**
471 * Called when an update has been attempted but failed.
472 */
Ben Kwad72a1da2015-12-01 19:56:57 -0800473 void onModelUpdateFailed(Exception e);
Ben Kwa0497da82015-11-30 23:00:02 -0800474 }
Ben Kwab8a5e082015-12-07 13:25:27 -0800475
476 /**
477 * @return An ordered array of model IDs representing the documents in the model. It is sorted
478 * according to the current sort order, which was set by the last model update.
479 */
480 public List<String> getModelIds() {
481 return mIds;
482 }
Ben Kwa0497da82015-11-30 23:00:02 -0800483}