/* * 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, 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 mSelection; final Set mProvisionalSelection; public Selection() { mSelection = new HashSet<>(); mProvisionalSelection = new HashSet<>(); } /** * Used by CREATOR. */ private Selection(Set 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 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 setProvisionalSelection(Set newSelection) { Map 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 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 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 CREATOR = new ClassLoaderCreator() { @Override public Selection createFromParcel(Parcel in) { return createFromParcel(in, null); } @Override public Selection createFromParcel(Parcel in, ClassLoader loader) { ArrayList selected = new ArrayList<>(); in.readStringList(selected); return new Selection(new HashSet<>(selected)); } @Override public Selection[] newArray(int size) { return new Selection[size]; } }; }