Home | History | Annotate | Download | only in sorting
      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.SharedMinimal.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
    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 
    122     public int getSize() {
    123         return mDimensions.size();
    124     }
    125 
    126     public SortDimension getDimensionAt(int index) {
    127         return mDimensions.valueAt(index);
    128     }
    129 
    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      */
    139     public int getSortedDimensionId() {
    140         return mSortedDimension != null ? mSortedDimension.getId() : SORT_DIMENSION_ID_UNKNOWN;
    141     }
    142 
    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      */
    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 
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    343     public void addListener(UpdateListener listener) {
    344         mListeners.add(listener);
    345     }
    346 
    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      */
    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
    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
    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
    406     public int describeContents() {
    407         return 0;
    408     }
    409 
    410     @Override
    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      */
    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 {
    511         void onModelUpdate(SortModel newModel, @UpdateType int updateType);
    512     }
    513 }
    514