blob: 05833f7f418d59940625320397a56572550a2f6a [file] [log] [blame]
Garfield, Tan171e6f52016-07-29 14:44:58 -07001/*
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
17package com.android.documentsui.sorting;
18
Steve McKayd9caa6a2016-09-15 16:36:45 -070019import static com.android.documentsui.base.Shared.DEBUG;
Garfield, Tan11d23482016-08-05 09:33:29 -070020
Garfield, Tan171e6f52016-07-29 14:44:58 -070021import android.annotation.IntDef;
Garfield, Tan11d23482016-08-05 09:33:29 -070022import android.annotation.Nullable;
Steve McKaya4b68e52016-12-16 14:21:56 -080023import android.content.ContentResolver;
Garfield Tan2010ff72016-09-30 14:55:32 -070024import android.database.Cursor;
Steve McKay50b9bae2017-01-17 11:12:08 -080025import android.os.Bundle;
Garfield, Tan171e6f52016-07-29 14:44:58 -070026import android.os.Parcel;
27import android.os.Parcelable;
Garfield, Tan11d23482016-08-05 09:33:29 -070028import android.provider.DocumentsContract.Document;
29import android.util.Log;
Garfield, Tan171e6f52016-07-29 14:44:58 -070030import android.util.SparseArray;
31import android.view.View;
32
33import com.android.documentsui.R;
Garfield, Tan61f564b2016-08-16 13:36:15 -070034import com.android.documentsui.sorting.SortDimension.SortDirection;
Garfield, Tan171e6f52016-07-29 14:44:58 -070035
36import java.lang.annotation.Retention;
37import java.lang.annotation.RetentionPolicy;
38import java.util.ArrayList;
39import java.util.Collection;
40import java.util.List;
Garfield, Tan11d23482016-08-05 09:33:29 -070041import java.util.function.Consumer;
Garfield, Tan171e6f52016-07-29 14:44:58 -070042
43/**
44 * Sort model that contains all columns and their sorting state.
45 */
46public class SortModel implements Parcelable {
47 @IntDef({
Garfield, Tan11d23482016-08-05 09:33:29 -070048 SORT_DIMENSION_ID_UNKNOWN,
Garfield, Tan171e6f52016-07-29 14:44:58 -070049 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 {}
Garfield, Tan11d23482016-08-05 09:33:29 -070056 public static final int SORT_DIMENSION_ID_UNKNOWN = 0;
Garfield, Tan171e6f52016-07-29 14:44:58 -070057 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
Garfield, Tan61f564b2016-08-16 13:36:15 -070062 @IntDef(flag = true, value = {
63 UPDATE_TYPE_NONE,
Garfield, Tan11d23482016-08-05 09:33:29 -070064 UPDATE_TYPE_UNSPECIFIED,
Garfield, Tan11d23482016-08-05 09:33:29 -070065 UPDATE_TYPE_VISIBILITY,
66 UPDATE_TYPE_SORTING
67 })
68 @Retention(RetentionPolicy.SOURCE)
69 public @interface UpdateType {}
70 /**
Garfield, Tan61f564b2016-08-16 13:36:15 -070071 * Default value for update type. Nothing is updated.
Garfield, Tan11d23482016-08-05 09:33:29 -070072 */
Garfield, Tan61f564b2016-08-16 13:36:15 -070073 public static final int UPDATE_TYPE_NONE = 0;
Garfield, Tan11d23482016-08-05 09:33:29 -070074 /**
Garfield, Tan11d23482016-08-05 09:33:29 -070075 * Indicates the visibility of at least one dimension has changed.
76 */
Garfield Tanfe199cb2016-10-03 11:46:10 -070077 public static final int UPDATE_TYPE_VISIBILITY = 1;
Garfield, Tan11d23482016-08-05 09:33:29 -070078 /**
79 * Indicates the sorting order has changed, either because the sorted dimension has changed or
80 * the sort direction has changed.
81 */
Garfield Tanfe199cb2016-10-03 11:46:10 -070082 public static final int UPDATE_TYPE_SORTING = 1 << 1;
Garfield, Tan61f564b2016-08-16 13:36:15 -070083 /**
84 * Anything can be changed if the type is unspecified.
85 */
86 public static final int UPDATE_TYPE_UNSPECIFIED = -1;
Garfield, Tan11d23482016-08-05 09:33:29 -070087
88 private static final String TAG = "SortModel";
89
Garfield, Tan171e6f52016-07-29 14:44:58 -070090 private final SparseArray<SortDimension> mDimensions;
91
92 private transient final List<UpdateListener> mListeners;
Garfield, Tan11d23482016-08-05 09:33:29 -070093 private transient Consumer<SortDimension> mMetricRecorder;
Garfield, Tan171e6f52016-07-29 14:44:58 -070094
Garfield, Tan11d23482016-08-05 09:33:29 -070095 private int mDefaultDimensionId = SORT_DIMENSION_ID_UNKNOWN;
96 private boolean mIsUserSpecified = false;
97 private @Nullable SortDimension mSortedDimension;
Garfield, Tan171e6f52016-07-29 14:44:58 -070098
Garfield, Tan171e6f52016-07-29 14:44:58 -070099 public SortModel(Collection<SortDimension> columns) {
100 mDimensions = new SparseArray<>(columns.size());
101
102 for (SortDimension column : columns) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700103 if (column.getId() == SORT_DIMENSION_ID_UNKNOWN) {
104 throw new IllegalArgumentException(
105 "SortDimension id can't be " + SORT_DIMENSION_ID_UNKNOWN + ".");
106 }
Garfield, Tan171e6f52016-07-29 14:44:58 -0700107 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
117 public int getSize() {
118 return mDimensions.size();
119 }
120
121 public SortDimension getDimensionAt(int index) {
122 return mDimensions.valueAt(index);
123 }
124
Garfield, Tan11d23482016-08-05 09:33:29 -0700125 public @Nullable SortDimension getDimensionById(int id) {
Garfield, Tan171e6f52016-07-29 14:44:58 -0700126 return mDimensions.get(id);
127 }
128
Garfield, Tan11d23482016-08-05 09:33:29 -0700129 /**
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 */
134 public int getSortedDimensionId() {
135 return mSortedDimension != null ? mSortedDimension.getId() : SORT_DIMENSION_ID_UNKNOWN;
Garfield, Tan171e6f52016-07-29 14:44:58 -0700136 }
137
Garfield, Tan61f564b2016-08-16 13:36:15 -0700138 public @SortDirection int getCurrentSortDirection() {
139 return mSortedDimension != null
140 ? mSortedDimension.getSortDirection()
141 : SortDimension.SORT_DIRECTION_NONE;
142 }
143
Garfield, Tan11d23482016-08-05 09:33:29 -0700144 /**
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 */
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
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 */
Garfield, Tan61f564b2016-08-16 13:36:15 -0700169 public void sortByUser(int dimensionId, @SortDirection int direction) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700170 SortDimension dimension = mDimensions.get(dimensionId);
171 if (dimension == null) {
172 throw new IllegalArgumentException("Unknown column id: " + dimensionId);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700173 }
174
Garfield, Tan11d23482016-08-05 09:33:29 -0700175 sortByDimension(dimension, direction);
176
177 if (mMetricRecorder != null) {
178 mMetricRecorder.accept(dimension);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700179 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700180
181 mIsUserSpecified = true;
182 }
183
184 private void sortByDimension(
Garfield, Tan61f564b2016-08-16 13:36:15 -0700185 SortDimension newSortedDimension, @SortDirection int direction) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700186 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
Garfield, Tan171e6f52016-07-29 14:44:58 -0700198 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
Garfield, Tan11d23482016-08-05 09:33:29 -0700213 notifyListeners(UPDATE_TYPE_SORTING);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700214 }
215
216 public void setDimensionVisibility(int columnId, int visibility) {
217 assert(mDimensions.get(columnId) != null);
218
219 mDimensions.get(columnId).mVisibility = visibility;
220
Garfield, Tan11d23482016-08-05 09:33:29 -0700221 notifyListeners(UPDATE_TYPE_VISIBILITY);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700222 }
223
Garfield Tan2010ff72016-09-30 14:55:32 -0700224 public Cursor sortCursor(Cursor cursor) {
225 if (mSortedDimension != null) {
226 return new SortingCursorWrapper(cursor, mSortedDimension);
227 } else {
228 return cursor;
229 }
230 }
231
Steve McKay50b9bae2017-01-17 11:12:08 -0800232 public void addQuerySortArgs(Bundle queryArgs) {
Steve McKay98f8c5f2017-03-03 13:52:14 -0800233 // should only be called when R.bool.feature_content_paging is true
Steve McKay50b9bae2017-01-17 11:12:08 -0800234
Garfield, Tan11d23482016-08-05 09:33:29 -0700235 final int id = getSortedDimensionId();
Garfield, Tan11d23482016-08-05 09:33:29 -0700236 switch (id) {
Garfield, Tan61f564b2016-08-16 13:36:15 -0700237 case SORT_DIMENSION_ID_UNKNOWN:
Steve McKay50b9bae2017-01-17 11:12:08 -0800238 return;
Garfield, Tan11d23482016-08-05 09:33:29 -0700239 case SortModel.SORT_DIMENSION_ID_TITLE:
Steve McKay50b9bae2017-01-17 11:12:08 -0800240 queryArgs.putStringArray(
241 ContentResolver.QUERY_ARG_SORT_COLUMNS,
242 new String[]{ Document.COLUMN_DISPLAY_NAME });
Garfield, Tan11d23482016-08-05 09:33:29 -0700243 break;
244 case SortModel.SORT_DIMENSION_ID_DATE:
Steve McKay50b9bae2017-01-17 11:12:08 -0800245 queryArgs.putStringArray(
246 ContentResolver.QUERY_ARG_SORT_COLUMNS,
247 new String[]{ Document.COLUMN_LAST_MODIFIED });
Garfield, Tan11d23482016-08-05 09:33:29 -0700248 break;
249 case SortModel.SORT_DIMENSION_ID_SIZE:
Steve McKay50b9bae2017-01-17 11:12:08 -0800250 queryArgs.putStringArray(
251 ContentResolver.QUERY_ARG_SORT_COLUMNS,
252 new String[]{ Document.COLUMN_SIZE });
Garfield, Tan11d23482016-08-05 09:33:29 -0700253 break;
254 default:
255 throw new IllegalStateException(
256 "Unexpected sort dimension id: " + id);
257 }
258
259 final SortDimension dimension = getDimensionById(id);
Garfield, Tan11d23482016-08-05 09:33:29 -0700260 switch (dimension.getSortDirection()) {
261 case SortDimension.SORT_DIRECTION_ASCENDING:
Steve McKay50b9bae2017-01-17 11:12:08 -0800262 queryArgs.putInt(
263 ContentResolver.QUERY_ARG_SORT_DIRECTION,
264 ContentResolver.QUERY_SORT_DIRECTION_ASCENDING);
Garfield, Tan11d23482016-08-05 09:33:29 -0700265 break;
266 case SortDimension.SORT_DIRECTION_DESCENDING:
Steve McKay50b9bae2017-01-17 11:12:08 -0800267 queryArgs.putInt(
268 ContentResolver.QUERY_ARG_SORT_DIRECTION,
269 ContentResolver.QUERY_SORT_DIRECTION_DESCENDING);
Garfield, Tan11d23482016-08-05 09:33:29 -0700270 break;
271 default:
272 throw new IllegalStateException(
273 "Unexpected sort direction: " + dimension.getSortDirection());
274 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700275 }
276
Steve McKayf208d842017-02-27 12:57:58 -0800277 public @Nullable String getDocumentSortQuery() {
Steve McKay710248d2017-03-14 10:09:30 -0700278 // 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.
Steve McKay5918a062017-03-15 18:22:54 -0700284 int readTheCommentAbove = R.bool.feature_content_paging;
Steve McKayf208d842017-02-27 12:57:58 -0800285
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
Garfield, Tan11d23482016-08-05 09:33:29 -0700322 private void notifyListeners(@UpdateType int updateType) {
Garfield, Tan171e6f52016-07-29 14:44:58 -0700323 for (int i = mListeners.size() - 1; i >= 0; --i) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700324 mListeners.get(i).onModelUpdate(this, updateType);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700325 }
326 }
327
328 public void addListener(UpdateListener listener) {
329 mListeners.add(listener);
330 }
331
332 public void removeListener(UpdateListener listener) {
333 mListeners.remove(listener);
334 }
335
Garfield, Tan11d23482016-08-05 09:33:29 -0700336 /**
337 * Sort by default dimension and direction if there is no history of user specifying a sort
338 * order.
339 */
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
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
Garfield, Tan11d23482016-08-05 09:33:29 -0700375 && (mSortedDimension == other.mSortedDimension
376 || mSortedDimension.equals(other.mSortedDimension));
377 }
378
379 @Override
380 public String toString() {
381 return new StringBuilder()
382 .append("SortModel{")
Garfield Tanfe199cb2016-10-03 11:46:10 -0700383 .append("dimensions=").append(mDimensions)
Garfield, Tan11d23482016-08-05 09:33:29 -0700384 .append(", defaultDimensionId=").append(mDefaultDimensionId)
385 .append(", sortedDimension=").append(mSortedDimension)
386 .append("}")
387 .toString();
Garfield, Tan171e6f52016-07-29 14:44:58 -0700388 }
389
390 @Override
391 public int describeContents() {
392 return 0;
393 }
394
395 @Override
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 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700401
402 out.writeInt(mDefaultDimensionId);
Garfield, Tan61f564b2016-08-16 13:36:15 -0700403 out.writeInt(getSortedDimensionId());
Garfield, Tan171e6f52016-07-29 14:44:58 -0700404 }
405
406 public static Parcelable.Creator<SortModel> CREATOR = new Parcelable.Creator<SortModel>() {
407
408 @Override
409 public SortModel createFromParcel(Parcel in) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700410 final int size = in.readInt();
Garfield, Tan171e6f52016-07-29 14:44:58 -0700411 Collection<SortDimension> columns = new ArrayList<>(size);
412 for (int i = 0; i < size; ++i) {
413 columns.add(in.readParcelable(getClass().getClassLoader()));
414 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700415 SortModel model = new SortModel(columns);
416
417 model.mDefaultDimensionId = in.readInt();
Garfield, Tan11d23482016-08-05 09:33:29 -0700418 model.mSortedDimension = model.getDimensionById(in.readInt());
419
420 return model;
Garfield, Tan171e6f52016-07-29 14:44:58 -0700421 }
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 */
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)
Garfield, Tan11d23482016-08-05 09:33:29 -0700441 .withLabelId(R.string.sort_dimension_name)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700442 .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)
Garfield, Tan11d23482016-08-05 09:33:29 -0700453 .withLabelId(R.string.sort_dimension_summary)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700454 .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)
Garfield, Tan11d23482016-08-05 09:33:29 -0700463 .withLabelId(R.string.sort_dimension_size)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700464 .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)
Garfield, Tan11d23482016-08-05 09:33:29 -0700474 .withLabelId(R.string.sort_dimension_date)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700475 .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 {
Garfield, Tan11d23482016-08-05 09:33:29 -0700486 void onModelUpdate(SortModel newModel, @UpdateType int updateType);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700487 }
488}