blob: 108be35e16581ec62c930f22642074e9c618a662 [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
Garfield, Tan11d23482016-08-05 09:33:29 -070019import static com.android.documentsui.Shared.DEBUG;
20
Garfield, Tan171e6f52016-07-29 14:44:58 -070021import android.annotation.IntDef;
Garfield, Tan11d23482016-08-05 09:33:29 -070022import android.annotation.Nullable;
Garfield, Tan171e6f52016-07-29 14:44:58 -070023import android.os.Parcel;
24import android.os.Parcelable;
Garfield, Tan11d23482016-08-05 09:33:29 -070025import android.provider.DocumentsContract.Document;
26import android.util.Log;
Garfield, Tan171e6f52016-07-29 14:44:58 -070027import android.util.SparseArray;
28import android.view.View;
29
30import com.android.documentsui.R;
Garfield, Tan61f564b2016-08-16 13:36:15 -070031import com.android.documentsui.sorting.SortDimension.SortDirection;
Garfield, Tan171e6f52016-07-29 14:44:58 -070032
33import java.lang.annotation.Retention;
34import java.lang.annotation.RetentionPolicy;
35import java.util.ArrayList;
36import java.util.Collection;
37import java.util.List;
Garfield, Tan11d23482016-08-05 09:33:29 -070038import java.util.function.Consumer;
Garfield, Tan171e6f52016-07-29 14:44:58 -070039
40/**
41 * Sort model that contains all columns and their sorting state.
42 */
43public class SortModel implements Parcelable {
44 @IntDef({
Garfield, Tan11d23482016-08-05 09:33:29 -070045 SORT_DIMENSION_ID_UNKNOWN,
Garfield, Tan171e6f52016-07-29 14:44:58 -070046 SORT_DIMENSION_ID_TITLE,
47 SORT_DIMENSION_ID_SUMMARY,
48 SORT_DIMENSION_ID_DATE,
49 SORT_DIMENSION_ID_SIZE
50 })
51 @Retention(RetentionPolicy.SOURCE)
52 public @interface SortDimensionId {}
Garfield, Tan11d23482016-08-05 09:33:29 -070053 public static final int SORT_DIMENSION_ID_UNKNOWN = 0;
Garfield, Tan171e6f52016-07-29 14:44:58 -070054 public static final int SORT_DIMENSION_ID_TITLE = android.R.id.title;
55 public static final int SORT_DIMENSION_ID_SUMMARY = android.R.id.summary;
56 public static final int SORT_DIMENSION_ID_SIZE = R.id.size;
57 public static final int SORT_DIMENSION_ID_DATE = R.id.date;
58
Garfield, Tan61f564b2016-08-16 13:36:15 -070059 @IntDef(flag = true, value = {
60 UPDATE_TYPE_NONE,
Garfield, Tan11d23482016-08-05 09:33:29 -070061 UPDATE_TYPE_UNSPECIFIED,
62 UPDATE_TYPE_STATUS,
63 UPDATE_TYPE_VISIBILITY,
64 UPDATE_TYPE_SORTING
65 })
66 @Retention(RetentionPolicy.SOURCE)
67 public @interface UpdateType {}
68 /**
Garfield, Tan61f564b2016-08-16 13:36:15 -070069 * Default value for update type. Nothing is updated.
Garfield, Tan11d23482016-08-05 09:33:29 -070070 */
Garfield, Tan61f564b2016-08-16 13:36:15 -070071 public static final int UPDATE_TYPE_NONE = 0;
Garfield, Tan11d23482016-08-05 09:33:29 -070072 /**
73 * Indicates the status of sorting has changed, i.e. whether soring is enabled.
74 */
75 public static final int UPDATE_TYPE_STATUS = 1;
76 /**
77 * Indicates the visibility of at least one dimension has changed.
78 */
Garfield, Tan61f564b2016-08-16 13:36:15 -070079 public static final int UPDATE_TYPE_VISIBILITY = 1 << 1;
Garfield, Tan11d23482016-08-05 09:33:29 -070080 /**
81 * Indicates the sorting order has changed, either because the sorted dimension has changed or
82 * the sort direction has changed.
83 */
Garfield, Tan61f564b2016-08-16 13:36:15 -070084 public static final int UPDATE_TYPE_SORTING = 1 << 2;
85 /**
86 * Anything can be changed if the type is unspecified.
87 */
88 public static final int UPDATE_TYPE_UNSPECIFIED = -1;
Garfield, Tan11d23482016-08-05 09:33:29 -070089
90 private static final String TAG = "SortModel";
91
Garfield, Tan171e6f52016-07-29 14:44:58 -070092 private final SparseArray<SortDimension> mDimensions;
93
94 private transient final List<UpdateListener> mListeners;
Garfield, Tan11d23482016-08-05 09:33:29 -070095 private transient Consumer<SortDimension> mMetricRecorder;
Garfield, Tan171e6f52016-07-29 14:44:58 -070096
Garfield, Tan11d23482016-08-05 09:33:29 -070097 private int mDefaultDimensionId = SORT_DIMENSION_ID_UNKNOWN;
98 private boolean mIsUserSpecified = false;
99 private @Nullable SortDimension mSortedDimension;
Garfield, Tan171e6f52016-07-29 14:44:58 -0700100
101 private boolean mIsSortEnabled = true;
102
103 public SortModel(Collection<SortDimension> columns) {
104 mDimensions = new SparseArray<>(columns.size());
105
106 for (SortDimension column : columns) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700107 if (column.getId() == SORT_DIMENSION_ID_UNKNOWN) {
108 throw new IllegalArgumentException(
109 "SortDimension id can't be " + SORT_DIMENSION_ID_UNKNOWN + ".");
110 }
Garfield, Tan171e6f52016-07-29 14:44:58 -0700111 if (mDimensions.get(column.getId()) != null) {
112 throw new IllegalStateException(
113 "SortDimension id must be unique. Duplicate id: " + column.getId());
114 }
115 mDimensions.put(column.getId(), column);
116 }
117
118 mListeners = new ArrayList<>();
119 }
120
121 public int getSize() {
122 return mDimensions.size();
123 }
124
125 public SortDimension getDimensionAt(int index) {
126 return mDimensions.valueAt(index);
127 }
128
Garfield, Tan11d23482016-08-05 09:33:29 -0700129 public @Nullable SortDimension getDimensionById(int id) {
Garfield, Tan171e6f52016-07-29 14:44:58 -0700130 return mDimensions.get(id);
131 }
132
Garfield, Tan11d23482016-08-05 09:33:29 -0700133 /**
134 * Gets the sorted dimension id.
135 * @return the sorted dimension id or {@link #SORT_DIMENSION_ID_UNKNOWN} if there is no sorted
136 * dimension.
137 */
138 public int getSortedDimensionId() {
139 return mSortedDimension != null ? mSortedDimension.getId() : SORT_DIMENSION_ID_UNKNOWN;
Garfield, Tan171e6f52016-07-29 14:44:58 -0700140 }
141
Garfield, Tan61f564b2016-08-16 13:36:15 -0700142 public @SortDirection int getCurrentSortDirection() {
143 return mSortedDimension != null
144 ? mSortedDimension.getSortDirection()
145 : SortDimension.SORT_DIRECTION_NONE;
146 }
147
Garfield, Tan171e6f52016-07-29 14:44:58 -0700148 public void setSortEnabled(boolean enabled) {
Garfield, Tan171e6f52016-07-29 14:44:58 -0700149 mIsSortEnabled = enabled;
150
Garfield, Tan11d23482016-08-05 09:33:29 -0700151 notifyListeners(UPDATE_TYPE_STATUS);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700152 }
153
154 public boolean isSortEnabled() {
155 return mIsSortEnabled;
156 }
157
Garfield, Tan11d23482016-08-05 09:33:29 -0700158 /**
159 * Sort by the default direction of the given dimension if user has never specified any sort
160 * direction before.
161 * @param dimensionId the id of the dimension
162 */
163 public void setDefaultDimension(int dimensionId) {
164 final boolean mayNeedSorting = (mDefaultDimensionId != dimensionId);
165
166 mDefaultDimensionId = dimensionId;
167
168 if (mayNeedSorting) {
169 sortOnDefault();
170 }
171 }
172
173 void setMetricRecorder(Consumer<SortDimension> metricRecorder) {
174 mMetricRecorder = metricRecorder;
175 }
176
177 /**
178 * Sort by given dimension and direction. Should only be used when user explicitly asks to sort
179 * docs.
180 * @param dimensionId the id of the dimension
181 * @param direction the direction to sort docs in
182 */
Garfield, Tan61f564b2016-08-16 13:36:15 -0700183 public void sortByUser(int dimensionId, @SortDirection int direction) {
Garfield, Tan171e6f52016-07-29 14:44:58 -0700184 if (!mIsSortEnabled) {
185 throw new IllegalStateException("Sort is not enabled.");
186 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700187
188 SortDimension dimension = mDimensions.get(dimensionId);
189 if (dimension == null) {
190 throw new IllegalArgumentException("Unknown column id: " + dimensionId);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700191 }
192
Garfield, Tan11d23482016-08-05 09:33:29 -0700193 sortByDimension(dimension, direction);
194
195 if (mMetricRecorder != null) {
196 mMetricRecorder.accept(dimension);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700197 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700198
199 mIsUserSpecified = true;
200 }
201
202 private void sortByDimension(
Garfield, Tan61f564b2016-08-16 13:36:15 -0700203 SortDimension newSortedDimension, @SortDirection int direction) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700204 if (newSortedDimension == mSortedDimension
205 && mSortedDimension.mSortDirection == direction) {
206 // Sort direction not changed, no need to proceed.
207 return;
208 }
209
210 if ((newSortedDimension.getSortCapability() & direction) == 0) {
211 throw new IllegalStateException(
212 "Dimension with id: " + newSortedDimension.getId()
213 + " can't be sorted in direction:" + direction);
214 }
215
Garfield, Tan171e6f52016-07-29 14:44:58 -0700216 switch (direction) {
217 case SortDimension.SORT_DIRECTION_ASCENDING:
218 case SortDimension.SORT_DIRECTION_DESCENDING:
219 newSortedDimension.mSortDirection = direction;
220 break;
221 default:
222 throw new IllegalArgumentException("Unknown sort direction: " + direction);
223 }
224
225 if (mSortedDimension != null && mSortedDimension != newSortedDimension) {
226 mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
227 }
228
229 mSortedDimension = newSortedDimension;
230
Garfield, Tan11d23482016-08-05 09:33:29 -0700231 notifyListeners(UPDATE_TYPE_SORTING);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700232 }
233
234 public void setDimensionVisibility(int columnId, int visibility) {
235 assert(mDimensions.get(columnId) != null);
236
237 mDimensions.get(columnId).mVisibility = visibility;
238
Garfield, Tan11d23482016-08-05 09:33:29 -0700239 notifyListeners(UPDATE_TYPE_VISIBILITY);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700240 }
241
Garfield, Tan11d23482016-08-05 09:33:29 -0700242 public @Nullable String getDocumentSortQuery() {
243 final int id = getSortedDimensionId();
244 final String columnName;
245 switch (id) {
Garfield, Tan61f564b2016-08-16 13:36:15 -0700246 case SORT_DIMENSION_ID_UNKNOWN:
Garfield, Tan11d23482016-08-05 09:33:29 -0700247 return null;
248 case SortModel.SORT_DIMENSION_ID_TITLE:
249 columnName = Document.COLUMN_DISPLAY_NAME;
250 break;
251 case SortModel.SORT_DIMENSION_ID_DATE:
252 columnName = Document.COLUMN_LAST_MODIFIED;
253 break;
254 case SortModel.SORT_DIMENSION_ID_SIZE:
255 columnName = Document.COLUMN_SIZE;
256 break;
257 default:
258 throw new IllegalStateException(
259 "Unexpected sort dimension id: " + id);
260 }
261
262 final SortDimension dimension = getDimensionById(id);
263 final String direction;
264 switch (dimension.getSortDirection()) {
265 case SortDimension.SORT_DIRECTION_ASCENDING:
266 direction = " ASC";
267 break;
268 case SortDimension.SORT_DIRECTION_DESCENDING:
269 direction = " DESC";
270 break;
271 default:
272 throw new IllegalStateException(
273 "Unexpected sort direction: " + dimension.getSortDirection());
274 }
275
276 return columnName + direction;
277 }
278
279 private void notifyListeners(@UpdateType int updateType) {
Garfield, Tan171e6f52016-07-29 14:44:58 -0700280 for (int i = mListeners.size() - 1; i >= 0; --i) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700281 mListeners.get(i).onModelUpdate(this, updateType);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700282 }
283 }
284
285 public void addListener(UpdateListener listener) {
286 mListeners.add(listener);
287 }
288
289 public void removeListener(UpdateListener listener) {
290 mListeners.remove(listener);
291 }
292
293 public void clearSortDirection() {
294 if (mSortedDimension != null) {
295 mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
296 mSortedDimension = null;
297 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700298
299 mIsUserSpecified = false;
300
301 sortOnDefault();
302 }
303
304 /**
305 * Sort by default dimension and direction if there is no history of user specifying a sort
306 * order.
307 */
308 private void sortOnDefault() {
309 if (!mIsUserSpecified) {
310 SortDimension dimension = mDimensions.get(mDefaultDimensionId);
311 if (dimension == null) {
312 if (DEBUG) Log.d(TAG, "No default sort dimension.");
313 return;
314 }
315
316 sortByDimension(dimension, dimension.getDefaultSortDirection());
317 }
318 }
319
320 @Override
321 public boolean equals(Object o) {
322 if (o == null || !(o instanceof SortModel)) {
323 return false;
324 }
325
326 if (this == o) {
327 return true;
328 }
329
330 SortModel other = (SortModel) o;
331 if (mDimensions.size() != other.mDimensions.size()) {
332 return false;
333 }
334 for (int i = 0; i < mDimensions.size(); ++i) {
335 final SortDimension dimension = mDimensions.valueAt(i);
336 final int id = dimension.getId();
337 if (!dimension.equals(other.getDimensionById(id))) {
338 return false;
339 }
340 }
341
342 return mDefaultDimensionId == other.mDefaultDimensionId
343 && mIsSortEnabled == other.mIsSortEnabled
344 && (mSortedDimension == other.mSortedDimension
345 || mSortedDimension.equals(other.mSortedDimension));
346 }
347
348 @Override
349 public String toString() {
350 return new StringBuilder()
351 .append("SortModel{")
352 .append("enabled=").append(mIsSortEnabled)
353 .append(", dimensions=").append(mDimensions)
354 .append(", defaultDimensionId=").append(mDefaultDimensionId)
355 .append(", sortedDimension=").append(mSortedDimension)
356 .append("}")
357 .toString();
Garfield, Tan171e6f52016-07-29 14:44:58 -0700358 }
359
360 @Override
361 public int describeContents() {
362 return 0;
363 }
364
365 @Override
366 public void writeToParcel(Parcel out, int flag) {
367 out.writeInt(mDimensions.size());
368 for (int i = 0; i < mDimensions.size(); ++i) {
369 out.writeParcelable(mDimensions.valueAt(i), flag);
370 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700371
372 out.writeInt(mDefaultDimensionId);
373 out.writeInt(mIsSortEnabled ? 1 : 0);
Garfield, Tan61f564b2016-08-16 13:36:15 -0700374 out.writeInt(getSortedDimensionId());
Garfield, Tan171e6f52016-07-29 14:44:58 -0700375 }
376
377 public static Parcelable.Creator<SortModel> CREATOR = new Parcelable.Creator<SortModel>() {
378
379 @Override
380 public SortModel createFromParcel(Parcel in) {
Garfield, Tan11d23482016-08-05 09:33:29 -0700381 final int size = in.readInt();
Garfield, Tan171e6f52016-07-29 14:44:58 -0700382 Collection<SortDimension> columns = new ArrayList<>(size);
383 for (int i = 0; i < size; ++i) {
384 columns.add(in.readParcelable(getClass().getClassLoader()));
385 }
Garfield, Tan11d23482016-08-05 09:33:29 -0700386 SortModel model = new SortModel(columns);
387
388 model.mDefaultDimensionId = in.readInt();
389 model.mIsSortEnabled = (in.readInt() == 1);
390 model.mSortedDimension = model.getDimensionById(in.readInt());
391
392 return model;
Garfield, Tan171e6f52016-07-29 14:44:58 -0700393 }
394
395 @Override
396 public SortModel[] newArray(int size) {
397 return new SortModel[size];
398 }
399 };
400
401 /**
402 * Creates a model for all other roots.
403 *
404 * TODO: move definition of columns into xml, and inflate model from it.
405 */
406 public static SortModel createModel() {
407 List<SortDimension> dimensions = new ArrayList<>(4);
408 SortDimension.Builder builder = new SortDimension.Builder();
409
410 // Name column
411 dimensions.add(builder
412 .withId(SORT_DIMENSION_ID_TITLE)
Garfield, Tan11d23482016-08-05 09:33:29 -0700413 .withLabelId(R.string.sort_dimension_name)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700414 .withDataType(SortDimension.DATA_TYPE_STRING)
415 .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
416 .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
417 .withVisibility(View.VISIBLE)
418 .build()
419 );
420
421 // Summary column
422 // Summary is only visible in Downloads and Recents root.
423 dimensions.add(builder
424 .withId(SORT_DIMENSION_ID_SUMMARY)
Garfield, Tan11d23482016-08-05 09:33:29 -0700425 .withLabelId(R.string.sort_dimension_summary)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700426 .withDataType(SortDimension.DATA_TYPE_STRING)
427 .withSortCapability(SortDimension.SORT_CAPABILITY_NONE)
428 .withVisibility(View.INVISIBLE)
429 .build()
430 );
431
432 // Size column
433 dimensions.add(builder
434 .withId(SORT_DIMENSION_ID_SIZE)
Garfield, Tan11d23482016-08-05 09:33:29 -0700435 .withLabelId(R.string.sort_dimension_size)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700436 .withDataType(SortDimension.DATA_TYPE_NUMBER)
437 .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
438 .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
439 .withVisibility(View.VISIBLE)
440 .build()
441 );
442
443 // Date column
444 dimensions.add(builder
445 .withId(SORT_DIMENSION_ID_DATE)
Garfield, Tan11d23482016-08-05 09:33:29 -0700446 .withLabelId(R.string.sort_dimension_date)
Garfield, Tan171e6f52016-07-29 14:44:58 -0700447 .withDataType(SortDimension.DATA_TYPE_NUMBER)
448 .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
449 .withDefaultSortDirection(SortDimension.SORT_DIRECTION_DESCENDING)
450 .withVisibility(View.VISIBLE)
451 .build()
452 );
453
454 return new SortModel(dimensions);
455 }
456
457 public interface UpdateListener {
Garfield, Tan11d23482016-08-05 09:33:29 -0700458 void onModelUpdate(SortModel newModel, @UpdateType int updateType);
Garfield, Tan171e6f52016-07-29 14:44:58 -0700459 }
460}