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