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