Home | History | Annotate | Download | only in dirlist
      1 /*
      2  * Copyright (C) 2015 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.dirlist;
     18 
     19 import static com.android.documentsui.Shared.DEBUG;
     20 import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY;
     21 import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT;
     22 
     23 import android.annotation.IntDef;
     24 import android.graphics.Point;
     25 import android.graphics.Rect;
     26 import android.graphics.drawable.Drawable;
     27 import android.os.Parcel;
     28 import android.os.Parcelable;
     29 import android.support.annotation.Nullable;
     30 import android.support.annotation.VisibleForTesting;
     31 import android.support.v7.widget.GridLayoutManager;
     32 import android.support.v7.widget.RecyclerView;
     33 import android.util.Log;
     34 import android.util.SparseArray;
     35 import android.util.SparseBooleanArray;
     36 import android.util.SparseIntArray;
     37 import android.view.MotionEvent;
     38 import android.view.View;
     39 
     40 import com.android.documentsui.Events.InputEvent;
     41 import com.android.documentsui.Events.MotionInputEvent;
     42 import com.android.documentsui.R;
     43 
     44 import java.lang.annotation.Retention;
     45 import java.lang.annotation.RetentionPolicy;
     46 import java.util.ArrayList;
     47 import java.util.Collection;
     48 import java.util.Collections;
     49 import java.util.HashMap;
     50 import java.util.HashSet;
     51 import java.util.List;
     52 import java.util.Map;
     53 import java.util.Set;
     54 
     55 /**
     56  * MultiSelectManager provides support traditional multi-item selection support to RecyclerView.
     57  * Additionally it can be configured to restrict selection to a single element, @see
     58  * #setSelectMode.
     59  */
     60 public final class MultiSelectManager {
     61 
     62     @IntDef(flag = true, value = {
     63             MODE_MULTIPLE,
     64             MODE_SINGLE
     65     })
     66     @Retention(RetentionPolicy.SOURCE)
     67     public @interface SelectionMode {}
     68     public static final int MODE_MULTIPLE = 0;
     69     public static final int MODE_SINGLE = 1;
     70 
     71     private static final String TAG = "MultiSelectManager";
     72 
     73     private final Selection mSelection = new Selection();
     74 
     75     private final SelectionEnvironment mEnvironment;
     76     private final DocumentsAdapter mAdapter;
     77     private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
     78 
     79     private Range mRanger;
     80     private boolean mSingleSelect;
     81 
     82     @Nullable private BandController mBandManager;
     83 
     84 
     85     /**
     86      * @param mode Selection single or multiple selection mode.
     87      * @param initialSelection selection state probably preserved in external state.
     88      */
     89     public MultiSelectManager(
     90             final RecyclerView recyclerView,
     91             DocumentsAdapter adapter,
     92             @SelectionMode int mode,
     93             @Nullable Selection initialSelection) {
     94 
     95         this(new RuntimeSelectionEnvironment(recyclerView), adapter, mode, initialSelection);
     96 
     97         if (mode == MODE_MULTIPLE) {
     98             // TODO: Don't load this on low memory devices.
     99             mBandManager = new BandController();
    100         }
    101 
    102         recyclerView.addOnItemTouchListener(
    103                 new RecyclerView.OnItemTouchListener() {
    104                     @Override
    105                     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
    106                         if (mBandManager != null) {
    107                             return mBandManager.handleEvent(new MotionInputEvent(e, recyclerView));
    108                         }
    109                         return false;
    110                     }
    111 
    112                     @Override
    113                     public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    114                         mBandManager.processInputEvent(
    115                                 new MotionInputEvent(e, recyclerView));
    116                     }
    117                     @Override
    118                     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
    119                 });
    120     }
    121 
    122     /**
    123      * Constructs a new instance with {@code adapter} and {@code helper}.
    124      * @param runtimeSelectionEnvironment
    125      * @hide
    126      */
    127     @VisibleForTesting
    128     MultiSelectManager(
    129             SelectionEnvironment environment,
    130             DocumentsAdapter adapter,
    131             @SelectionMode int mode,
    132             @Nullable Selection initialSelection) {
    133 
    134         assert(environment != null);
    135         assert(adapter != null);
    136 
    137         mEnvironment = environment;
    138         mAdapter = adapter;
    139 
    140         mSingleSelect = mode == MODE_SINGLE;
    141         if (initialSelection != null) {
    142             mSelection.copyFrom(initialSelection);
    143         }
    144 
    145         mAdapter.registerAdapterDataObserver(
    146                 new RecyclerView.AdapterDataObserver() {
    147 
    148                     private List<String> mModelIds;
    149 
    150                     @Override
    151                     public void onChanged() {
    152                         mModelIds = mAdapter.getModelIds();
    153 
    154                         // Update the selection to remove any disappeared IDs.
    155                         mSelection.cancelProvisionalSelection();
    156                         mSelection.intersect(mModelIds);
    157 
    158                         if (mBandManager != null && mBandManager.isActive()) {
    159                             mBandManager.endBandSelect();
    160                         }
    161                     }
    162 
    163                     @Override
    164                     public void onItemRangeChanged(
    165                             int startPosition, int itemCount, Object payload) {
    166                         // No change in position. Ignoring.
    167                     }
    168 
    169                     @Override
    170                     public void onItemRangeInserted(int startPosition, int itemCount) {
    171                         mSelection.cancelProvisionalSelection();
    172                     }
    173 
    174                     @Override
    175                     public void onItemRangeRemoved(int startPosition, int itemCount) {
    176                         assert(startPosition >= 0);
    177                         assert(itemCount > 0);
    178 
    179                         mSelection.cancelProvisionalSelection();
    180                         // Remove any disappeared IDs from the selection.
    181                         mSelection.intersect(mModelIds);
    182                     }
    183 
    184                     @Override
    185                     public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
    186                         throw new UnsupportedOperationException();
    187                     }
    188                 });
    189     }
    190 
    191     /**
    192      * Adds {@code callback} such that it will be notified when {@code MultiSelectManager}
    193      * events occur.
    194      *
    195      * @param callback
    196      */
    197     public void addCallback(MultiSelectManager.Callback callback) {
    198         mCallbacks.add(callback);
    199     }
    200 
    201     public boolean hasSelection() {
    202         return !mSelection.isEmpty();
    203     }
    204 
    205     /**
    206      * Returns a Selection object that provides a live view
    207      * on the current selection.
    208      *
    209      * @see #getSelection(Selection) on how to get a snapshot
    210      *     of the selection that will not reflect future changes
    211      *     to selection.
    212      *
    213      * @return The current selection.
    214      */
    215     public Selection getSelection() {
    216         return mSelection;
    217     }
    218 
    219     /**
    220      * Updates {@code dest} to reflect the current selection.
    221      * @param dest
    222      *
    223      * @return The Selection instance passed in, for convenience.
    224      */
    225     public Selection getSelection(Selection dest) {
    226         dest.copyFrom(mSelection);
    227         return dest;
    228     }
    229 
    230     /**
    231      * Updates selection to include items in {@code selection}.
    232      */
    233     public void updateSelection(Selection selection) {
    234         setItemsSelected(selection.toList(), true);
    235     }
    236 
    237     /**
    238      * Sets the selected state of the specified items. Note that the callback will NOT
    239      * be consulted to see if an item can be selected.
    240      *
    241      * @param ids
    242      * @param selected
    243      * @return
    244      */
    245     public boolean setItemsSelected(Iterable<String> ids, boolean selected) {
    246         boolean changed = false;
    247         for (String id: ids) {
    248             boolean itemChanged = selected ? mSelection.add(id) : mSelection.remove(id);
    249             if (itemChanged) {
    250                 notifyItemStateChanged(id, selected);
    251             }
    252             changed |= itemChanged;
    253         }
    254         notifySelectionChanged();
    255         return changed;
    256     }
    257 
    258     /**
    259      * Clears the selection and notifies (even if nothing changes).
    260      */
    261     public void clearSelection() {
    262         clearSelectionQuietly();
    263         notifySelectionChanged();
    264     }
    265 
    266     public void handleLayoutChanged() {
    267         if (mBandManager != null) {
    268             mBandManager.handleLayoutChanged();
    269         }
    270     }
    271 
    272     /**
    273      * Clears the selection, without notifying selection listeners. UI elements still need to be
    274      * notified about state changes so that they can update their appearance.
    275      */
    276     private void clearSelectionQuietly() {
    277         mRanger = null;
    278 
    279         if (!hasSelection()) {
    280             return;
    281         }
    282 
    283         Selection oldSelection = getSelection(new Selection());
    284         mSelection.clear();
    285 
    286         for (String id: oldSelection.getAll()) {
    287             notifyItemStateChanged(id, false);
    288         }
    289     }
    290 
    291     @VisibleForTesting
    292     void onLongPress(InputEvent input) {
    293         if (DEBUG) Log.d(TAG, "Handling long press event.");
    294 
    295         if (!input.isOverItem()) {
    296             if (DEBUG) Log.i(TAG, "Cannot handle tap. No adapter position available.");
    297         }
    298 
    299         handleAdapterEvent(input);
    300     }
    301 
    302     @VisibleForTesting
    303     boolean onSingleTapUp(InputEvent input) {
    304         if (DEBUG) Log.d(TAG, "Processing tap event.");
    305         if (!hasSelection()) {
    306             // No selection active - do nothing.
    307             return false;
    308         }
    309 
    310         if (!input.isOverItem()) {
    311             if (DEBUG) Log.d(TAG, "Activity has no position. Canceling selection.");
    312             clearSelection();
    313             return false;
    314         }
    315 
    316         handleAdapterEvent(input);
    317         return true;
    318     }
    319 
    320     /**
    321      * Handles a change caused by a click on the item with the given position. If the Shift key is
    322      * held down, this performs a range select; otherwise, it simply toggles the item's selection
    323      * state.
    324      */
    325     private void handleAdapterEvent(InputEvent input) {
    326         if (mRanger != null && input.isShiftKeyDown()) {
    327             mRanger.snapSelection(input.getItemPosition());
    328 
    329             // We're being lazy here notifying even when something might not have changed.
    330             // To make this more correct, we'd need to update the Ranger class to return
    331             // information about what has changed.
    332             notifySelectionChanged();
    333         } else {
    334             int position = input.getItemPosition();
    335             toggleSelection(position);
    336             setSelectionRangeBegin(position);
    337         }
    338     }
    339 
    340     /**
    341      * A convenience method for toggling selection by adapter position.
    342      *
    343      * @param position Adapter position to toggle.
    344      */
    345     private void toggleSelection(int position) {
    346         // Position may be special "no position" during certain
    347         // transitional phases. If so, skip handling of the event.
    348         if (position == RecyclerView.NO_POSITION) {
    349             if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position.");
    350             return;
    351         }
    352         String id = mAdapter.getModelId(position);
    353         if (id != null) {
    354             toggleSelection(id);
    355         }
    356     }
    357 
    358     /**
    359      * Toggles selection on the item with the given model ID.
    360      *
    361      * @param modelId
    362      */
    363     public void toggleSelection(String modelId) {
    364         assert(modelId != null);
    365 
    366         boolean changed = false;
    367         if (mSelection.contains(modelId)) {
    368             changed = attemptDeselect(modelId);
    369         } else {
    370             changed = attemptSelect(modelId);
    371         }
    372 
    373         if (changed) {
    374             notifySelectionChanged();
    375         }
    376     }
    377 
    378     /**
    379      * Starts a range selection. If a range selection is already active, this will start a new range
    380      * selection (which will reset the range anchor).
    381      *
    382      * @param pos The anchor position for the selection range.
    383      */
    384     void startRangeSelection(int pos) {
    385         attemptSelect(mAdapter.getModelId(pos));
    386         setSelectionRangeBegin(pos);
    387     }
    388 
    389     /**
    390      * Sets the end point for the current range selection, started by a call to
    391      * {@link #startRangeSelection(int)}. This function should only be called when a range selection
    392      * is active (see {@link #isRangeSelectionActive()}. Items in the range [anchor, end] will be
    393      * selected.
    394      *
    395      * @param pos The new end position for the selection range.
    396      */
    397     void snapRangeSelection(int pos) {
    398         assert(mRanger != null);
    399 
    400         mRanger.snapSelection(pos);
    401         notifySelectionChanged();
    402     }
    403 
    404     /**
    405      * Stops an in-progress range selection.
    406      */
    407     void endRangeSelection() {
    408         mRanger = null;
    409     }
    410 
    411     /**
    412      * @return Whether or not there is a current range selection active.
    413      */
    414     boolean isRangeSelectionActive() {
    415         return mRanger != null;
    416     }
    417 
    418     /**
    419      * Sets the magic location at which a selection range begins (the selection anchor). This value
    420      * is consulted when determining how to extend, and modify selection ranges. Calling this when a
    421      * range selection is active will reset the range selection.
    422      *
    423      * @throws IllegalStateException if {@code position} is not already be selected
    424      * @param position
    425      */
    426     void setSelectionRangeBegin(int position) {
    427         if (position == RecyclerView.NO_POSITION) {
    428             return;
    429         }
    430 
    431         if (mSelection.contains(mAdapter.getModelId(position))) {
    432             mRanger = new Range(position);
    433         }
    434     }
    435 
    436     /**
    437      * Try to set selection state for all elements in range. Not that callbacks can cancel selection
    438      * of specific items, so some or even all items may not reflect the desired state after the
    439      * update is complete.
    440      *
    441      * @param begin Adapter position for range start (inclusive).
    442      * @param end Adapter position for range end (inclusive).
    443      * @param selected New selection state.
    444      */
    445     private void updateRange(int begin, int end, boolean selected) {
    446         assert(end >= begin);
    447         for (int i = begin; i <= end; i++) {
    448             String id = mAdapter.getModelId(i);
    449             if (id == null) {
    450                 continue;
    451             }
    452 
    453             if (selected) {
    454                 boolean canSelect = notifyBeforeItemStateChange(id, true);
    455                 if (canSelect) {
    456                     if (mSingleSelect && hasSelection()) {
    457                         clearSelectionQuietly();
    458                     }
    459                     selectAndNotify(id);
    460                 }
    461             } else {
    462                 attemptDeselect(id);
    463             }
    464         }
    465     }
    466 
    467     /**
    468      * @param modelId
    469      * @return True if the update was applied.
    470      */
    471     private boolean selectAndNotify(String modelId) {
    472         boolean changed = mSelection.add(modelId);
    473         if (changed) {
    474             notifyItemStateChanged(modelId, true);
    475         }
    476         return changed;
    477     }
    478 
    479     /**
    480      * @param id
    481      * @return True if the update was applied.
    482      */
    483     private boolean attemptDeselect(String id) {
    484         assert(id != null);
    485         if (notifyBeforeItemStateChange(id, false)) {
    486             mSelection.remove(id);
    487             notifyItemStateChanged(id, false);
    488             if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection);
    489             return true;
    490         } else {
    491             if (DEBUG) Log.d(TAG, "Select cancelled by listener.");
    492             return false;
    493         }
    494     }
    495 
    496     /**
    497      * @param id
    498      * @return True if the update was applied.
    499      */
    500     private boolean attemptSelect(String id) {
    501         assert(id != null);
    502         boolean canSelect = notifyBeforeItemStateChange(id, true);
    503         if (!canSelect) {
    504             return false;
    505         }
    506         if (mSingleSelect && hasSelection()) {
    507             clearSelectionQuietly();
    508         }
    509 
    510         selectAndNotify(id);
    511         return true;
    512     }
    513 
    514     private boolean notifyBeforeItemStateChange(String id, boolean nextState) {
    515         int lastListener = mCallbacks.size() - 1;
    516         for (int i = lastListener; i > -1; i--) {
    517             if (!mCallbacks.get(i).onBeforeItemStateChange(id, nextState)) {
    518                 return false;
    519             }
    520         }
    521         return true;
    522     }
    523 
    524     /**
    525      * Notifies registered listeners when the selection status of a single item
    526      * (identified by {@code position}) changes.
    527      */
    528     private void notifyItemStateChanged(String id, boolean selected) {
    529         assert(id != null);
    530         int lastListener = mCallbacks.size() - 1;
    531         for (int i = lastListener; i > -1; i--) {
    532             mCallbacks.get(i).onItemStateChanged(id, selected);
    533         }
    534         mAdapter.onItemSelectionChanged(id);
    535     }
    536 
    537     /**
    538      * Notifies registered listeners when the selection has changed. This
    539      * notification should be sent only once a full series of changes
    540      * is complete, e.g. clearingSelection, or updating the single
    541      * selection from one item to another.
    542      */
    543     private void notifySelectionChanged() {
    544         int lastListener = mCallbacks.size() - 1;
    545         for (int i = lastListener; i > -1; i--) {
    546             mCallbacks.get(i).onSelectionChanged();
    547         }
    548     }
    549 
    550     /**
    551      * Class providing support for managing range selections.
    552      */
    553     private final class Range {
    554         private static final int UNDEFINED = -1;
    555 
    556         final int mBegin;
    557         int mEnd = UNDEFINED;
    558 
    559         public Range(int begin) {
    560             if (DEBUG) Log.d(TAG, "New Ranger created beginning @ " + begin);
    561             mBegin = begin;
    562         }
    563 
    564         private void snapSelection(int position) {
    565             assert(mRanger != null);
    566             assert(position != RecyclerView.NO_POSITION);
    567 
    568             if (mEnd == UNDEFINED || mEnd == mBegin) {
    569                 // Reset mEnd so it can be established in establishRange.
    570                 mEnd = UNDEFINED;
    571                 establishRange(position);
    572             } else {
    573                 reviseRange(position);
    574             }
    575         }
    576 
    577         private void establishRange(int position) {
    578             assert(mRanger.mEnd == UNDEFINED);
    579 
    580             if (position == mBegin) {
    581                 mEnd = position;
    582             }
    583 
    584             if (position > mBegin) {
    585                 updateRange(mBegin + 1, position, true);
    586             } else if (position < mBegin) {
    587                 updateRange(position, mBegin - 1, true);
    588             }
    589 
    590             mEnd = position;
    591         }
    592 
    593         private void reviseRange(int position) {
    594             assert(mEnd != UNDEFINED);
    595             assert(mBegin != mEnd);
    596 
    597             if (position == mEnd) {
    598                 if (DEBUG) Log.i(TAG, "Skipping no-op revision click on mEndRange.");
    599             }
    600 
    601             if (mEnd > mBegin) {
    602                 reviseAscendingRange(position);
    603             } else if (mEnd < mBegin) {
    604                 reviseDescendingRange(position);
    605             }
    606             // the "else" case is covered by checkState at beginning of method.
    607 
    608             mEnd = position;
    609         }
    610 
    611         /**
    612          * Updates an existing ascending seleciton.
    613          * @param position
    614          */
    615         private void reviseAscendingRange(int position) {
    616             // Reducing or reversing the range....
    617             if (position < mEnd) {
    618                 if (position < mBegin) {
    619                     updateRange(mBegin + 1, mEnd, false);
    620                     updateRange(position, mBegin -1, true);
    621                 } else {
    622                     updateRange(position + 1, mEnd, false);
    623                 }
    624             }
    625 
    626             // Extending the range...
    627             else if (position > mEnd) {
    628                 updateRange(mEnd + 1, position, true);
    629             }
    630         }
    631 
    632         private void reviseDescendingRange(int position) {
    633             // Reducing or reversing the range....
    634             if (position > mEnd) {
    635                 if (position > mBegin) {
    636                     updateRange(mEnd, mBegin - 1, false);
    637                     updateRange(mBegin + 1, position, true);
    638                 } else {
    639                     updateRange(mEnd, position - 1, false);
    640                 }
    641             }
    642 
    643             // Extending the range...
    644             else if (position < mEnd) {
    645                 updateRange(position, mEnd - 1, true);
    646             }
    647         }
    648     }
    649 
    650     /**
    651      * Object representing the current selection. Provides read only access
    652      * public access, and private write access.
    653      */
    654     public static final class Selection implements Parcelable {
    655 
    656         // This class tracks selected items by managing two sets: the saved selection, and the total
    657         // selection. Saved selections are those which have been completed by tapping an item or by
    658         // completing a band select operation. Provisional selections are selections which have been
    659         // temporarily created by an in-progress band select operation (once the user releases the
    660         // mouse button during a band select operation, the selected items become saved). The total
    661         // selection is the combination of both the saved selection and the provisional
    662         // selection. Tracking both separately is necessary to ensure that saved selections do not
    663         // become deselected when they are removed from the provisional selection; for example, if
    664         // item A is tapped (and selected), then an in-progress band select covers A then uncovers
    665         // A, A should still be selected as it has been saved. To ensure this behavior, the saved
    666         // selection must be tracked separately.
    667         private final Set<String> mSelection;
    668         private final Set<String> mProvisionalSelection;
    669         private String mDirectoryKey;
    670 
    671         public Selection() {
    672             mSelection = new HashSet<String>();
    673             mProvisionalSelection = new HashSet<String>();
    674         }
    675 
    676         /**
    677          * Used by CREATOR.
    678          */
    679         private Selection(String directoryKey, Set<String> selection) {
    680             mDirectoryKey = directoryKey;
    681             mSelection = selection;
    682             mProvisionalSelection = new HashSet<String>();
    683         }
    684 
    685         /**
    686          * @param id
    687          * @return true if the position is currently selected.
    688          */
    689         public boolean contains(@Nullable String id) {
    690             return mSelection.contains(id) || mProvisionalSelection.contains(id);
    691         }
    692 
    693         /**
    694          * Returns an unordered array of selected positions.
    695          */
    696         public String[] getAll() {
    697             return toList().toArray(new String[0]);
    698         }
    699 
    700         /**
    701          * Returns an unordered array of selected positions (including any
    702          * provisional selections current in effect).
    703          */
    704         public List<String> toList() {
    705             ArrayList<String> selection = new ArrayList<String>(mSelection);
    706             selection.addAll(mProvisionalSelection);
    707             return selection;
    708         }
    709 
    710         /**
    711          * @return size of the selection.
    712          */
    713         public int size() {
    714             return mSelection.size() + mProvisionalSelection.size();
    715         }
    716 
    717         /**
    718          * @return true if the selection is empty.
    719          */
    720         public boolean isEmpty() {
    721             return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
    722         }
    723 
    724         /**
    725          * Sets the provisional selection, which is a temporary selection that can be saved,
    726          * canceled, or adjusted at a later time. When a new provision selection is applied, the old
    727          * one (if it exists) is abandoned.
    728          * @return Map of ids added or removed. Added ids have a value of true, removed are false.
    729          */
    730         @VisibleForTesting
    731         protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) {
    732             Map<String, Boolean> delta = new HashMap<>();
    733 
    734             for (String id: mProvisionalSelection) {
    735                 // Mark each item that used to be in the selection but is unsaved and not in the new
    736                 // provisional selection.
    737                 if (!newSelection.contains(id) && !mSelection.contains(id)) {
    738                     delta.put(id, false);
    739                 }
    740             }
    741 
    742             for (String id: mSelection) {
    743                 // Mark each item that used to be in the selection but is unsaved and not in the new
    744                 // provisional selection.
    745                 if (!newSelection.contains(id)) {
    746                     delta.put(id, false);
    747                 }
    748             }
    749 
    750             for (String id: newSelection) {
    751                 // Mark each item that was not previously in the selection but is in the new
    752                 // provisional selection.
    753                 if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) {
    754                     delta.put(id, true);
    755                 }
    756             }
    757 
    758             // Now, iterate through the changes and actually add/remove them to/from the current
    759             // selection. This could not be done in the previous loops because changing the size of
    760             // the selection mid-iteration changes iteration order erroneously.
    761             for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
    762                 String id = entry.getKey();
    763                 if (entry.getValue()) {
    764                     mProvisionalSelection.add(id);
    765                 } else {
    766                     mProvisionalSelection.remove(id);
    767                 }
    768             }
    769 
    770             return delta;
    771         }
    772 
    773         /**
    774          * Saves the existing provisional selection. Once the provisional selection is saved,
    775          * subsequent provisional selections which are different from this existing one cannot
    776          * cause items in this existing provisional selection to become deselected.
    777          */
    778         @VisibleForTesting
    779         protected void applyProvisionalSelection() {
    780             mSelection.addAll(mProvisionalSelection);
    781             mProvisionalSelection.clear();
    782         }
    783 
    784         /**
    785          * Abandons the existing provisional selection so that all items provisionally selected are
    786          * now deselected.
    787          */
    788         @VisibleForTesting
    789         void cancelProvisionalSelection() {
    790             mProvisionalSelection.clear();
    791         }
    792 
    793         /** @hide */
    794         @VisibleForTesting
    795         boolean add(String id) {
    796             if (!mSelection.contains(id)) {
    797                 mSelection.add(id);
    798                 return true;
    799             }
    800             return false;
    801         }
    802 
    803         /** @hide */
    804         @VisibleForTesting
    805         boolean remove(String id) {
    806             if (mSelection.contains(id)) {
    807                 mSelection.remove(id);
    808                 return true;
    809             }
    810             return false;
    811         }
    812 
    813         public void clear() {
    814             mSelection.clear();
    815         }
    816 
    817         /**
    818          * Trims this selection to be the intersection of itself with the set of given IDs.
    819          */
    820         public void intersect(Collection<String> ids) {
    821             mSelection.retainAll(ids);
    822             mProvisionalSelection.retainAll(ids);
    823         }
    824 
    825         @VisibleForTesting
    826         void copyFrom(Selection source) {
    827             mSelection.clear();
    828             mSelection.addAll(source.mSelection);
    829 
    830             mProvisionalSelection.clear();
    831             mProvisionalSelection.addAll(source.mProvisionalSelection);
    832         }
    833 
    834         @Override
    835         public String toString() {
    836             if (size() <= 0) {
    837                 return "size=0, items=[]";
    838             }
    839 
    840             StringBuilder buffer = new StringBuilder(size() * 28);
    841             buffer.append("Selection{")
    842                 .append("applied{size=" + mSelection.size())
    843                 .append(", entries=" + mSelection)
    844                 .append("}, provisional{size=" + mProvisionalSelection.size())
    845                 .append(", entries=" + mProvisionalSelection)
    846                 .append("}}");
    847             return buffer.toString();
    848         }
    849 
    850         @Override
    851         public int hashCode() {
    852             return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
    853         }
    854 
    855         @Override
    856         public boolean equals(Object that) {
    857           if (this == that) {
    858               return true;
    859           }
    860 
    861           if (!(that instanceof Selection)) {
    862               return false;
    863           }
    864 
    865           return mSelection.equals(((Selection) that).mSelection) &&
    866                   mProvisionalSelection.equals(((Selection) that).mProvisionalSelection);
    867         }
    868 
    869         /**
    870          * Sets the state key for this selection, which allows us to match selections
    871          * to particular states (of DirectoryFragment). Basically this lets us avoid
    872          * loading a persisted selection in the wrong directory.
    873          */
    874         public void setDirectoryKey(String key) {
    875             mDirectoryKey = key;
    876         }
    877 
    878         /**
    879          * Sets the state key for this selection, which allows us to match selections
    880          * to particular states (of DirectoryFragment). Basically this lets us avoid
    881          * loading a persisted selection in the wrong directory.
    882          */
    883         public boolean hasDirectoryKey(String key) {
    884             return key.equals(mDirectoryKey);
    885         }
    886 
    887         @Override
    888         public int describeContents() {
    889             return 0;
    890         }
    891 
    892         public void writeToParcel(Parcel dest, int flags) {
    893             dest.writeString(mDirectoryKey);
    894             dest.writeStringList(new ArrayList<>(mSelection));
    895             // We don't include provisional selection since it is
    896             // typically coupled to some other runtime state (like a band).
    897         }
    898 
    899         public static final ClassLoaderCreator<Selection> CREATOR =
    900                 new ClassLoaderCreator<Selection>() {
    901             @Override
    902             public Selection createFromParcel(Parcel in) {
    903                 return createFromParcel(in, null);
    904             }
    905 
    906             @Override
    907             public Selection createFromParcel(Parcel in, ClassLoader loader) {
    908                 String directoryKey = in.readString();
    909 
    910                 ArrayList<String> selected = new ArrayList<>();
    911                 in.readStringList(selected);
    912 
    913                 return new Selection(directoryKey, new HashSet<String>(selected));
    914             }
    915 
    916             @Override
    917             public Selection[] newArray(int size) {
    918                 return new Selection[size];
    919             }
    920         };
    921     }
    922 
    923     /**
    924      * Provides functionality for BandController. Exists primarily to tests that are
    925      * fully isolated from RecyclerView.
    926      */
    927     interface SelectionEnvironment {
    928         void showBand(Rect rect);
    929         void hideBand();
    930         void addOnScrollListener(RecyclerView.OnScrollListener listener);
    931         void removeOnScrollListener(RecyclerView.OnScrollListener listener);
    932         void scrollBy(int dy);
    933         int getHeight();
    934         void invalidateView();
    935         void runAtNextFrame(Runnable r);
    936         void removeCallback(Runnable r);
    937         Point createAbsolutePoint(Point relativePoint);
    938         Rect getAbsoluteRectForChildViewAt(int index);
    939         int getAdapterPositionAt(int index);
    940         int getColumnCount();
    941         int getChildCount();
    942         int getVisibleChildCount();
    943         /**
    944          * Layout items are excluded from the GridModel.
    945          */
    946         boolean isLayoutItem(int adapterPosition);
    947         /**
    948          * Items may be in the adapter, but without an attached view.
    949          */
    950         boolean hasView(int adapterPosition);
    951     }
    952 
    953     /** Recycler view facade implementation backed by good ol' RecyclerView. */
    954     private static final class RuntimeSelectionEnvironment implements SelectionEnvironment {
    955 
    956         private final RecyclerView mView;
    957         private final Drawable mBand;
    958 
    959         private boolean mIsOverlayShown = false;
    960 
    961         RuntimeSelectionEnvironment(RecyclerView view) {
    962             mView = view;
    963             mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
    964         }
    965 
    966         @Override
    967         public int getAdapterPositionAt(int index) {
    968             return mView.getChildAdapterPosition(mView.getChildAt(index));
    969         }
    970 
    971         @Override
    972         public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
    973             mView.addOnScrollListener(listener);
    974         }
    975 
    976         @Override
    977         public void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
    978             mView.removeOnScrollListener(listener);
    979         }
    980 
    981         @Override
    982         public Point createAbsolutePoint(Point relativePoint) {
    983             return new Point(relativePoint.x + mView.computeHorizontalScrollOffset(),
    984                     relativePoint.y + mView.computeVerticalScrollOffset());
    985         }
    986 
    987         @Override
    988         public Rect getAbsoluteRectForChildViewAt(int index) {
    989             final View child = mView.getChildAt(index);
    990             final Rect childRect = new Rect();
    991             child.getHitRect(childRect);
    992             childRect.left += mView.computeHorizontalScrollOffset();
    993             childRect.right += mView.computeHorizontalScrollOffset();
    994             childRect.top += mView.computeVerticalScrollOffset();
    995             childRect.bottom += mView.computeVerticalScrollOffset();
    996             return childRect;
    997         }
    998 
    999         @Override
   1000         public int getChildCount() {
   1001             return mView.getAdapter().getItemCount();
   1002         }
   1003 
   1004         @Override
   1005         public int getVisibleChildCount() {
   1006             return mView.getChildCount();
   1007         }
   1008 
   1009         @Override
   1010         public int getColumnCount() {
   1011             RecyclerView.LayoutManager layoutManager = mView.getLayoutManager();
   1012             if (layoutManager instanceof GridLayoutManager) {
   1013                 return ((GridLayoutManager) layoutManager).getSpanCount();
   1014             }
   1015 
   1016             // Otherwise, it is a list with 1 column.
   1017             return 1;
   1018         }
   1019 
   1020         @Override
   1021         public int getHeight() {
   1022             return mView.getHeight();
   1023         }
   1024 
   1025         @Override
   1026         public void invalidateView() {
   1027             mView.invalidate();
   1028         }
   1029 
   1030         @Override
   1031         public void runAtNextFrame(Runnable r) {
   1032             mView.postOnAnimation(r);
   1033         }
   1034 
   1035         @Override
   1036         public void removeCallback(Runnable r) {
   1037             mView.removeCallbacks(r);
   1038         }
   1039 
   1040         @Override
   1041         public void scrollBy(int dy) {
   1042             mView.scrollBy(0, dy);
   1043         }
   1044 
   1045         @Override
   1046         public void showBand(Rect rect) {
   1047             mBand.setBounds(rect);
   1048 
   1049             if (!mIsOverlayShown) {
   1050                 mView.getOverlay().add(mBand);
   1051             }
   1052         }
   1053 
   1054         @Override
   1055         public void hideBand() {
   1056             mView.getOverlay().remove(mBand);
   1057         }
   1058 
   1059         @Override
   1060         public boolean isLayoutItem(int pos) {
   1061             // The band selection model only operates on documents and directories. Exclude other
   1062             // types of adapter items (e.g. whitespace items like dividers).
   1063             RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
   1064             switch (vh.getItemViewType()) {
   1065                 case ITEM_TYPE_DOCUMENT:
   1066                 case ITEM_TYPE_DIRECTORY:
   1067                     return false;
   1068                 default:
   1069                     return true;
   1070             }
   1071         }
   1072 
   1073         @Override
   1074         public boolean hasView(int pos) {
   1075             return mView.findViewHolderForAdapterPosition(pos) != null;
   1076         }
   1077     }
   1078 
   1079     public interface Callback {
   1080         /**
   1081          * Called when an item is selected or unselected while in selection mode.
   1082          *
   1083          * @param position Adapter position of the item that was checked or unchecked
   1084          * @param selected <code>true</code> if the item is now selected, <code>false</code>
   1085          *                if the item is now unselected.
   1086          */
   1087         public void onItemStateChanged(String id, boolean selected);
   1088 
   1089         /**
   1090          * Called prior to an item changing state. Callbacks can cancel
   1091          * the change at {@code position} by returning {@code false}.
   1092          *
   1093          * @param id Adapter position of the item that was checked or unchecked
   1094          * @param selected <code>true</code> if the item is to be selected, <code>false</code>
   1095          *                if the item is to be unselected.
   1096          */
   1097         public boolean onBeforeItemStateChange(String id, boolean selected);
   1098 
   1099         /**
   1100          * Called immediately after completion of any set of changes.
   1101          */
   1102         public void onSelectionChanged();
   1103     }
   1104 
   1105     /**
   1106      * Provides mouse driven band-select support when used in conjunction with {@link RecyclerView}
   1107      * and {@link MultiSelectManager}. This class is responsible for rendering the band select
   1108      * overlay and selecting overlaid items via MultiSelectManager.
   1109      */
   1110     public class BandController extends RecyclerView.OnScrollListener
   1111             implements GridModel.OnSelectionChangedListener {
   1112 
   1113         private static final int NOT_SET = -1;
   1114 
   1115         private final Runnable mModelBuilder;
   1116 
   1117         @Nullable private Rect mBounds;
   1118         @Nullable private Point mCurrentPosition;
   1119         @Nullable private Point mOrigin;
   1120         @Nullable private GridModel mModel;
   1121 
   1122         // The time at which the current band selection-induced scroll began. If no scroll is in
   1123         // progress, the value is NOT_SET.
   1124         private long mScrollStartTime = NOT_SET;
   1125         private final Runnable mViewScroller = new ViewScroller();
   1126 
   1127         public BandController() {
   1128             mEnvironment.addOnScrollListener(this);
   1129 
   1130             mModelBuilder = new Runnable() {
   1131                 @Override
   1132                 public void run() {
   1133                     mModel = new GridModel(mEnvironment, mAdapter);
   1134                     mModel.addOnSelectionChangedListener(BandController.this);
   1135                 }
   1136             };
   1137         }
   1138 
   1139         public boolean handleEvent(MotionInputEvent e) {
   1140             // b/23793622 notes the fact that we *never* receive ACTION_DOWN
   1141             // events in onTouchEvent. Where it not for this issue, we'd
   1142             // push start handling down into handleInputEvent.
   1143             if (mBandManager.shouldStart(e)) {
   1144                 // endBandSelect is handled in handleInputEvent.
   1145                 mBandManager.startBandSelect(e.getOrigin());
   1146             } else if (mBandManager.isActive()
   1147                     && e.isMouseEvent()
   1148                     && e.isActionUp()) {
   1149                 // Same issue here w b/23793622. The ACTION_UP event
   1150                 // is only evert dispatched to onTouchEvent when
   1151                 // there is some associated motion. If a user taps
   1152                 // mouse, but doesn't move, then band select gets
   1153                 // started BUT not ended. Causing phantom
   1154                 // bands to appear when the user later clicks to start
   1155                 // band select.
   1156                 mBandManager.processInputEvent(e);
   1157             }
   1158 
   1159             return isActive();
   1160         }
   1161 
   1162         private boolean isActive() {
   1163             return mModel != null;
   1164         }
   1165 
   1166         /**
   1167          * Handle a change in layout by cleaning up and getting rid of the old model and creating
   1168          * a new model which will track the new layout.
   1169          */
   1170         public void handleLayoutChanged() {
   1171             if (mModel != null) {
   1172                 mModel.removeOnSelectionChangedListener(this);
   1173                 mModel.stopListening();
   1174 
   1175                 // build a new model, all fresh and happy.
   1176                 mModelBuilder.run();
   1177             }
   1178         }
   1179 
   1180         boolean shouldStart(MotionInputEvent e) {
   1181             return !isActive()
   1182                     && e.isMouseEvent()  // a mouse
   1183                     && e.isActionDown()  // the initial button press
   1184                     && mAdapter.getItemCount() > 0
   1185                     && e.getItemPosition() == RecyclerView.NO_ID;  // in empty space
   1186         }
   1187 
   1188         boolean shouldStop(InputEvent input) {
   1189             return isActive()
   1190                     && input.isMouseEvent()
   1191                     && input.isActionUp();
   1192         }
   1193 
   1194         /**
   1195          * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
   1196          * @param input
   1197          */
   1198         private void processInputEvent(InputEvent input) {
   1199             assert(input.isMouseEvent());
   1200 
   1201             if (shouldStop(input)) {
   1202                 endBandSelect();
   1203                 return;
   1204             }
   1205 
   1206             // We shouldn't get any events in this method when band select is not active,
   1207             // but it turns some guests show up late to the party.
   1208             if (!isActive()) {
   1209                 return;
   1210             }
   1211 
   1212             mCurrentPosition = input.getOrigin();
   1213             mModel.resizeSelection(input.getOrigin());
   1214             scrollViewIfNecessary();
   1215             resizeBandSelectRectangle();
   1216         }
   1217 
   1218         /**
   1219          * Starts band select by adding the drawable to the RecyclerView's overlay.
   1220          */
   1221         private void startBandSelect(Point origin) {
   1222             if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
   1223 
   1224             mOrigin = origin;
   1225             mModelBuilder.run();  // Creates a new selection model.
   1226             mModel.startSelection(mOrigin);
   1227         }
   1228 
   1229         /**
   1230          * Scrolls the view if necessary.
   1231          */
   1232         private void scrollViewIfNecessary() {
   1233             mEnvironment.removeCallback(mViewScroller);
   1234             mViewScroller.run();
   1235             mEnvironment.invalidateView();
   1236         }
   1237 
   1238         /**
   1239          * Resizes the band select rectangle by using the origin and the current pointer position as
   1240          * two opposite corners of the selection.
   1241          */
   1242         private void resizeBandSelectRectangle() {
   1243             mBounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
   1244                     Math.min(mOrigin.y, mCurrentPosition.y),
   1245                     Math.max(mOrigin.x, mCurrentPosition.x),
   1246                     Math.max(mOrigin.y, mCurrentPosition.y));
   1247             mEnvironment.showBand(mBounds);
   1248         }
   1249 
   1250         /**
   1251          * Ends band select by removing the overlay.
   1252          */
   1253         private void endBandSelect() {
   1254             if (DEBUG) Log.d(TAG, "Ending band select.");
   1255 
   1256             mEnvironment.hideBand();
   1257             mSelection.applyProvisionalSelection();
   1258             mModel.endSelection();
   1259             int firstSelected = mModel.getPositionNearestOrigin();
   1260             if (firstSelected != NOT_SET) {
   1261                 if (mSelection.contains(mAdapter.getModelId(firstSelected))) {
   1262                     // TODO: firstSelected should really be lastSelected, we want to anchor the item
   1263                     // where the mouse-up occurred.
   1264                     setSelectionRangeBegin(firstSelected);
   1265                 } else {
   1266                     // TODO: Check if this is really happening.
   1267                     Log.w(TAG, "First selected by band is NOT in selection!");
   1268                 }
   1269             }
   1270 
   1271             mModel = null;
   1272             mOrigin = null;
   1273         }
   1274 
   1275         @Override
   1276         public void onSelectionChanged(Set<String> updatedSelection) {
   1277             Map<String, Boolean> delta = mSelection.setProvisionalSelection(updatedSelection);
   1278             for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
   1279                 notifyItemStateChanged(entry.getKey(), entry.getValue());
   1280             }
   1281             notifySelectionChanged();
   1282         }
   1283 
   1284         @Override
   1285         public boolean onBeforeItemStateChange(String id, boolean nextState) {
   1286             return notifyBeforeItemStateChange(id, nextState);
   1287         }
   1288 
   1289         private class ViewScroller implements Runnable {
   1290             /**
   1291              * The number of milliseconds of scrolling at which scroll speed continues to increase.
   1292              * At first, the scroll starts slowly; then, the rate of scrolling increases until it
   1293              * reaches its maximum value at after this many milliseconds.
   1294              */
   1295             private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
   1296 
   1297             @Override
   1298             public void run() {
   1299                 // Compute the number of pixels the pointer's y-coordinate is past the view.
   1300                 // Negative values mean the pointer is at or before the top of the view, and
   1301                 // positive values mean that the pointer is at or after the bottom of the view. Note
   1302                 // that one additional pixel is added here so that the view still scrolls when the
   1303                 // pointer is exactly at the top or bottom.
   1304                 int pixelsPastView = 0;
   1305                 if (mCurrentPosition.y <= 0) {
   1306                     pixelsPastView = mCurrentPosition.y - 1;
   1307                 } else if (mCurrentPosition.y >= mEnvironment.getHeight() - 1) {
   1308                     pixelsPastView = mCurrentPosition.y - mEnvironment.getHeight() + 1;
   1309                 }
   1310 
   1311                 if (!isActive() || pixelsPastView == 0) {
   1312                     // If band selection is inactive, or if it is active but not at the edge of the
   1313                     // view, no scrolling is necessary.
   1314                     mScrollStartTime = NOT_SET;
   1315                     return;
   1316                 }
   1317 
   1318                 if (mScrollStartTime == NOT_SET) {
   1319                     // If the pointer was previously not at the edge of the view but now is, set the
   1320                     // start time for the scroll.
   1321                     mScrollStartTime = System.currentTimeMillis();
   1322                 }
   1323 
   1324                 // Compute the number of pixels to scroll, and scroll that many pixels.
   1325                 final int numPixels = computeScrollDistance(
   1326                         pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
   1327                 mEnvironment.scrollBy(numPixels);
   1328 
   1329                 mEnvironment.removeCallback(mViewScroller);
   1330                 mEnvironment.runAtNextFrame(this);
   1331             }
   1332 
   1333             /**
   1334              * Computes the number of pixels to scroll based on how far the pointer is past the end
   1335              * of the view and how long it has been there. Roughly based on ItemTouchHelper's
   1336              * algorithm for computing the number of pixels to scroll when an item is dragged to the
   1337              * end of a {@link RecyclerView}.
   1338              * @param pixelsPastView
   1339              * @param scrollDuration
   1340              * @return
   1341              */
   1342             private int computeScrollDistance(int pixelsPastView, long scrollDuration) {
   1343                 final int maxScrollStep = mEnvironment.getHeight();
   1344                 final int direction = (int) Math.signum(pixelsPastView);
   1345                 final int absPastView = Math.abs(pixelsPastView);
   1346 
   1347                 // Calculate the ratio of how far out of the view the pointer currently resides to
   1348                 // the entire height of the view.
   1349                 final float outOfBoundsRatio = Math.min(
   1350                         1.0f, (float) absPastView / mEnvironment.getHeight());
   1351                 // Interpolate this ratio and use it to compute the maximum scroll that should be
   1352                 // possible for this step.
   1353                 final float cappedScrollStep =
   1354                         direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);
   1355 
   1356                 // Likewise, calculate the ratio of the time spent in the scroll to the limit.
   1357                 final float timeRatio = Math.min(
   1358                         1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
   1359                 // Interpolate this ratio and use it to compute the final number of pixels to
   1360                 // scroll.
   1361                 final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));
   1362 
   1363                 // If the final number of pixels to scroll ends up being 0, the view should still
   1364                 // scroll at least one pixel.
   1365                 return numPixels != 0 ? numPixels : direction;
   1366             }
   1367 
   1368             /**
   1369              * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
   1370              * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
   1371              * drags that are at the edge or barely past the edge of the view still cause sufficient
   1372              * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if
   1373              * needed.
   1374              * @param ratio A ratio which is in the range [0, 1].
   1375              * @return A "smoothed" value, also in the range [0, 1].
   1376              */
   1377             private float smoothOutOfBoundsRatio(float ratio) {
   1378                 return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
   1379             }
   1380 
   1381             /**
   1382              * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1)
   1383              * and stays close to 0 for most input values except those very close to 1. This ensures
   1384              * that scrolls start out very slowly but speed up drastically after the scroll has been
   1385              * in progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used,
   1386              * but this could also be tweaked if needed.
   1387              * @param ratio A ratio which is in the range [0, 1].
   1388              * @return A "smoothed" value, also in the range [0, 1].
   1389              */
   1390             private float smoothTimeRatio(float ratio) {
   1391                 return (float) Math.pow(ratio, 5);
   1392             }
   1393         };
   1394 
   1395         @Override
   1396         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
   1397             if (!isActive()) {
   1398                 return;
   1399             }
   1400 
   1401             // Adjust the y-coordinate of the origin the opposite number of pixels so that the
   1402             // origin remains in the same place relative to the view's items.
   1403             mOrigin.y -= dy;
   1404             resizeBandSelectRectangle();
   1405         }
   1406     }
   1407 
   1408     /**
   1409      * Provides a band selection item model for views within a RecyclerView. This class queries the
   1410      * RecyclerView to determine where its items are placed; then, once band selection is underway,
   1411      * it alerts listeners of which items are covered by the selections.
   1412      */
   1413     public static final class GridModel extends RecyclerView.OnScrollListener {
   1414 
   1415         public static final int NOT_SET = -1;
   1416 
   1417         // Enum values used to determine the corner at which the origin is located within the
   1418         private static final int UPPER = 0x00;
   1419         private static final int LOWER = 0x01;
   1420         private static final int LEFT = 0x00;
   1421         private static final int RIGHT = 0x02;
   1422         private static final int UPPER_LEFT = UPPER | LEFT;
   1423         private static final int UPPER_RIGHT = UPPER | RIGHT;
   1424         private static final int LOWER_LEFT = LOWER | LEFT;
   1425         private static final int LOWER_RIGHT = LOWER | RIGHT;
   1426 
   1427         private final SelectionEnvironment mHelper;
   1428         private final DocumentsAdapter mAdapter;
   1429 
   1430         private final List<OnSelectionChangedListener> mOnSelectionChangedListeners =
   1431                 new ArrayList<>();
   1432 
   1433         // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
   1434         // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
   1435         // mColumns.get(5) would return an array of positions in that column. Within that array, the
   1436         // value for key y is the adapter position for the item whose y-offset is y.
   1437         private final SparseArray<SparseIntArray> mColumns = new SparseArray<>();
   1438 
   1439         // List of limits along the x-axis (columns).
   1440         // This list is sorted from furthest left to furthest right.
   1441         private final List<Limits> mColumnBounds = new ArrayList<>();
   1442 
   1443         // List of limits along the y-axis (rows). Note that this list only contains items which
   1444         // have been in the viewport.
   1445         private final List<Limits> mRowBounds = new ArrayList<>();
   1446 
   1447         // The adapter positions which have been recorded so far.
   1448         private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
   1449 
   1450         // Array passed to registered OnSelectionChangedListeners. One array is created and reused
   1451         // throughout the lifetime of the object.
   1452         private final Set<String> mSelection = new HashSet<>();
   1453 
   1454         // The current pointer (in absolute positioning from the top of the view).
   1455         private Point mPointer = null;
   1456 
   1457         // The bounds of the band selection.
   1458         private RelativePoint mRelativeOrigin;
   1459         private RelativePoint mRelativePointer;
   1460 
   1461         private boolean mIsActive;
   1462 
   1463         // Tracks where the band select originated from. This is used to determine where selections
   1464         // should expand from when Shift+click is used.
   1465         private int mPositionNearestOrigin = NOT_SET;
   1466 
   1467         GridModel(SelectionEnvironment helper, DocumentsAdapter adapter) {
   1468             mHelper = helper;
   1469             mAdapter = adapter;
   1470             mHelper.addOnScrollListener(this);
   1471         }
   1472 
   1473         /**
   1474          * Stops listening to the view's scrolls. Call this function before discarding a
   1475          * BandSelecModel object to prevent memory leaks.
   1476          */
   1477         void stopListening() {
   1478             mHelper.removeOnScrollListener(this);
   1479         }
   1480 
   1481         /**
   1482          * Start a band select operation at the given point.
   1483          * @param relativeOrigin The origin of the band select operation, relative to the viewport.
   1484          *     For example, if the view is scrolled to the bottom, the top-left of the viewport
   1485          *     would have a relative origin of (0, 0), even though its absolute point has a higher
   1486          *     y-value.
   1487          */
   1488         void startSelection(Point relativeOrigin) {
   1489             recordVisibleChildren();
   1490             if (isEmpty()) {
   1491                 // The selection band logic works only if there is at least one visible child.
   1492                 return;
   1493             }
   1494 
   1495             mIsActive = true;
   1496             mPointer = mHelper.createAbsolutePoint(relativeOrigin);
   1497             mRelativeOrigin = new RelativePoint(mPointer);
   1498             mRelativePointer = new RelativePoint(mPointer);
   1499             computeCurrentSelection();
   1500             notifyListeners();
   1501         }
   1502 
   1503         /**
   1504          * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
   1505          * opposite the origin.
   1506          * @param relativePointer The pointer (opposite of the origin) of the band select operation,
   1507          *     relative to the viewport. For example, if the view is scrolled to the bottom, the
   1508          *     top-left of the viewport would have a relative origin of (0, 0), even though its
   1509          *     absolute point has a higher y-value.
   1510          */
   1511         @VisibleForTesting
   1512         void resizeSelection(Point relativePointer) {
   1513             mPointer = mHelper.createAbsolutePoint(relativePointer);
   1514             updateModel();
   1515         }
   1516 
   1517         /**
   1518          * Ends the band selection.
   1519          */
   1520         void endSelection() {
   1521             mIsActive = false;
   1522         }
   1523 
   1524         /**
   1525          * @return The adapter position for the item nearest the origin corresponding to the latest
   1526          *         band select operation, or NOT_SET if the selection did not cover any items.
   1527          */
   1528         int getPositionNearestOrigin() {
   1529             return mPositionNearestOrigin;
   1530         }
   1531 
   1532         @Override
   1533         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
   1534             if (!mIsActive) {
   1535                 return;
   1536             }
   1537 
   1538             mPointer.x += dx;
   1539             mPointer.y += dy;
   1540             recordVisibleChildren();
   1541             updateModel();
   1542         }
   1543 
   1544         /**
   1545          * Queries the view for all children and records their location metadata.
   1546          */
   1547         private void recordVisibleChildren() {
   1548             for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
   1549                 int adapterPosition = mHelper.getAdapterPositionAt(i);
   1550                 // Sometimes the view is not attached, as we notify the multi selection manager
   1551                 // synchronously, while views are attached asynchronously. As a result items which
   1552                 // are in the adapter may not actually have a corresponding view (yet).
   1553                 if (mHelper.hasView(adapterPosition) &&
   1554                         !mHelper.isLayoutItem(adapterPosition) &&
   1555                         !mKnownPositions.get(adapterPosition)) {
   1556                     mKnownPositions.put(adapterPosition, true);
   1557                     recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
   1558                 }
   1559             }
   1560         }
   1561 
   1562         /**
   1563          * Checks if there are any recorded children.
   1564          */
   1565         private boolean isEmpty() {
   1566             return mColumnBounds.size() == 0 || mRowBounds.size() == 0;
   1567         }
   1568 
   1569         /**
   1570          * Updates the limits lists and column map with the given item metadata.
   1571          * @param absoluteChildRect The absolute rectangle for the child view being processed.
   1572          * @param adapterPosition The position of the child view being processed.
   1573          */
   1574         private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
   1575             if (mColumnBounds.size() != mHelper.getColumnCount()) {
   1576                 // If not all x-limits have been recorded, record this one.
   1577                 recordLimits(
   1578                         mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right));
   1579             }
   1580 
   1581             recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
   1582 
   1583             SparseIntArray columnList = mColumns.get(absoluteChildRect.left);
   1584             if (columnList == null) {
   1585                 columnList = new SparseIntArray();
   1586                 mColumns.put(absoluteChildRect.left, columnList);
   1587             }
   1588             columnList.put(absoluteChildRect.top, adapterPosition);
   1589         }
   1590 
   1591         /**
   1592          * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
   1593          * does not exist.
   1594          */
   1595         private void recordLimits(List<Limits> limitsList, Limits limits) {
   1596             int index = Collections.binarySearch(limitsList, limits);
   1597             if (index < 0) {
   1598                 limitsList.add(~index, limits);
   1599             }
   1600         }
   1601 
   1602         /**
   1603          * Handles a moved pointer; this function determines whether the pointer movement resulted
   1604          * in a selection change and, if it has, notifies listeners of this change.
   1605          */
   1606         private void updateModel() {
   1607             RelativePoint old = mRelativePointer;
   1608             mRelativePointer = new RelativePoint(mPointer);
   1609             if (old != null && mRelativePointer.equals(old)) {
   1610                 return;
   1611             }
   1612 
   1613             computeCurrentSelection();
   1614             notifyListeners();
   1615         }
   1616 
   1617         /**
   1618          * Computes the currently-selected items.
   1619          */
   1620         private void computeCurrentSelection() {
   1621             if (areItemsCoveredByBand(mRelativePointer, mRelativeOrigin)) {
   1622                 updateSelection(computeBounds());
   1623             } else {
   1624                 mSelection.clear();
   1625                 mPositionNearestOrigin = NOT_SET;
   1626             }
   1627         }
   1628 
   1629         /**
   1630          * Notifies all listeners of a selection change. Note that this function simply passes
   1631          * mSelection, so computeCurrentSelection() should be called before this
   1632          * function.
   1633          */
   1634         private void notifyListeners() {
   1635             for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
   1636                 listener.onSelectionChanged(mSelection);
   1637             }
   1638         }
   1639 
   1640         /**
   1641          * @param rect Rectangle including all covered items.
   1642          */
   1643         private void updateSelection(Rect rect) {
   1644             int columnStart =
   1645                     Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
   1646             assert(columnStart >= 0);
   1647             int columnEnd = columnStart;
   1648 
   1649             for (int i = columnStart; i < mColumnBounds.size()
   1650                     && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
   1651                 columnEnd = i;
   1652             }
   1653 
   1654             int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
   1655             if (rowStart < 0) {
   1656                 mPositionNearestOrigin = NOT_SET;
   1657                 return;
   1658             }
   1659 
   1660             int rowEnd = rowStart;
   1661             for (int i = rowStart; i < mRowBounds.size()
   1662                     && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
   1663                 rowEnd = i;
   1664             }
   1665 
   1666             updateSelection(columnStart, columnEnd, rowStart, rowEnd);
   1667         }
   1668 
   1669         /**
   1670          * Computes the selection given the previously-computed start- and end-indices for each
   1671          * row and column.
   1672          */
   1673         private void updateSelection(
   1674                 int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
   1675             if (DEBUG) Log.d(TAG, String.format("updateSelection: %d, %d, %d, %d",
   1676                     columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
   1677 
   1678             mSelection.clear();
   1679             for (int column = columnStartIndex; column <= columnEndIndex; column++) {
   1680                 SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
   1681                 for (int row = rowStartIndex; row <= rowEndIndex; row++) {
   1682                     // The default return value for SparseIntArray.get is 0, which is a valid
   1683                     // position. Use a sentry value to prevent erroneously selecting item 0.
   1684                     final int rowKey = mRowBounds.get(row).lowerLimit;
   1685                     int position = items.get(rowKey, NOT_SET);
   1686                     if (position != NOT_SET) {
   1687                         String id = mAdapter.getModelId(position);
   1688                         if (id != null) {
   1689                             // The adapter inserts items for UI layout purposes that aren't associated
   1690                             // with files.  Those will have a null model ID.  Don't select them.
   1691                             if (canSelect(id)) {
   1692                                 mSelection.add(id);
   1693                             }
   1694                         }
   1695                         if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
   1696                                 row, rowStartIndex, rowEndIndex)) {
   1697                             // If this is the position nearest the origin, record it now so that it
   1698                             // can be returned by endSelection() later.
   1699                             mPositionNearestOrigin = position;
   1700                         }
   1701                     }
   1702                 }
   1703             }
   1704         }
   1705 
   1706         /**
   1707          * @return True if the item is selectable.
   1708          */
   1709         private boolean canSelect(String id) {
   1710             // TODO: Simplify the logic, so the check whether we can select is done in one place.
   1711             // Consider injecting FragmentTuner, or move the checks from MultiSelectManager to
   1712             // Selection.
   1713             for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
   1714                 if (!listener.onBeforeItemStateChange(id, true)) {
   1715                     return false;
   1716                 }
   1717             }
   1718             return true;
   1719         }
   1720 
   1721         /**
   1722          * @return Returns true if the position is the nearest to the origin, or, in the case of the
   1723          *     lower-right corner, whether it is possible that the position is the nearest to the
   1724          *     origin. See comment below for reasoning for this special case.
   1725          */
   1726         private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
   1727                 int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
   1728             int corner = computeCornerNearestOrigin();
   1729             switch (corner) {
   1730                 case UPPER_LEFT:
   1731                     return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
   1732                 case UPPER_RIGHT:
   1733                     return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
   1734                 case LOWER_LEFT:
   1735                     return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
   1736                 case LOWER_RIGHT:
   1737                     // Note that in some cases, the last row will not have as many items as there
   1738                     // are columns (e.g., if there are 4 items and 3 columns, the second row will
   1739                     // only have one item in the first column). This function is invoked for each
   1740                     // position from left to right, so return true for any position in the bottom
   1741                     // row and only the right-most position in the bottom row will be recorded.
   1742                     return rowIndex == rowEndIndex;
   1743                 default:
   1744                     throw new RuntimeException("Invalid corner type.");
   1745             }
   1746         }
   1747 
   1748         /**
   1749          * Listener for changes in which items have been band selected.
   1750          */
   1751         static interface OnSelectionChangedListener {
   1752             public void onSelectionChanged(Set<String> updatedSelection);
   1753             public boolean onBeforeItemStateChange(String id, boolean nextState);
   1754         }
   1755 
   1756         void addOnSelectionChangedListener(OnSelectionChangedListener listener) {
   1757             mOnSelectionChangedListeners.add(listener);
   1758         }
   1759 
   1760         void removeOnSelectionChangedListener(OnSelectionChangedListener listener) {
   1761             mOnSelectionChangedListeners.remove(listener);
   1762         }
   1763 
   1764         /**
   1765          * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
   1766          * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
   1767          * of item columns and the top- and bottom sides of item rows so that it can be determined
   1768          * whether the pointer is located within the bounds of an item.
   1769          */
   1770         private static class Limits implements Comparable<Limits> {
   1771             int lowerLimit;
   1772             int upperLimit;
   1773 
   1774             Limits(int lowerLimit, int upperLimit) {
   1775                 this.lowerLimit = lowerLimit;
   1776                 this.upperLimit = upperLimit;
   1777             }
   1778 
   1779             @Override
   1780             public int compareTo(Limits other) {
   1781                 return lowerLimit - other.lowerLimit;
   1782             }
   1783 
   1784             @Override
   1785             public boolean equals(Object other) {
   1786                 if (!(other instanceof Limits)) {
   1787                     return false;
   1788                 }
   1789 
   1790                 return ((Limits) other).lowerLimit == lowerLimit &&
   1791                         ((Limits) other).upperLimit == upperLimit;
   1792             }
   1793 
   1794             @Override
   1795             public String toString() {
   1796                 return "(" + lowerLimit + ", " + upperLimit + ")";
   1797             }
   1798         }
   1799 
   1800         /**
   1801          * The location of a coordinate relative to items. This class represents a general area of the
   1802          * view as it relates to band selection rather than an explicit point. For example, two
   1803          * different points within an item are considered to have the same "location" because band
   1804          * selection originating within the item would select the same items no matter which point
   1805          * was used. Same goes for points between items as well as those at the very beginning or end
   1806          * of the view.
   1807          *
   1808          * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
   1809          * advantage of tying the value to the Limits of items along that axis. This allows easy
   1810          * selection of items within those Limits as opposed to a search through every item to see if a
   1811          * given coordinate value falls within those Limits.
   1812          */
   1813         private static class RelativeCoordinate
   1814                 implements Comparable<RelativeCoordinate> {
   1815             /**
   1816              * Location describing points after the last known item.
   1817              */
   1818             static final int AFTER_LAST_ITEM = 0;
   1819 
   1820             /**
   1821              * Location describing points before the first known item.
   1822              */
   1823             static final int BEFORE_FIRST_ITEM = 1;
   1824 
   1825             /**
   1826              * Location describing points between two items.
   1827              */
   1828             static final int BETWEEN_TWO_ITEMS = 2;
   1829 
   1830             /**
   1831              * Location describing points within the limits of one item.
   1832              */
   1833             static final int WITHIN_LIMITS = 3;
   1834 
   1835             /**
   1836              * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
   1837              * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
   1838              */
   1839             final int type;
   1840 
   1841             /**
   1842              * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
   1843              * BETWEEN_TWO_ITEMS.
   1844              */
   1845             Limits limitsBeforeCoordinate;
   1846 
   1847             /**
   1848              * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
   1849              */
   1850             Limits limitsAfterCoordinate;
   1851 
   1852             // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
   1853             Limits mFirstKnownItem;
   1854             // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
   1855             Limits mLastKnownItem;
   1856 
   1857             /**
   1858              * @param limitsList The sorted limits list for the coordinate type. If this
   1859              *     CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise,
   1860              *     mYLimitsList should be pased.
   1861              * @param value The coordinate value.
   1862              */
   1863             RelativeCoordinate(List<Limits> limitsList, int value) {
   1864                 int index = Collections.binarySearch(limitsList, new Limits(value, value));
   1865 
   1866                 if (index >= 0) {
   1867                     this.type = WITHIN_LIMITS;
   1868                     this.limitsBeforeCoordinate = limitsList.get(index);
   1869                 } else if (~index == 0) {
   1870                     this.type = BEFORE_FIRST_ITEM;
   1871                     this.mFirstKnownItem = limitsList.get(0);
   1872                 } else if (~index == limitsList.size()) {
   1873                     Limits lastLimits = limitsList.get(limitsList.size() - 1);
   1874                     if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
   1875                         this.type = WITHIN_LIMITS;
   1876                         this.limitsBeforeCoordinate = lastLimits;
   1877                     } else {
   1878                         this.type = AFTER_LAST_ITEM;
   1879                         this.mLastKnownItem = lastLimits;
   1880                     }
   1881                 } else {
   1882                     Limits limitsBeforeIndex = limitsList.get(~index - 1);
   1883                     if (limitsBeforeIndex.lowerLimit <= value && value <= limitsBeforeIndex.upperLimit) {
   1884                         this.type = WITHIN_LIMITS;
   1885                         this.limitsBeforeCoordinate = limitsList.get(~index - 1);
   1886                     } else {
   1887                         this.type = BETWEEN_TWO_ITEMS;
   1888                         this.limitsBeforeCoordinate = limitsList.get(~index - 1);
   1889                         this.limitsAfterCoordinate = limitsList.get(~index);
   1890                     }
   1891                 }
   1892             }
   1893 
   1894             int toComparisonValue() {
   1895                 if (type == BEFORE_FIRST_ITEM) {
   1896                     return mFirstKnownItem.lowerLimit - 1;
   1897                 } else if (type == AFTER_LAST_ITEM) {
   1898                     return mLastKnownItem.upperLimit + 1;
   1899                 } else if (type == BETWEEN_TWO_ITEMS) {
   1900                     return limitsBeforeCoordinate.upperLimit + 1;
   1901                 } else {
   1902                     return limitsBeforeCoordinate.lowerLimit;
   1903                 }
   1904             }
   1905 
   1906             @Override
   1907             public boolean equals(Object other) {
   1908                 if (!(other instanceof RelativeCoordinate)) {
   1909                     return false;
   1910                 }
   1911 
   1912                 RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
   1913                 return toComparisonValue() == otherCoordinate.toComparisonValue();
   1914             }
   1915 
   1916             @Override
   1917             public int compareTo(RelativeCoordinate other) {
   1918                 return toComparisonValue() - other.toComparisonValue();
   1919             }
   1920         }
   1921 
   1922         /**
   1923          * The location of a point relative to the Limits of nearby items; consists of both an x- and
   1924          * y-RelativeCoordinateLocation.
   1925          */
   1926         private class RelativePoint {
   1927             final RelativeCoordinate xLocation;
   1928             final RelativeCoordinate yLocation;
   1929 
   1930             RelativePoint(Point point) {
   1931                 this.xLocation = new RelativeCoordinate(mColumnBounds, point.x);
   1932                 this.yLocation = new RelativeCoordinate(mRowBounds, point.y);
   1933             }
   1934 
   1935             @Override
   1936             public boolean equals(Object other) {
   1937                 if (!(other instanceof RelativePoint)) {
   1938                     return false;
   1939                 }
   1940 
   1941                 RelativePoint otherPoint = (RelativePoint) other;
   1942                 return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation);
   1943             }
   1944         }
   1945 
   1946         /**
   1947          * Generates a rectangle which contains the items selected by the pointer and origin.
   1948          * @return The rectangle, or null if no items were selected.
   1949          */
   1950         private Rect computeBounds() {
   1951             Rect rect = new Rect();
   1952             rect.left = getCoordinateValue(
   1953                     min(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
   1954                     mColumnBounds,
   1955                     true);
   1956             rect.right = getCoordinateValue(
   1957                     max(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
   1958                     mColumnBounds,
   1959                     false);
   1960             rect.top = getCoordinateValue(
   1961                     min(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
   1962                     mRowBounds,
   1963                     true);
   1964             rect.bottom = getCoordinateValue(
   1965                     max(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
   1966                     mRowBounds,
   1967                     false);
   1968             return rect;
   1969         }
   1970 
   1971         /**
   1972          * Computes the corner of the selection nearest the origin.
   1973          * @return
   1974          */
   1975         private int computeCornerNearestOrigin() {
   1976             int cornerValue = 0;
   1977 
   1978             if (mRelativeOrigin.yLocation ==
   1979                     min(mRelativeOrigin.yLocation, mRelativePointer.yLocation)) {
   1980                 cornerValue |= UPPER;
   1981             } else {
   1982                 cornerValue |= LOWER;
   1983             }
   1984 
   1985             if (mRelativeOrigin.xLocation ==
   1986                     min(mRelativeOrigin.xLocation, mRelativePointer.xLocation)) {
   1987                 cornerValue |= LEFT;
   1988             } else {
   1989                 cornerValue |= RIGHT;
   1990             }
   1991 
   1992             return cornerValue;
   1993         }
   1994 
   1995         private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
   1996             return first.compareTo(second) < 0 ? first : second;
   1997         }
   1998 
   1999         private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) {
   2000             return first.compareTo(second) > 0 ? first : second;
   2001         }
   2002 
   2003         /**
   2004          * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
   2005          *     coordinate.
   2006          */
   2007         private int getCoordinateValue(RelativeCoordinate coordinate,
   2008                 List<Limits> limitsList, boolean isStartOfRange) {
   2009             switch (coordinate.type) {
   2010                 case RelativeCoordinate.BEFORE_FIRST_ITEM:
   2011                     return limitsList.get(0).lowerLimit;
   2012                 case RelativeCoordinate.AFTER_LAST_ITEM:
   2013                     return limitsList.get(limitsList.size() - 1).upperLimit;
   2014                 case RelativeCoordinate.BETWEEN_TWO_ITEMS:
   2015                     if (isStartOfRange) {
   2016                         return coordinate.limitsAfterCoordinate.lowerLimit;
   2017                     } else {
   2018                         return coordinate.limitsBeforeCoordinate.upperLimit;
   2019                     }
   2020                 case RelativeCoordinate.WITHIN_LIMITS:
   2021                     return coordinate.limitsBeforeCoordinate.lowerLimit;
   2022             }
   2023 
   2024             throw new RuntimeException("Invalid coordinate value.");
   2025         }
   2026 
   2027         private boolean areItemsCoveredByBand(
   2028                 RelativePoint first, RelativePoint second) {
   2029             return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) &&
   2030                     doesCoordinateLocationCoverItems(first.yLocation, second.yLocation);
   2031         }
   2032 
   2033         private boolean doesCoordinateLocationCoverItems(
   2034                 RelativeCoordinate pointerCoordinate,
   2035                 RelativeCoordinate originCoordinate) {
   2036             if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM &&
   2037                     originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
   2038                 return false;
   2039             }
   2040 
   2041             if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM &&
   2042                     originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
   2043                 return false;
   2044             }
   2045 
   2046             if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
   2047                     originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
   2048                     pointerCoordinate.limitsBeforeCoordinate.equals(
   2049                             originCoordinate.limitsBeforeCoordinate) &&
   2050                     pointerCoordinate.limitsAfterCoordinate.equals(
   2051                             originCoordinate.limitsAfterCoordinate)) {
   2052                 return false;
   2053             }
   2054 
   2055             return true;
   2056         }
   2057     }
   2058 }
   2059