Home | History | Annotate | Download | only in selection
      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 package com.android.documentsui.selection;
     17 
     18 import android.os.Parcel;
     19 import android.os.Parcelable;
     20 import android.support.annotation.VisibleForTesting;
     21 
     22 import java.util.ArrayList;
     23 import java.util.Collection;
     24 import java.util.HashMap;
     25 import java.util.HashSet;
     26 import java.util.Iterator;
     27 import java.util.Map;
     28 import java.util.Set;
     29 
     30 import javax.annotation.Nullable;
     31 
     32 /**
     33  * Object representing the current selection. Provides read only access
     34  * public access, and private write access.
     35  */
     36 public final class Selection implements Iterable<String>, Parcelable {
     37 
     38     // This class tracks selected items by managing two sets: the saved selection, and the total
     39     // selection. Saved selections are those which have been completed by tapping an item or by
     40     // completing a band select operation. Provisional selections are selections which have been
     41     // temporarily created by an in-progress band select operation (once the user releases the
     42     // mouse button during a band select operation, the selected items become saved). The total
     43     // selection is the combination of both the saved selection and the provisional
     44     // selection. Tracking both separately is necessary to ensure that saved selections do not
     45     // become deselected when they are removed from the provisional selection; for example, if
     46     // item A is tapped (and selected), then an in-progress band select covers A then uncovers
     47     // A, A should still be selected as it has been saved. To ensure this behavior, the saved
     48     // selection must be tracked separately.
     49     final Set<String> mSelection;
     50     final Set<String> mProvisionalSelection;
     51 
     52     public Selection() {
     53         mSelection = new HashSet<>();
     54         mProvisionalSelection = new HashSet<>();
     55     }
     56 
     57     /**
     58      * Used by CREATOR.
     59      */
     60     private Selection(Set<String> selection) {
     61         mSelection = selection;
     62         mProvisionalSelection = new HashSet<>();
     63     }
     64 
     65     /**
     66      * @param id
     67      * @return true if the position is currently selected.
     68      */
     69     public boolean contains(@Nullable String id) {
     70         return mSelection.contains(id) || mProvisionalSelection.contains(id);
     71     }
     72 
     73     /**
     74      * Returns an {@link Iterator} that iterators over the selection, *excluding*
     75      * any provisional selection.
     76      *
     77      * {@inheritDoc}
     78      */
     79     @Override
     80     public Iterator<String> iterator() {
     81         return mSelection.iterator();
     82     }
     83 
     84     /**
     85      * @return size of the selection including both final and provisional selected items.
     86      */
     87     public int size() {
     88         return mSelection.size() + mProvisionalSelection.size();
     89     }
     90 
     91     /**
     92      * @return true if the selection is empty.
     93      */
     94     public boolean isEmpty() {
     95         return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
     96     }
     97 
     98     /**
     99      * Sets the provisional selection, which is a temporary selection that can be saved,
    100      * canceled, or adjusted at a later time. When a new provision selection is applied, the old
    101      * one (if it exists) is abandoned.
    102      * @return Map of ids added or removed. Added ids have a value of true, removed are false.
    103      */
    104     @VisibleForTesting
    105     protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) {
    106         Map<String, Boolean> delta = new HashMap<>();
    107 
    108         for (String id: mProvisionalSelection) {
    109             // Mark each item that used to be in the selection but is unsaved and not in the new
    110             // provisional selection.
    111             if (!newSelection.contains(id) && !mSelection.contains(id)) {
    112                 delta.put(id, false);
    113             }
    114         }
    115 
    116         for (String id: mSelection) {
    117             // Mark each item that used to be in the selection but is unsaved and not in the new
    118             // provisional selection.
    119             if (!newSelection.contains(id)) {
    120                 delta.put(id, false);
    121             }
    122         }
    123 
    124         for (String id: newSelection) {
    125             // Mark each item that was not previously in the selection but is in the new
    126             // provisional selection.
    127             if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) {
    128                 delta.put(id, true);
    129             }
    130         }
    131 
    132         // Now, iterate through the changes and actually add/remove them to/from the current
    133         // selection. This could not be done in the previous loops because changing the size of
    134         // the selection mid-iteration changes iteration order erroneously.
    135         for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
    136             String id = entry.getKey();
    137             if (entry.getValue()) {
    138                 mProvisionalSelection.add(id);
    139             } else {
    140                 mProvisionalSelection.remove(id);
    141             }
    142         }
    143 
    144         return delta;
    145     }
    146 
    147     /**
    148      * Saves the existing provisional selection. Once the provisional selection is saved,
    149      * subsequent provisional selections which are different from this existing one cannot
    150      * cause items in this existing provisional selection to become deselected.
    151      */
    152     @VisibleForTesting
    153     protected void applyProvisionalSelection() {
    154         mSelection.addAll(mProvisionalSelection);
    155         mProvisionalSelection.clear();
    156     }
    157 
    158     /**
    159      * Abandons the existing provisional selection so that all items provisionally selected are
    160      * now deselected.
    161      */
    162     @VisibleForTesting
    163     void cancelProvisionalSelection() {
    164         mProvisionalSelection.clear();
    165     }
    166 
    167     /** @hide */
    168     @VisibleForTesting
    169     public boolean add(String id) {
    170         if (!mSelection.contains(id)) {
    171             mSelection.add(id);
    172             return true;
    173         }
    174         return false;
    175     }
    176 
    177     /** @hide */
    178     @VisibleForTesting
    179     boolean remove(String id) {
    180         if (mSelection.contains(id)) {
    181             mSelection.remove(id);
    182             return true;
    183         }
    184         return false;
    185     }
    186 
    187     public void clear() {
    188         mSelection.clear();
    189     }
    190 
    191     /**
    192      * Trims this selection to be the intersection of itself with the set of given IDs.
    193      */
    194     public void intersect(Collection<String> ids) {
    195         mSelection.retainAll(ids);
    196         mProvisionalSelection.retainAll(ids);
    197     }
    198 
    199     @VisibleForTesting
    200     void copyFrom(Selection source) {
    201         mSelection.clear();
    202         mSelection.addAll(source.mSelection);
    203 
    204         mProvisionalSelection.clear();
    205         mProvisionalSelection.addAll(source.mProvisionalSelection);
    206     }
    207 
    208     @Override
    209     public String toString() {
    210         if (size() <= 0) {
    211             return "size=0, items=[]";
    212         }
    213 
    214         StringBuilder buffer = new StringBuilder(size() * 28);
    215         buffer.append("Selection{")
    216             .append("applied{size=" + mSelection.size())
    217             .append(", entries=" + mSelection)
    218             .append("}, provisional{size=" + mProvisionalSelection.size())
    219             .append(", entries=" + mProvisionalSelection)
    220             .append("}}");
    221         return buffer.toString();
    222     }
    223 
    224     @Override
    225     public int hashCode() {
    226         return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
    227     }
    228 
    229     @Override
    230     public boolean equals(Object that) {
    231       if (this == that) {
    232           return true;
    233       }
    234 
    235       if (!(that instanceof Selection)) {
    236           return false;
    237       }
    238 
    239       return mSelection.equals(((Selection) that).mSelection) &&
    240               mProvisionalSelection.equals(((Selection) that).mProvisionalSelection);
    241     }
    242 
    243     @Override
    244     public int describeContents() {
    245         return 0;
    246     }
    247 
    248     @Override
    249     public void writeToParcel(Parcel dest, int flags) {
    250         dest.writeStringList(new ArrayList<>(mSelection));
    251         // We don't include provisional selection since it is
    252         // typically coupled to some other runtime state (like a band).
    253     }
    254 
    255     public static final ClassLoaderCreator<Selection> CREATOR =
    256             new ClassLoaderCreator<Selection>() {
    257         @Override
    258         public Selection createFromParcel(Parcel in) {
    259             return createFromParcel(in, null);
    260         }
    261 
    262         @Override
    263         public Selection createFromParcel(Parcel in, ClassLoader loader) {
    264             ArrayList<String> selected = new ArrayList<>();
    265             in.readStringList(selected);
    266 
    267             return new Selection(new HashSet<>(selected));
    268         }
    269 
    270         @Override
    271         public Selection[] newArray(int size) {
    272             return new Selection[size];
    273         }
    274     };
    275 }