blob: 8775c583189dccf4c70a0e9ca5df6a95d17df737 [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.selection;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Object representing the current selection. Provides read only access
* public access, and private write access.
*/
public final class Selection implements Iterable<String>, Parcelable {
// This class tracks selected items by managing two sets: the saved selection, and the total
// selection. Saved selections are those which have been completed by tapping an item or by
// completing a band select operation. Provisional selections are selections which have been
// temporarily created by an in-progress band select operation (once the user releases the
// mouse button during a band select operation, the selected items become saved). The total
// selection is the combination of both the saved selection and the provisional
// selection. Tracking both separately is necessary to ensure that saved selections do not
// become deselected when they are removed from the provisional selection; for example, if
// item A is tapped (and selected), then an in-progress band select covers A then uncovers
// A, A should still be selected as it has been saved. To ensure this behavior, the saved
// selection must be tracked separately.
final Set<String> mSelection;
final Set<String> mProvisionalSelection;
public Selection() {
mSelection = new HashSet<>();
mProvisionalSelection = new HashSet<>();
}
/**
* Used by CREATOR.
*/
private Selection(Set<String> selection) {
mSelection = selection;
mProvisionalSelection = new HashSet<>();
}
/**
* @param id
* @return true if the position is currently selected.
*/
public boolean contains(@Nullable String id) {
return mSelection.contains(id) || mProvisionalSelection.contains(id);
}
/**
* Returns an {@link Iterator} that iterators over the selection, *excluding*
* any provisional selection.
*
* {@inheritDoc}
*/
@Override
public Iterator<String> iterator() {
return mSelection.iterator();
}
/**
* @return size of the selection including both final and provisional selected items.
*/
public int size() {
return mSelection.size() + mProvisionalSelection.size();
}
/**
* @return true if the selection is empty.
*/
public boolean isEmpty() {
return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
}
/**
* Sets the provisional selection, which is a temporary selection that can be saved,
* canceled, or adjusted at a later time. When a new provision selection is applied, the old
* one (if it exists) is abandoned.
* @return Map of ids added or removed. Added ids have a value of true, removed are false.
*/
@VisibleForTesting
protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) {
Map<String, Boolean> delta = new HashMap<>();
for (String id: mProvisionalSelection) {
// Mark each item that used to be in the selection but is unsaved and not in the new
// provisional selection.
if (!newSelection.contains(id) && !mSelection.contains(id)) {
delta.put(id, false);
}
}
for (String id: mSelection) {
// Mark each item that used to be in the selection but is unsaved and not in the new
// provisional selection.
if (!newSelection.contains(id)) {
delta.put(id, false);
}
}
for (String id: newSelection) {
// Mark each item that was not previously in the selection but is in the new
// provisional selection.
if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) {
delta.put(id, true);
}
}
// Now, iterate through the changes and actually add/remove them to/from the current
// selection. This could not be done in the previous loops because changing the size of
// the selection mid-iteration changes iteration order erroneously.
for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
String id = entry.getKey();
if (entry.getValue()) {
mProvisionalSelection.add(id);
} else {
mProvisionalSelection.remove(id);
}
}
return delta;
}
/**
* Saves the existing provisional selection. Once the provisional selection is saved,
* subsequent provisional selections which are different from this existing one cannot
* cause items in this existing provisional selection to become deselected.
*/
@VisibleForTesting
protected void applyProvisionalSelection() {
mSelection.addAll(mProvisionalSelection);
mProvisionalSelection.clear();
}
/**
* Abandons the existing provisional selection so that all items provisionally selected are
* now deselected.
*/
@VisibleForTesting
void cancelProvisionalSelection() {
mProvisionalSelection.clear();
}
/** @hide */
@VisibleForTesting
public boolean add(String id) {
if (!mSelection.contains(id)) {
mSelection.add(id);
return true;
}
return false;
}
/** @hide */
@VisibleForTesting
boolean remove(String id) {
if (mSelection.contains(id)) {
mSelection.remove(id);
return true;
}
return false;
}
public void clear() {
mSelection.clear();
}
/**
* Trims this selection to be the intersection of itself with the set of given IDs.
*/
public void intersect(Collection<String> ids) {
mSelection.retainAll(ids);
mProvisionalSelection.retainAll(ids);
}
@VisibleForTesting
void copyFrom(Selection source) {
mSelection.clear();
mSelection.addAll(source.mSelection);
mProvisionalSelection.clear();
mProvisionalSelection.addAll(source.mProvisionalSelection);
}
@Override
public String toString() {
if (size() <= 0) {
return "size=0, items=[]";
}
StringBuilder buffer = new StringBuilder(size() * 28);
buffer.append("Selection{")
.append("applied{size=" + mSelection.size())
.append(", entries=" + mSelection)
.append("}, provisional{size=" + mProvisionalSelection.size())
.append(", entries=" + mProvisionalSelection)
.append("}}");
return buffer.toString();
}
@Override
public int hashCode() {
return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
}
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (!(that instanceof Selection)) {
return false;
}
return mSelection.equals(((Selection) that).mSelection) &&
mProvisionalSelection.equals(((Selection) that).mProvisionalSelection);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeStringList(new ArrayList<>(mSelection));
// We don't include provisional selection since it is
// typically coupled to some other runtime state (like a band).
}
public static final ClassLoaderCreator<Selection> CREATOR =
new ClassLoaderCreator<Selection>() {
@Override
public Selection createFromParcel(Parcel in) {
return createFromParcel(in, null);
}
@Override
public Selection createFromParcel(Parcel in, ClassLoader loader) {
ArrayList<String> selected = new ArrayList<>();
in.readStringList(selected);
return new Selection(new HashSet<>(selected));
}
@Override
public Selection[] newArray(int size) {
return new Selection[size];
}
};
}