blob: f64bc286a615ea0ffaa1a9b234b8ed106a5e7d2d [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;
Garfield Tan2010ff72016-09-30 14:55:32 -070023import android.database.Cursor;
Garfield, Tan171e6f52016-07-29 14:44:58 -070024import android.os.Parcel;
25import android.os.Parcelable;
Garfield, Tan11d23482016-08-05 09:33:29 -070026import android.provider.DocumentsContract.Document;
27import android.util.Log;
Garfield, Tan171e6f52016-07-29 14:44:58 -070028import android.util.SparseArray;
29import android.view.View;
30
31import com.android.documentsui.R;
Garfield, Tan61f564b2016-08-16 13:36:15 -070032import com.android.documentsui.sorting.SortDimension.SortDirection;
Garfield, Tan171e6f52016-07-29 14:44:58 -070033
34import java.lang.annotation.Retention;
35import java.lang.annotation.RetentionPolicy;
36import java.util.ArrayList;
37import java.util.Collection;
38import java.util.List;
Garfield, Tan11d23482016-08-05 09:33:29 -070039import java.util.function.Consumer;
Garfield, Tan171e6f52016-07-29 14:44:58 -070040
41/**
42 * Sort model that contains all columns and their sorting state.
43 */
44public class SortModel implements Parcelable {
45 @IntDef({
Garfield, Tan11d23482016-08-05 09:33:29 -070046 SORT_DIMENSION_ID_UNKNOWN,
Garfield, Tan171e6f52016-07-29 14:44:58 -070047 SORT_DIMENSION_ID_TITLE,
48 SORT_DIMENSION_ID_SUMMARY,
49 SORT_DIMENSION_ID_DATE,
50 SORT_DIMENSION_ID_SIZE
51 })
52 @Retention(RetentionPolicy.SOURCE)
53 public @interface SortDimensionId {}
Garfield, Tan11d23482016-08-05 09:33:29 -070054 public static final int SORT_DIMENSION_ID_UNKNOWN = 0;
Garfield, Tan171e6f52016-07-29 14:44:58 -070055 public static final int SORT_DIMENSION_ID_TITLE = android.R.id.title;
56 public static final int SORT_DIMENSION_ID_SUMMARY = android.R.id.summary;
57 public static final int SORT_DIMENSION_ID_SIZE = R.id.size;
58 public static final int SORT_DIMENSION_ID_DATE = R.id.date;
59
Garfield, Tan61f564b2016-08-16 13:36:15 -070060 @IntDef(flag = true, value = {
61 UPDATE_TYPE_NONE,
Garfield, Tan11d23482016-08-05 09:33:29 -070062 UPDATE_TYPE_UNSPECIFIED,
63 UPDATE_TYPE_STATUS,
64 UPDATE_TYPE_VISIBILITY,
65 UPDATE_TYPE_SORTING
66 })
67 @Retention(RetentionPolicy.SOURCE)
68 public @interface UpdateType {}
69 /**
Garfield, Tan61f564b2016-08-16 13:36:15 -070070 * Default value for update type. Nothing is updated.
Garfield, Tan11d23482016-08-05 09:33:29 -070071 */
Garfield, Tan61f564b2016-08-16 13:36:15 -070072 public static final int UPDATE_TYPE_NONE = 0;
Garfield, Tan11d23482016-08-05 09:33:29 -070073 /**
74 * Indicates the status of sorting has changed, i.e. whether soring is enabled.
75 */
76 public static final int UPDATE_TYPE_STATUS = 1;
77 /**
78 * Indicates the visibility of at least one dimension has changed.
79 */
Garfield, Tan61f564b2016-08-16 13:36:15 -070080 public static final int UPDATE_TYPE_VISIBILITY = 1 << 1;
Garfield, Tan11d23482016-08-05 09:33:29 -070081 /**
82 * Indicates the sorting order has changed, either because the sorted dimension has changed or
83 * the sort direction has changed.
84 */
Garfield, Tan61f564b2016-08-16 13:36:15 -070085 public static final int UPDATE_TYPE_SORTING = 1 << 2;
86 /**
87 * Anything can be changed if the type is unspecified.
88 */
89 public static final int UPDATE_TYPE_UNSPECIFIED = -1;
Garfield, Tan11d23482016-08-05 09:33:29 -070090
91 private static final String TAG = "SortModel";
92
Garfield, Tan171e6f52016-07-29 14:44:58 -070093 private final SparseArray<SortDimension> mDimensions;
94
95 private transient final List<UpdateListener> mListeners;
Garfield, Tan11d23482016-08-05 09:33:29 -070096 private transient Consumer<SortDimension> mMetricRecorder;
Garfield, Tan171e6f52016-07-29 14:44:58 -070097
Garfield, Tan11d23482016-08-05 09:33:29 -070098 private int mDefaultDimensionId = SORT_DIMENSION_ID_UNKNOWN;
99 private boolean mIsUserSpecified = false;
100 private @Nullable SortDimension mSortedDimension;
Garfield, Tan171e6f52016-07-29 14:44:58 -0700101
102 private boolean mIsSortEnabled = true;
103
104 public SortModel(Collection<SortDimension> columns) {
105 mDimensions = new SparseArray<>(columns.size());
106
107 for (SortDimension column : columns) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700108 if (column.getId() == SORT_DIMENSION_ID_UNKNOWN) {
109 throw new IllegalArgumentException(
110 "SortDimension id can't be " + SORT_DIMENSION_ID_UNKNOWN + ".");
111 }
Garfield, Tan171e6f52016-07-29 14:44:58 -0700112 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
Garfield, Tan11d23482016-08-05 09:33:29 -0700130 public @Nullable SortDimension getDimensionById(int id) {
Garfield, Tan171e6f52016-07-29 14:44:58 -0700131 return mDimensions.get(id);
132 }
133
Garfield, Tan11d23482016-08-05 09:33:29 -0700134 /**
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;
Garfield, Tan171e6f52016-07-29 14:44:58 -0700141 }
142
Garfield, Tan61f564b2016-08-16 13:36:15 -0700143 public @SortDirection int getCurrentSortDirection() {
144 return mSortedDimension != null
145 ? mSortedDimension.getSortDirection()
146 : SortDimension.SORT_DIRECTION_NONE;
147 }
148
Garfield, Tan171e6f52016-07-29 14:44:58 -0700149 public void setSortEnabled(boolean enabled) {
Garfield, Tan171e6f52016-07-29 14:44:58 -0700150 mIsSortEnabled = enabled;
151
Garfield, Tan11d23482016-08-05 09:33:29 -0700152 notifyListeners(UPDATE_TYPE_STATUS);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700153 }
154
155 public boolean isSortEnabled() {
156 return mIsSortEnabled;
157 }
158
Garfield, Tan11d23482016-08-05 09:33:29 -0700159 /**
160 * Sort by the default direction of the given dimension if user has never specified any sort
161 * direction before.
162 * @param dimensionId the id of the dimension
163 */
164 public void setDefaultDimension(int dimensionId) {
165 final boolean mayNeedSorting = (mDefaultDimensionId != dimensionId);
166
167 mDefaultDimensionId = dimensionId;
168
169 if (mayNeedSorting) {
170 sortOnDefault();
171 }
172 }
173
174 void setMetricRecorder(Consumer<SortDimension> metricRecorder) {
175 mMetricRecorder = metricRecorder;
176 }
177
178 /**
179 * Sort by given dimension and direction. Should only be used when user explicitly asks to sort
180 * docs.
181 * @param dimensionId the id of the dimension
182 * @param direction the direction to sort docs in
183 */
Garfield, Tan61f564b2016-08-16 13:36:15 -0700184 public void sortByUser(int dimensionId, @SortDirection int direction) {
Garfield, Tan171e6f52016-07-29 14:44:58 -0700185 if (!mIsSortEnabled) {
186 throw new IllegalStateException("Sort is not enabled.");
187 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700188
189 SortDimension dimension = mDimensions.get(dimensionId);
190 if (dimension == null) {
191 throw new IllegalArgumentException("Unknown column id: " + dimensionId);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700192 }
193
Garfield, Tan11d23482016-08-05 09:33:29 -0700194 sortByDimension(dimension, direction);
195
196 if (mMetricRecorder != null) {
197 mMetricRecorder.accept(dimension);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700198 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700199
200 mIsUserSpecified = true;
201 }
202
203 private void sortByDimension(
Garfield, Tan61f564b2016-08-16 13:36:15 -0700204 SortDimension newSortedDimension, @SortDirection int direction) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700205 if (newSortedDimension == mSortedDimension
206 && mSortedDimension.mSortDirection == direction) {
207 // Sort direction not changed, no need to proceed.
208 return;
209 }
210
211 if ((newSortedDimension.getSortCapability() & direction) == 0) {
212 throw new IllegalStateException(
213 "Dimension with id: " + newSortedDimension.getId()
214 + " can't be sorted in direction:" + direction);
215 }
216
Garfield, Tan171e6f52016-07-29 14:44:58 -0700217 switch (direction) {
218 case SortDimension.SORT_DIRECTION_ASCENDING:
219 case SortDimension.SORT_DIRECTION_DESCENDING:
220 newSortedDimension.mSortDirection = direction;
221 break;
222 default:
223 throw new IllegalArgumentException("Unknown sort direction: " + direction);
224 }
225
226 if (mSortedDimension != null && mSortedDimension != newSortedDimension) {
227 mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
228 }
229
230 mSortedDimension = newSortedDimension;
231
Garfield, Tan11d23482016-08-05 09:33:29 -0700232 notifyListeners(UPDATE_TYPE_SORTING);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700233 }
234
235 public void setDimensionVisibility(int columnId, int visibility) {
236 assert(mDimensions.get(columnId) != null);
237
238 mDimensions.get(columnId).mVisibility = visibility;
239
Garfield, Tan11d23482016-08-05 09:33:29 -0700240 notifyListeners(UPDATE_TYPE_VISIBILITY);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700241 }
242
Garfield Tan2010ff72016-09-30 14:55:32 -0700243 public Cursor sortCursor(Cursor cursor) {
244 if (mSortedDimension != null) {
245 return new SortingCursorWrapper(cursor, mSortedDimension);
246 } else {
247 return cursor;
248 }
249 }
250
Garfield, Tan11d23482016-08-05 09:33:29 -0700251 public @Nullable String getDocumentSortQuery() {
252 final int id = getSortedDimensionId();
253 final String columnName;
254 switch (id) {
Garfield, Tan61f564b2016-08-16 13:36:15 -0700255 case SORT_DIMENSION_ID_UNKNOWN:
Garfield, Tan11d23482016-08-05 09:33:29 -0700256 return null;
257 case SortModel.SORT_DIMENSION_ID_TITLE:
258 columnName = Document.COLUMN_DISPLAY_NAME;
259 break;
260 case SortModel.SORT_DIMENSION_ID_DATE:
261 columnName = Document.COLUMN_LAST_MODIFIED;
262 break;
263 case SortModel.SORT_DIMENSION_ID_SIZE:
264 columnName = Document.COLUMN_SIZE;
265 break;
266 default:
267 throw new IllegalStateException(
268 "Unexpected sort dimension id: " + id);
269 }
270
271 final SortDimension dimension = getDimensionById(id);
272 final String direction;
273 switch (dimension.getSortDirection()) {
274 case SortDimension.SORT_DIRECTION_ASCENDING:
275 direction = " ASC";
276 break;
277 case SortDimension.SORT_DIRECTION_DESCENDING:
278 direction = " DESC";
279 break;
280 default:
281 throw new IllegalStateException(
282 "Unexpected sort direction: " + dimension.getSortDirection());
283 }
284
285 return columnName + direction;
286 }
287
288 private void notifyListeners(@UpdateType int updateType) {
Garfield, Tan171e6f52016-07-29 14:44:58 -0700289 for (int i = mListeners.size() - 1; i >= 0; --i) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700290 mListeners.get(i).onModelUpdate(this, updateType);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700291 }
292 }
293
294 public void addListener(UpdateListener listener) {
295 mListeners.add(listener);
296 }
297
298 public void removeListener(UpdateListener listener) {
299 mListeners.remove(listener);
300 }
301
302 public void clearSortDirection() {
303 if (mSortedDimension != null) {
304 mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
305 mSortedDimension = null;
306 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700307
308 mIsUserSpecified = false;
309
310 sortOnDefault();
311 }
312
313 /**
314 * Sort by default dimension and direction if there is no history of user specifying a sort
315 * order.
316 */
317 private void sortOnDefault() {
318 if (!mIsUserSpecified) {
319 SortDimension dimension = mDimensions.get(mDefaultDimensionId);
320 if (dimension == null) {
321 if (DEBUG) Log.d(TAG, "No default sort dimension.");
322 return;
323 }
324
325 sortByDimension(dimension, dimension.getDefaultSortDirection());
326 }
327 }
328
329 @Override
330 public boolean equals(Object o) {
331 if (o == null || !(o instanceof SortModel)) {
332 return false;
333 }
334
335 if (this == o) {
336 return true;
337 }
338
339 SortModel other = (SortModel) o;
340 if (mDimensions.size() != other.mDimensions.size()) {
341 return false;
342 }
343 for (int i = 0; i < mDimensions.size(); ++i) {
344 final SortDimension dimension = mDimensions.valueAt(i);
345 final int id = dimension.getId();
346 if (!dimension.equals(other.getDimensionById(id))) {
347 return false;
348 }
349 }
350
351 return mDefaultDimensionId == other.mDefaultDimensionId
352 && mIsSortEnabled == other.mIsSortEnabled
353 && (mSortedDimension == other.mSortedDimension
354 || mSortedDimension.equals(other.mSortedDimension));
355 }
356
357 @Override
358 public String toString() {
359 return new StringBuilder()
360 .append("SortModel{")
361 .append("enabled=").append(mIsSortEnabled)
362 .append(", dimensions=").append(mDimensions)
363 .append(", defaultDimensionId=").append(mDefaultDimensionId)
364 .append(", sortedDimension=").append(mSortedDimension)
365 .append("}")
366 .toString();
Garfield, Tan171e6f52016-07-29 14:44:58 -0700367 }
368
369 @Override
370 public int describeContents() {
371 return 0;
372 }
373
374 @Override
375 public void writeToParcel(Parcel out, int flag) {
376 out.writeInt(mDimensions.size());
377 for (int i = 0; i < mDimensions.size(); ++i) {
378 out.writeParcelable(mDimensions.valueAt(i), flag);
379 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700380
381 out.writeInt(mDefaultDimensionId);
382 out.writeInt(mIsSortEnabled ? 1 : 0);
Garfield, Tan61f564b2016-08-16 13:36:15 -0700383 out.writeInt(getSortedDimensionId());
Garfield, Tan171e6f52016-07-29 14:44:58 -0700384 }
385
386 public static Parcelable.Creator<SortModel> CREATOR = new Parcelable.Creator<SortModel>() {
387
388 @Override
389 public SortModel createFromParcel(Parcel in) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700390 final int size = in.readInt();
Garfield, Tan171e6f52016-07-29 14:44:58 -0700391 Collection<SortDimension> columns = new ArrayList<>(size);
392 for (int i = 0; i < size; ++i) {
393 columns.add(in.readParcelable(getClass().getClassLoader()));
394 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700395 SortModel model = new SortModel(columns);
396
397 model.mDefaultDimensionId = in.readInt();
398 model.mIsSortEnabled = (in.readInt() == 1);
399 model.mSortedDimension = model.getDimensionById(in.readInt());
400
401 return model;
Garfield, Tan171e6f52016-07-29 14:44:58 -0700402 }
403
404 @Override
405 public SortModel[] newArray(int size) {
406 return new SortModel[size];
407 }
408 };
409
410 /**
411 * Creates a model for all other roots.
412 *
413 * TODO: move definition of columns into xml, and inflate model from it.
414 */
415 public static SortModel createModel() {
416 List<SortDimension> dimensions = new ArrayList<>(4);
417 SortDimension.Builder builder = new SortDimension.Builder();
418
419 // Name column
420 dimensions.add(builder
421 .withId(SORT_DIMENSION_ID_TITLE)
Garfield, Tan11d23482016-08-05 09:33:29 -0700422 .withLabelId(R.string.sort_dimension_name)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700423 .withDataType(SortDimension.DATA_TYPE_STRING)
424 .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
425 .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
426 .withVisibility(View.VISIBLE)
427 .build()
428 );
429
430 // Summary column
431 // Summary is only visible in Downloads and Recents root.
432 dimensions.add(builder
433 .withId(SORT_DIMENSION_ID_SUMMARY)
Garfield, Tan11d23482016-08-05 09:33:29 -0700434 .withLabelId(R.string.sort_dimension_summary)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700435 .withDataType(SortDimension.DATA_TYPE_STRING)
436 .withSortCapability(SortDimension.SORT_CAPABILITY_NONE)
437 .withVisibility(View.INVISIBLE)
438 .build()
439 );
440
441 // Size column
442 dimensions.add(builder
443 .withId(SORT_DIMENSION_ID_SIZE)
Garfield, Tan11d23482016-08-05 09:33:29 -0700444 .withLabelId(R.string.sort_dimension_size)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700445 .withDataType(SortDimension.DATA_TYPE_NUMBER)
446 .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
447 .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
448 .withVisibility(View.VISIBLE)
449 .build()
450 );
451
452 // Date column
453 dimensions.add(builder
454 .withId(SORT_DIMENSION_ID_DATE)
Garfield, Tan11d23482016-08-05 09:33:29 -0700455 .withLabelId(R.string.sort_dimension_date)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700456 .withDataType(SortDimension.DATA_TYPE_NUMBER)
457 .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
458 .withDefaultSortDirection(SortDimension.SORT_DIRECTION_DESCENDING)
459 .withVisibility(View.VISIBLE)
460 .build()
461 );
462
463 return new SortModel(dimensions);
464 }
465
466 public interface UpdateListener {
Garfield, Tan11d23482016-08-05 09:33:29 -0700467 void onModelUpdate(SortModel newModel, @UpdateType int updateType);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700468 }
469}