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