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.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.util.Log;
     30 import android.util.SparseArray;
     31 import android.view.View;
     32 
     33 import com.android.documentsui.R;
     34 import com.android.documentsui.sorting.SortDimension.SortDirection;
     35 
     36 import java.lang.annotation.Retention;
     37 import java.lang.annotation.RetentionPolicy;
     38 import java.util.ArrayList;
     39 import java.util.Collection;
     40 import java.util.List;
     41 import java.util.function.Consumer;
     42 
     43 /**
     44  * Sort model that contains all columns and their sorting state.
     45  */
     46 public class SortModel implements Parcelable {
     47     @IntDef({
     48             SORT_DIMENSION_ID_UNKNOWN,
     49             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 {}
     56     public static final int SORT_DIMENSION_ID_UNKNOWN = 0;
     57     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 
     62     @IntDef(flag = true, value = {
     63             UPDATE_TYPE_NONE,
     64             UPDATE_TYPE_UNSPECIFIED,
     65             UPDATE_TYPE_VISIBILITY,
     66             UPDATE_TYPE_SORTING
     67     })
     68     @Retention(RetentionPolicy.SOURCE)
     69     public @interface UpdateType {}
     70     /**
     71      * Default value for update type. Nothing is updated.
     72      */
     73     public static final int UPDATE_TYPE_NONE = 0;
     74     /**
     75      * Indicates the visibility of at least one dimension has changed.
     76      */
     77     public static final int UPDATE_TYPE_VISIBILITY = 1;
     78     /**
     79      * Indicates the sorting order has changed, either because the sorted dimension has changed or
     80      * the sort direction has changed.
     81      */
     82     public static final int UPDATE_TYPE_SORTING = 1 << 1;
     83     /**
     84      * Anything can be changed if the type is unspecified.
     85      */
     86     public static final int UPDATE_TYPE_UNSPECIFIED = -1;
     87 
     88     private static final String TAG = "SortModel";
     89 
     90     private final SparseArray<SortDimension> mDimensions;
     91 
     92     private transient final List<UpdateListener> mListeners;
     93     private transient Consumer<SortDimension> mMetricRecorder;
     94 
     95     private int mDefaultDimensionId = SORT_DIMENSION_ID_UNKNOWN;
     96     private boolean mIsUserSpecified = false;
     97     private @Nullable SortDimension mSortedDimension;
     98 
     99     public SortModel(Collection<SortDimension> columns) {
    100         mDimensions = new SparseArray<>(columns.size());
    101 
    102         for (SortDimension column : columns) {
    103             if (column.getId() == SORT_DIMENSION_ID_UNKNOWN) {
    104                 throw new IllegalArgumentException(
    105                         "SortDimension id can't be " + SORT_DIMENSION_ID_UNKNOWN + ".");
    106             }
    107             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 
    125     public @Nullable SortDimension getDimensionById(int id) {
    126         return mDimensions.get(id);
    127     }
    128 
    129     /**
    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;
    136     }
    137 
    138     public @SortDirection int getCurrentSortDirection() {
    139         return mSortedDimension != null
    140                 ? mSortedDimension.getSortDirection()
    141                 : SortDimension.SORT_DIRECTION_NONE;
    142     }
    143 
    144     /**
    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      */
    169     public void sortByUser(int dimensionId, @SortDirection int direction) {
    170         SortDimension dimension = mDimensions.get(dimensionId);
    171         if (dimension == null) {
    172             throw new IllegalArgumentException("Unknown column id: " + dimensionId);
    173         }
    174 
    175         sortByDimension(dimension, direction);
    176 
    177         if (mMetricRecorder != null) {
    178             mMetricRecorder.accept(dimension);
    179         }
    180 
    181         mIsUserSpecified = true;
    182     }
    183 
    184     private void sortByDimension(
    185             SortDimension newSortedDimension, @SortDirection int direction) {
    186         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 
    198         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 
    213         notifyListeners(UPDATE_TYPE_SORTING);
    214     }
    215 
    216     public void setDimensionVisibility(int columnId, int visibility) {
    217         assert(mDimensions.get(columnId) != null);
    218 
    219         mDimensions.get(columnId).mVisibility = visibility;
    220 
    221         notifyListeners(UPDATE_TYPE_VISIBILITY);
    222     }
    223 
    224     public Cursor sortCursor(Cursor cursor) {
    225         if (mSortedDimension != null) {
    226             return new SortingCursorWrapper(cursor, mSortedDimension);
    227         } else {
    228             return cursor;
    229         }
    230     }
    231 
    232     public void addQuerySortArgs(Bundle queryArgs) {
    233         // should only be called when R.bool.feature_content_paging is true
    234 
    235         final int id = getSortedDimensionId();
    236         switch (id) {
    237             case SORT_DIMENSION_ID_UNKNOWN:
    238                 return;
    239             case SortModel.SORT_DIMENSION_ID_TITLE:
    240                 queryArgs.putStringArray(
    241                         ContentResolver.QUERY_ARG_SORT_COLUMNS,
    242                         new String[]{ Document.COLUMN_DISPLAY_NAME });
    243                 break;
    244             case SortModel.SORT_DIMENSION_ID_DATE:
    245                 queryArgs.putStringArray(
    246                         ContentResolver.QUERY_ARG_SORT_COLUMNS,
    247                         new String[]{ Document.COLUMN_LAST_MODIFIED });
    248                 break;
    249             case SortModel.SORT_DIMENSION_ID_SIZE:
    250                 queryArgs.putStringArray(
    251                         ContentResolver.QUERY_ARG_SORT_COLUMNS,
    252                         new String[]{ Document.COLUMN_SIZE });
    253                 break;
    254             default:
    255                 throw new IllegalStateException(
    256                         "Unexpected sort dimension id: " + id);
    257         }
    258 
    259         final SortDimension dimension = getDimensionById(id);
    260         switch (dimension.getSortDirection()) {
    261             case SortDimension.SORT_DIRECTION_ASCENDING:
    262                 queryArgs.putInt(
    263                         ContentResolver.QUERY_ARG_SORT_DIRECTION,
    264                         ContentResolver.QUERY_SORT_DIRECTION_ASCENDING);
    265                 break;
    266             case SortDimension.SORT_DIRECTION_DESCENDING:
    267                 queryArgs.putInt(
    268                         ContentResolver.QUERY_ARG_SORT_DIRECTION,
    269                         ContentResolver.QUERY_SORT_DIRECTION_DESCENDING);
    270                 break;
    271             default:
    272                 throw new IllegalStateException(
    273                         "Unexpected sort direction: " + dimension.getSortDirection());
    274         }
    275     }
    276 
    277     public @Nullable String getDocumentSortQuery() {
    278         // 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.
    284         int readTheCommentAbove = R.bool.feature_content_paging;
    285 
    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 
    322     private void notifyListeners(@UpdateType int updateType) {
    323         for (int i = mListeners.size() - 1; i >= 0; --i) {
    324             mListeners.get(i).onModelUpdate(this, updateType);
    325         }
    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 
    336     /**
    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
    375                 && (mSortedDimension == other.mSortedDimension
    376                     || mSortedDimension.equals(other.mSortedDimension));
    377     }
    378 
    379     @Override
    380     public String toString() {
    381         return new StringBuilder()
    382                 .append("SortModel{")
    383                 .append("dimensions=").append(mDimensions)
    384                 .append(", defaultDimensionId=").append(mDefaultDimensionId)
    385                 .append(", sortedDimension=").append(mSortedDimension)
    386                 .append("}")
    387                 .toString();
    388     }
    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         }
    401 
    402         out.writeInt(mDefaultDimensionId);
    403         out.writeInt(getSortedDimensionId());
    404     }
    405 
    406     public static Parcelable.Creator<SortModel> CREATOR = new Parcelable.Creator<SortModel>() {
    407 
    408         @Override
    409         public SortModel createFromParcel(Parcel in) {
    410             final int size = in.readInt();
    411             Collection<SortDimension> columns = new ArrayList<>(size);
    412             for (int i = 0; i < size; ++i) {
    413                 columns.add(in.readParcelable(getClass().getClassLoader()));
    414             }
    415             SortModel model = new SortModel(columns);
    416 
    417             model.mDefaultDimensionId = in.readInt();
    418             model.mSortedDimension = model.getDimensionById(in.readInt());
    419 
    420             return model;
    421         }
    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)
    441                 .withLabelId(R.string.sort_dimension_name)
    442                 .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)
    453                 .withLabelId(R.string.sort_dimension_summary)
    454                 .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)
    463                 .withLabelId(R.string.sort_dimension_size)
    464                 .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)
    474                 .withLabelId(R.string.sort_dimension_date)
    475                 .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 {
    486         void onModelUpdate(SortModel newModel, @UpdateType int updateType);
    487     }
    488 }
    489