1 /* 2 * Copyright (C) 2016 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.sorting; 18 19 import static com.android.documentsui.base.Shared.DEBUG; 20 21 import android.annotation.IntDef; 22 import android.annotation.Nullable; 23 import android.content.ContentResolver; 24 import android.database.Cursor; 25 import android.os.Bundle; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.provider.DocumentsContract.Document; 29 import android.support.annotation.VisibleForTesting; 30 import android.util.Log; 31 import android.util.SparseArray; 32 import android.view.View; 33 34 import com.android.documentsui.R; 35 import com.android.documentsui.base.Lookup; 36 import com.android.documentsui.sorting.SortDimension.SortDirection; 37 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.util.ArrayList; 41 import java.util.Collection; 42 import java.util.List; 43 import java.util.function.Consumer; 44 45 /** 46 * Sort model that contains all columns and their sorting state. 47 */ 48 public class SortModel implements Parcelable { 49 @IntDef({ 50 SORT_DIMENSION_ID_UNKNOWN, 51 SORT_DIMENSION_ID_TITLE, 52 SORT_DIMENSION_ID_SUMMARY, 53 SORT_DIMENSION_ID_SIZE, 54 SORT_DIMENSION_ID_FILE_TYPE, 55 SORT_DIMENSION_ID_DATE 56 }) 57 @Retention(RetentionPolicy.SOURCE) 58 public @interface SortDimensionId {} 59 public static final int SORT_DIMENSION_ID_UNKNOWN = 0; 60 public static final int SORT_DIMENSION_ID_TITLE = android.R.id.title; 61 public static final int SORT_DIMENSION_ID_SUMMARY = android.R.id.summary; 62 public static final int SORT_DIMENSION_ID_SIZE = R.id.size; 63 public static final int SORT_DIMENSION_ID_FILE_TYPE = R.id.file_type; 64 public static final int SORT_DIMENSION_ID_DATE = R.id.date; 65 66 @IntDef(flag = true, value = { 67 UPDATE_TYPE_NONE, 68 UPDATE_TYPE_UNSPECIFIED, 69 UPDATE_TYPE_VISIBILITY, 70 UPDATE_TYPE_SORTING 71 }) 72 @Retention(RetentionPolicy.SOURCE) 73 public @interface UpdateType {} 74 /** 75 * Default value for update type. Nothing is updated. 76 */ 77 public static final int UPDATE_TYPE_NONE = 0; 78 /** 79 * Indicates the visibility of at least one dimension has changed. 80 */ 81 public static final int UPDATE_TYPE_VISIBILITY = 1; 82 /** 83 * Indicates the sorting order has changed, either because the sorted dimension has changed or 84 * the sort direction has changed. 85 */ 86 public static final int UPDATE_TYPE_SORTING = 1 << 1; 87 /** 88 * Anything can be changed if the type is unspecified. 89 */ 90 public static final int UPDATE_TYPE_UNSPECIFIED = -1; 91 92 private static final String TAG = "SortModel"; 93 94 private final SparseArray<SortDimension> mDimensions; 95 96 private transient final List<UpdateListener> mListeners; 97 private transient Consumer<SortDimension> mMetricRecorder; 98 99 private int mDefaultDimensionId = SORT_DIMENSION_ID_UNKNOWN; 100 private boolean mIsUserSpecified = false; 101 private @Nullable SortDimension mSortedDimension; 102 103 @VisibleForTesting 104 SortModel(Collection<SortDimension> columns) { 105 mDimensions = new SparseArray<>(columns.size()); 106 107 for (SortDimension column : columns) { 108 if (column.getId() == SORT_DIMENSION_ID_UNKNOWN) { 109 throw new IllegalArgumentException( 110 "SortDimension id can't be " + SORT_DIMENSION_ID_UNKNOWN + "."); 111 } 112 if (mDimensions.get(column.getId()) != null) { 113 throw new IllegalStateException( 114 "SortDimension id must be unique. Duplicate id: " + column.getId()); 115 } 116 mDimensions.put(column.getId(), column); 117 } 118 119 mListeners = new ArrayList<>(); 120 } 121 122 public int getSize() { 123 return mDimensions.size(); 124 } 125 126 public SortDimension getDimensionAt(int index) { 127 return mDimensions.valueAt(index); 128 } 129 130 public @Nullable SortDimension getDimensionById(int id) { 131 return mDimensions.get(id); 132 } 133 134 /** 135 * Gets the sorted dimension id. 136 * @return the sorted dimension id or {@link #SORT_DIMENSION_ID_UNKNOWN} if there is no sorted 137 * dimension. 138 */ 139 public int getSortedDimensionId() { 140 return mSortedDimension != null ? mSortedDimension.getId() : SORT_DIMENSION_ID_UNKNOWN; 141 } 142 143 public @SortDirection int getCurrentSortDirection() { 144 return mSortedDimension != null 145 ? mSortedDimension.getSortDirection() 146 : SortDimension.SORT_DIRECTION_NONE; 147 } 148 149 /** 150 * Sort by the default direction of the given dimension if user has never specified any sort 151 * direction before. 152 * @param dimensionId the id of the dimension 153 */ 154 public void setDefaultDimension(int dimensionId) { 155 final boolean mayNeedSorting = (mDefaultDimensionId != dimensionId); 156 157 mDefaultDimensionId = dimensionId; 158 159 if (mayNeedSorting) { 160 sortOnDefault(); 161 } 162 } 163 164 void setMetricRecorder(Consumer<SortDimension> metricRecorder) { 165 mMetricRecorder = metricRecorder; 166 } 167 168 /** 169 * Sort by given dimension and direction. Should only be used when user explicitly asks to sort 170 * docs. 171 * @param dimensionId the id of the dimension 172 * @param direction the direction to sort docs in 173 */ 174 public void sortByUser(int dimensionId, @SortDirection int direction) { 175 SortDimension dimension = mDimensions.get(dimensionId); 176 if (dimension == null) { 177 throw new IllegalArgumentException("Unknown column id: " + dimensionId); 178 } 179 180 sortByDimension(dimension, direction); 181 182 if (mMetricRecorder != null) { 183 mMetricRecorder.accept(dimension); 184 } 185 186 mIsUserSpecified = true; 187 } 188 189 private void sortByDimension( 190 SortDimension newSortedDimension, @SortDirection int direction) { 191 if (newSortedDimension == mSortedDimension 192 && mSortedDimension.mSortDirection == direction) { 193 // Sort direction not changed, no need to proceed. 194 return; 195 } 196 197 if ((newSortedDimension.getSortCapability() & direction) == 0) { 198 throw new IllegalStateException( 199 "Dimension with id: " + newSortedDimension.getId() 200 + " can't be sorted in direction:" + direction); 201 } 202 203 switch (direction) { 204 case SortDimension.SORT_DIRECTION_ASCENDING: 205 case SortDimension.SORT_DIRECTION_DESCENDING: 206 newSortedDimension.mSortDirection = direction; 207 break; 208 default: 209 throw new IllegalArgumentException("Unknown sort direction: " + direction); 210 } 211 212 if (mSortedDimension != null && mSortedDimension != newSortedDimension) { 213 mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE; 214 } 215 216 mSortedDimension = newSortedDimension; 217 218 notifyListeners(UPDATE_TYPE_SORTING); 219 } 220 221 public void setDimensionVisibility(int columnId, int visibility) { 222 assert(mDimensions.get(columnId) != null); 223 224 mDimensions.get(columnId).mVisibility = visibility; 225 226 notifyListeners(UPDATE_TYPE_VISIBILITY); 227 } 228 229 public Cursor sortCursor(Cursor cursor, Lookup<String, String> fileTypesMap) { 230 if (mSortedDimension != null) { 231 return new SortingCursorWrapper(cursor, mSortedDimension, fileTypesMap); 232 } else { 233 return cursor; 234 } 235 } 236 237 public void addQuerySortArgs(Bundle queryArgs) { 238 // should only be called when R.bool.feature_content_paging is true 239 240 final int id = getSortedDimensionId(); 241 switch (id) { 242 case SORT_DIMENSION_ID_UNKNOWN: 243 return; 244 case SortModel.SORT_DIMENSION_ID_TITLE: 245 queryArgs.putStringArray( 246 ContentResolver.QUERY_ARG_SORT_COLUMNS, 247 new String[]{ Document.COLUMN_DISPLAY_NAME }); 248 break; 249 case SortModel.SORT_DIMENSION_ID_DATE: 250 queryArgs.putStringArray( 251 ContentResolver.QUERY_ARG_SORT_COLUMNS, 252 new String[]{ Document.COLUMN_LAST_MODIFIED }); 253 break; 254 case SortModel.SORT_DIMENSION_ID_SIZE: 255 queryArgs.putStringArray( 256 ContentResolver.QUERY_ARG_SORT_COLUMNS, 257 new String[]{ Document.COLUMN_SIZE }); 258 break; 259 case SortModel.SORT_DIMENSION_ID_FILE_TYPE: 260 // Unfortunately sorting by mime type is pretty much guaranteed different from 261 // sorting by user-friendly type, so there is no point to guide the provider to sort 262 // in a particular order. 263 return; 264 default: 265 throw new IllegalStateException( 266 "Unexpected sort dimension id: " + id); 267 } 268 269 final SortDimension dimension = getDimensionById(id); 270 switch (dimension.getSortDirection()) { 271 case SortDimension.SORT_DIRECTION_ASCENDING: 272 queryArgs.putInt( 273 ContentResolver.QUERY_ARG_SORT_DIRECTION, 274 ContentResolver.QUERY_SORT_DIRECTION_ASCENDING); 275 break; 276 case SortDimension.SORT_DIRECTION_DESCENDING: 277 queryArgs.putInt( 278 ContentResolver.QUERY_ARG_SORT_DIRECTION, 279 ContentResolver.QUERY_SORT_DIRECTION_DESCENDING); 280 break; 281 default: 282 throw new IllegalStateException( 283 "Unexpected sort direction: " + dimension.getSortDirection()); 284 } 285 } 286 287 public @Nullable String getDocumentSortQuery() { 288 // This method should only be called when R.bool.feature_content_paging exists. 289 // Once that feature is enabled by default (and reference removed), this method 290 // should also be removed. 291 // The following log message exists simply to make reference to 292 // R.bool.feature_content_paging so that compiler will fail when value 293 // is remove from config.xml. 294 int readTheCommentAbove = R.bool.feature_content_paging; 295 296 final int id = getSortedDimensionId(); 297 final String columnName; 298 switch (id) { 299 case SORT_DIMENSION_ID_UNKNOWN: 300 return null; 301 case SortModel.SORT_DIMENSION_ID_TITLE: 302 columnName = Document.COLUMN_DISPLAY_NAME; 303 break; 304 case SortModel.SORT_DIMENSION_ID_DATE: 305 columnName = Document.COLUMN_LAST_MODIFIED; 306 break; 307 case SortModel.SORT_DIMENSION_ID_SIZE: 308 columnName = Document.COLUMN_SIZE; 309 break; 310 case SortModel.SORT_DIMENSION_ID_FILE_TYPE: 311 // Unfortunately sorting by mime type is pretty much guaranteed different from 312 // sorting by user-friendly type, so there is no point to guide the provider to sort 313 // in a particular order. 314 return null; 315 default: 316 throw new IllegalStateException( 317 "Unexpected sort dimension id: " + id); 318 } 319 320 final SortDimension dimension = getDimensionById(id); 321 final String direction; 322 switch (dimension.getSortDirection()) { 323 case SortDimension.SORT_DIRECTION_ASCENDING: 324 direction = " ASC"; 325 break; 326 case SortDimension.SORT_DIRECTION_DESCENDING: 327 direction = " DESC"; 328 break; 329 default: 330 throw new IllegalStateException( 331 "Unexpected sort direction: " + dimension.getSortDirection()); 332 } 333 334 return columnName + direction; 335 } 336 337 private void notifyListeners(@UpdateType int updateType) { 338 for (int i = mListeners.size() - 1; i >= 0; --i) { 339 mListeners.get(i).onModelUpdate(this, updateType); 340 } 341 } 342 343 public void addListener(UpdateListener listener) { 344 mListeners.add(listener); 345 } 346 347 public void removeListener(UpdateListener listener) { 348 mListeners.remove(listener); 349 } 350 351 /** 352 * Sort by default dimension and direction if there is no history of user specifying a sort 353 * order. 354 */ 355 private void sortOnDefault() { 356 if (!mIsUserSpecified) { 357 SortDimension dimension = mDimensions.get(mDefaultDimensionId); 358 if (dimension == null) { 359 if (DEBUG) Log.d(TAG, "No default sort dimension."); 360 return; 361 } 362 363 sortByDimension(dimension, dimension.getDefaultSortDirection()); 364 } 365 } 366 367 @Override 368 public boolean equals(Object o) { 369 if (o == null || !(o instanceof SortModel)) { 370 return false; 371 } 372 373 if (this == o) { 374 return true; 375 } 376 377 SortModel other = (SortModel) o; 378 if (mDimensions.size() != other.mDimensions.size()) { 379 return false; 380 } 381 for (int i = 0; i < mDimensions.size(); ++i) { 382 final SortDimension dimension = mDimensions.valueAt(i); 383 final int id = dimension.getId(); 384 if (!dimension.equals(other.getDimensionById(id))) { 385 return false; 386 } 387 } 388 389 return mDefaultDimensionId == other.mDefaultDimensionId 390 && (mSortedDimension == other.mSortedDimension 391 || mSortedDimension.equals(other.mSortedDimension)); 392 } 393 394 @Override 395 public String toString() { 396 return new StringBuilder() 397 .append("SortModel{") 398 .append("dimensions=").append(mDimensions) 399 .append(", defaultDimensionId=").append(mDefaultDimensionId) 400 .append(", sortedDimension=").append(mSortedDimension) 401 .append("}") 402 .toString(); 403 } 404 405 @Override 406 public int describeContents() { 407 return 0; 408 } 409 410 @Override 411 public void writeToParcel(Parcel out, int flag) { 412 out.writeInt(mDimensions.size()); 413 for (int i = 0; i < mDimensions.size(); ++i) { 414 out.writeParcelable(mDimensions.valueAt(i), flag); 415 } 416 417 out.writeInt(mDefaultDimensionId); 418 out.writeInt(getSortedDimensionId()); 419 } 420 421 public static Parcelable.Creator<SortModel> CREATOR = new Parcelable.Creator<SortModel>() { 422 423 @Override 424 public SortModel createFromParcel(Parcel in) { 425 final int size = in.readInt(); 426 Collection<SortDimension> columns = new ArrayList<>(size); 427 for (int i = 0; i < size; ++i) { 428 columns.add(in.readParcelable(getClass().getClassLoader())); 429 } 430 SortModel model = new SortModel(columns); 431 432 model.mDefaultDimensionId = in.readInt(); 433 model.mSortedDimension = model.getDimensionById(in.readInt()); 434 435 return model; 436 } 437 438 @Override 439 public SortModel[] newArray(int size) { 440 return new SortModel[size]; 441 } 442 }; 443 444 /** 445 * Creates a model for all other roots. 446 * 447 * TODO: move definition of columns into xml, and inflate model from it. 448 */ 449 public static SortModel createModel() { 450 List<SortDimension> dimensions = new ArrayList<>(4); 451 SortDimension.Builder builder = new SortDimension.Builder(); 452 453 // Name column 454 dimensions.add(builder 455 .withId(SORT_DIMENSION_ID_TITLE) 456 .withLabelId(R.string.sort_dimension_name) 457 .withDataType(SortDimension.DATA_TYPE_STRING) 458 .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION) 459 .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING) 460 .withVisibility(View.VISIBLE) 461 .build() 462 ); 463 464 // Summary column 465 // Summary is only visible in Downloads and Recents root. 466 dimensions.add(builder 467 .withId(SORT_DIMENSION_ID_SUMMARY) 468 .withLabelId(R.string.sort_dimension_summary) 469 .withDataType(SortDimension.DATA_TYPE_STRING) 470 .withSortCapability(SortDimension.SORT_CAPABILITY_NONE) 471 .withVisibility(View.INVISIBLE) 472 .build() 473 ); 474 475 // Size column 476 dimensions.add(builder 477 .withId(SORT_DIMENSION_ID_SIZE) 478 .withLabelId(R.string.sort_dimension_size) 479 .withDataType(SortDimension.DATA_TYPE_NUMBER) 480 .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION) 481 .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING) 482 .withVisibility(View.VISIBLE) 483 .build() 484 ); 485 486 // Type column 487 dimensions.add(builder 488 .withId(SORT_DIMENSION_ID_FILE_TYPE) 489 .withLabelId(R.string.sort_dimension_file_type) 490 .withDataType(SortDimension.DATA_TYPE_STRING) 491 .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION) 492 .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING) 493 .withVisibility(View.VISIBLE) 494 .build()); 495 496 // Date column 497 dimensions.add(builder 498 .withId(SORT_DIMENSION_ID_DATE) 499 .withLabelId(R.string.sort_dimension_date) 500 .withDataType(SortDimension.DATA_TYPE_NUMBER) 501 .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION) 502 .withDefaultSortDirection(SortDimension.SORT_DIRECTION_DESCENDING) 503 .withVisibility(View.VISIBLE) 504 .build() 505 ); 506 507 return new SortModel(dimensions); 508 } 509 510 public interface UpdateListener { 511 void onModelUpdate(SortModel newModel, @UpdateType int updateType); 512 } 513 } 514