blob: 311e89f8ad8f0b272cd8c003b552e409b32927a0 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui.sorting;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsContract.Document;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import com.android.documentsui.R;
import com.android.documentsui.base.Lookup;
import com.android.documentsui.sorting.SortDimension.SortDirection;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
/**
* Sort model that contains all columns and their sorting state.
*/
public class SortModel implements Parcelable {
@IntDef({
SORT_DIMENSION_ID_UNKNOWN,
SORT_DIMENSION_ID_TITLE,
SORT_DIMENSION_ID_SUMMARY,
SORT_DIMENSION_ID_SIZE,
SORT_DIMENSION_ID_FILE_TYPE,
SORT_DIMENSION_ID_DATE
})
@Retention(RetentionPolicy.SOURCE)
public @interface SortDimensionId {}
public static final int SORT_DIMENSION_ID_UNKNOWN = 0;
public static final int SORT_DIMENSION_ID_TITLE = android.R.id.title;
public static final int SORT_DIMENSION_ID_SUMMARY = android.R.id.summary;
public static final int SORT_DIMENSION_ID_SIZE = R.id.size;
public static final int SORT_DIMENSION_ID_FILE_TYPE = R.id.file_type;
public static final int SORT_DIMENSION_ID_DATE = R.id.date;
@IntDef(flag = true, value = {
UPDATE_TYPE_NONE,
UPDATE_TYPE_UNSPECIFIED,
UPDATE_TYPE_VISIBILITY,
UPDATE_TYPE_SORTING
})
@Retention(RetentionPolicy.SOURCE)
public @interface UpdateType {}
/**
* Default value for update type. Nothing is updated.
*/
public static final int UPDATE_TYPE_NONE = 0;
/**
* Indicates the visibility of at least one dimension has changed.
*/
public static final int UPDATE_TYPE_VISIBILITY = 1;
/**
* Indicates the sorting order has changed, either because the sorted dimension has changed or
* the sort direction has changed.
*/
public static final int UPDATE_TYPE_SORTING = 1 << 1;
/**
* Anything can be changed if the type is unspecified.
*/
public static final int UPDATE_TYPE_UNSPECIFIED = -1;
private static final String TAG = "SortModel";
private final SparseArray<SortDimension> mDimensions;
private transient final List<UpdateListener> mListeners;
private transient Consumer<SortDimension> mMetricRecorder;
private int mDefaultDimensionId = SORT_DIMENSION_ID_UNKNOWN;
private boolean mIsUserSpecified = false;
private @Nullable SortDimension mSortedDimension;
@VisibleForTesting
SortModel(Collection<SortDimension> columns) {
mDimensions = new SparseArray<>(columns.size());
for (SortDimension column : columns) {
if (column.getId() == SORT_DIMENSION_ID_UNKNOWN) {
throw new IllegalArgumentException(
"SortDimension id can't be " + SORT_DIMENSION_ID_UNKNOWN + ".");
}
if (mDimensions.get(column.getId()) != null) {
throw new IllegalStateException(
"SortDimension id must be unique. Duplicate id: " + column.getId());
}
mDimensions.put(column.getId(), column);
}
mListeners = new ArrayList<>();
}
public int getSize() {
return mDimensions.size();
}
public SortDimension getDimensionAt(int index) {
return mDimensions.valueAt(index);
}
public @Nullable SortDimension getDimensionById(int id) {
return mDimensions.get(id);
}
/**
* Gets the sorted dimension id.
* @return the sorted dimension id or {@link #SORT_DIMENSION_ID_UNKNOWN} if there is no sorted
* dimension.
*/
public int getSortedDimensionId() {
return mSortedDimension != null ? mSortedDimension.getId() : SORT_DIMENSION_ID_UNKNOWN;
}
public @SortDirection int getCurrentSortDirection() {
return mSortedDimension != null
? mSortedDimension.getSortDirection()
: SortDimension.SORT_DIRECTION_NONE;
}
/**
* Sort by the default direction of the given dimension if user has never specified any sort
* direction before.
* @param dimensionId the id of the dimension
*/
public void setDefaultDimension(int dimensionId) {
final boolean mayNeedSorting = (mDefaultDimensionId != dimensionId);
mDefaultDimensionId = dimensionId;
if (mayNeedSorting) {
sortOnDefault();
}
}
void setMetricRecorder(Consumer<SortDimension> metricRecorder) {
mMetricRecorder = metricRecorder;
}
/**
* Sort by given dimension and direction. Should only be used when user explicitly asks to sort
* docs.
* @param dimensionId the id of the dimension
* @param direction the direction to sort docs in
*/
public void sortByUser(int dimensionId, @SortDirection int direction) {
SortDimension dimension = mDimensions.get(dimensionId);
if (dimension == null) {
throw new IllegalArgumentException("Unknown column id: " + dimensionId);
}
sortByDimension(dimension, direction);
if (mMetricRecorder != null) {
mMetricRecorder.accept(dimension);
}
mIsUserSpecified = true;
}
private void sortByDimension(
SortDimension newSortedDimension, @SortDirection int direction) {
if (newSortedDimension == mSortedDimension
&& mSortedDimension.mSortDirection == direction) {
// Sort direction not changed, no need to proceed.
return;
}
if ((newSortedDimension.getSortCapability() & direction) == 0) {
throw new IllegalStateException(
"Dimension with id: " + newSortedDimension.getId()
+ " can't be sorted in direction:" + direction);
}
switch (direction) {
case SortDimension.SORT_DIRECTION_ASCENDING:
case SortDimension.SORT_DIRECTION_DESCENDING:
newSortedDimension.mSortDirection = direction;
break;
default:
throw new IllegalArgumentException("Unknown sort direction: " + direction);
}
if (mSortedDimension != null && mSortedDimension != newSortedDimension) {
mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
}
mSortedDimension = newSortedDimension;
notifyListeners(UPDATE_TYPE_SORTING);
}
public void setDimensionVisibility(int columnId, int visibility) {
assert(mDimensions.get(columnId) != null);
mDimensions.get(columnId).mVisibility = visibility;
notifyListeners(UPDATE_TYPE_VISIBILITY);
}
public Cursor sortCursor(Cursor cursor, Lookup<String, String> fileTypesMap) {
if (mSortedDimension != null) {
return new SortingCursorWrapper(cursor, mSortedDimension, fileTypesMap);
} else {
return cursor;
}
}
public void addQuerySortArgs(Bundle queryArgs) {
// should only be called when R.bool.feature_content_paging is true
final int id = getSortedDimensionId();
switch (id) {
case SORT_DIMENSION_ID_UNKNOWN:
return;
case SortModel.SORT_DIMENSION_ID_TITLE:
queryArgs.putStringArray(
ContentResolver.QUERY_ARG_SORT_COLUMNS,
new String[]{ Document.COLUMN_DISPLAY_NAME });
break;
case SortModel.SORT_DIMENSION_ID_DATE:
queryArgs.putStringArray(
ContentResolver.QUERY_ARG_SORT_COLUMNS,
new String[]{ Document.COLUMN_LAST_MODIFIED });
break;
case SortModel.SORT_DIMENSION_ID_SIZE:
queryArgs.putStringArray(
ContentResolver.QUERY_ARG_SORT_COLUMNS,
new String[]{ Document.COLUMN_SIZE });
break;
case SortModel.SORT_DIMENSION_ID_FILE_TYPE:
// Unfortunately sorting by mime type is pretty much guaranteed different from
// sorting by user-friendly type, so there is no point to guide the provider to sort
// in a particular order.
return;
default:
throw new IllegalStateException(
"Unexpected sort dimension id: " + id);
}
final SortDimension dimension = getDimensionById(id);
switch (dimension.getSortDirection()) {
case SortDimension.SORT_DIRECTION_ASCENDING:
queryArgs.putInt(
ContentResolver.QUERY_ARG_SORT_DIRECTION,
ContentResolver.QUERY_SORT_DIRECTION_ASCENDING);
break;
case SortDimension.SORT_DIRECTION_DESCENDING:
queryArgs.putInt(
ContentResolver.QUERY_ARG_SORT_DIRECTION,
ContentResolver.QUERY_SORT_DIRECTION_DESCENDING);
break;
default:
throw new IllegalStateException(
"Unexpected sort direction: " + dimension.getSortDirection());
}
}
public @Nullable String getDocumentSortQuery() {
// This method should only be called when R.bool.feature_content_paging exists.
// Once that feature is enabled by default (and reference removed), this method
// should also be removed.
// The following log message exists simply to make reference to
// R.bool.feature_content_paging so that compiler will fail when value
// is remove from config.xml.
int readTheCommentAbove = R.bool.feature_content_paging;
final int id = getSortedDimensionId();
final String columnName;
switch (id) {
case SORT_DIMENSION_ID_UNKNOWN:
return null;
case SortModel.SORT_DIMENSION_ID_TITLE:
columnName = Document.COLUMN_DISPLAY_NAME;
break;
case SortModel.SORT_DIMENSION_ID_DATE:
columnName = Document.COLUMN_LAST_MODIFIED;
break;
case SortModel.SORT_DIMENSION_ID_SIZE:
columnName = Document.COLUMN_SIZE;
break;
case SortModel.SORT_DIMENSION_ID_FILE_TYPE:
// Unfortunately sorting by mime type is pretty much guaranteed different from
// sorting by user-friendly type, so there is no point to guide the provider to sort
// in a particular order.
return null;
default:
throw new IllegalStateException(
"Unexpected sort dimension id: " + id);
}
final SortDimension dimension = getDimensionById(id);
final String direction;
switch (dimension.getSortDirection()) {
case SortDimension.SORT_DIRECTION_ASCENDING:
direction = " ASC";
break;
case SortDimension.SORT_DIRECTION_DESCENDING:
direction = " DESC";
break;
default:
throw new IllegalStateException(
"Unexpected sort direction: " + dimension.getSortDirection());
}
return columnName + direction;
}
private void notifyListeners(@UpdateType int updateType) {
for (int i = mListeners.size() - 1; i >= 0; --i) {
mListeners.get(i).onModelUpdate(this, updateType);
}
}
public void addListener(UpdateListener listener) {
mListeners.add(listener);
}
public void removeListener(UpdateListener listener) {
mListeners.remove(listener);
}
/**
* Sort by default dimension and direction if there is no history of user specifying a sort
* order.
*/
private void sortOnDefault() {
if (!mIsUserSpecified) {
SortDimension dimension = mDimensions.get(mDefaultDimensionId);
if (dimension == null) {
if (DEBUG) Log.d(TAG, "No default sort dimension.");
return;
}
sortByDimension(dimension, dimension.getDefaultSortDirection());
}
}
@Override
public boolean equals(Object o) {
if (o == null || !(o instanceof SortModel)) {
return false;
}
if (this == o) {
return true;
}
SortModel other = (SortModel) o;
if (mDimensions.size() != other.mDimensions.size()) {
return false;
}
for (int i = 0; i < mDimensions.size(); ++i) {
final SortDimension dimension = mDimensions.valueAt(i);
final int id = dimension.getId();
if (!dimension.equals(other.getDimensionById(id))) {
return false;
}
}
return mDefaultDimensionId == other.mDefaultDimensionId
&& (mSortedDimension == other.mSortedDimension
|| mSortedDimension.equals(other.mSortedDimension));
}
@Override
public String toString() {
return new StringBuilder()
.append("SortModel{")
.append("dimensions=").append(mDimensions)
.append(", defaultDimensionId=").append(mDefaultDimensionId)
.append(", sortedDimension=").append(mSortedDimension)
.append("}")
.toString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flag) {
out.writeInt(mDimensions.size());
for (int i = 0; i < mDimensions.size(); ++i) {
out.writeParcelable(mDimensions.valueAt(i), flag);
}
out.writeInt(mDefaultDimensionId);
out.writeInt(getSortedDimensionId());
}
public static Parcelable.Creator<SortModel> CREATOR = new Parcelable.Creator<SortModel>() {
@Override
public SortModel createFromParcel(Parcel in) {
final int size = in.readInt();
Collection<SortDimension> columns = new ArrayList<>(size);
for (int i = 0; i < size; ++i) {
columns.add(in.readParcelable(getClass().getClassLoader()));
}
SortModel model = new SortModel(columns);
model.mDefaultDimensionId = in.readInt();
model.mSortedDimension = model.getDimensionById(in.readInt());
return model;
}
@Override
public SortModel[] newArray(int size) {
return new SortModel[size];
}
};
/**
* Creates a model for all other roots.
*
* TODO: move definition of columns into xml, and inflate model from it.
*/
public static SortModel createModel() {
List<SortDimension> dimensions = new ArrayList<>(4);
SortDimension.Builder builder = new SortDimension.Builder();
// Name column
dimensions.add(builder
.withId(SORT_DIMENSION_ID_TITLE)
.withLabelId(R.string.sort_dimension_name)
.withDataType(SortDimension.DATA_TYPE_STRING)
.withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
.withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
.withVisibility(View.VISIBLE)
.build()
);
// Summary column
// Summary is only visible in Downloads and Recents root.
dimensions.add(builder
.withId(SORT_DIMENSION_ID_SUMMARY)
.withLabelId(R.string.sort_dimension_summary)
.withDataType(SortDimension.DATA_TYPE_STRING)
.withSortCapability(SortDimension.SORT_CAPABILITY_NONE)
.withVisibility(View.INVISIBLE)
.build()
);
// Size column
dimensions.add(builder
.withId(SORT_DIMENSION_ID_SIZE)
.withLabelId(R.string.sort_dimension_size)
.withDataType(SortDimension.DATA_TYPE_NUMBER)
.withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
.withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
.withVisibility(View.VISIBLE)
.build()
);
// Type column
dimensions.add(builder
.withId(SORT_DIMENSION_ID_FILE_TYPE)
.withLabelId(R.string.sort_dimension_file_type)
.withDataType(SortDimension.DATA_TYPE_STRING)
.withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
.withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
.withVisibility(View.VISIBLE)
.build());
// Date column
dimensions.add(builder
.withId(SORT_DIMENSION_ID_DATE)
.withLabelId(R.string.sort_dimension_date)
.withDataType(SortDimension.DATA_TYPE_NUMBER)
.withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
.withDefaultSortDirection(SortDimension.SORT_DIRECTION_DESCENDING)
.withVisibility(View.VISIBLE)
.build()
);
return new SortModel(dimensions);
}
public interface UpdateListener {
void onModelUpdate(SortModel newModel, @UpdateType int updateType);
}
}