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