14f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay/*
24f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * Copyright (C) 2015 The Android Open Source Project
34f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay *
44f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * Licensed under the Apache License, Version 2.0 (the "License");
54f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * you may not use this file except in compliance with the License.
64f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * You may obtain a copy of the License at
74f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay *
84f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay *      http://www.apache.org/licenses/LICENSE-2.0
94f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay *
104f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * Unless required by applicable law or agreed to in writing, software
114f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * distributed under the License is distributed on an "AS IS" BASIS,
124f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
134f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * See the License for the specific language governing permissions and
144f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * limitations under the License.
154f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay */
164f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
174f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKaypackage com.android.documentsui.selection;
184f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
194f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKayimport static com.android.documentsui.base.Shared.DEBUG;
204f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
214f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKayimport android.annotation.IntDef;
22ed895580275101312d7d6fa6ebf78b79b4905a1eJon Mannimport android.support.annotation.VisibleForTesting;
234f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKayimport android.support.v7.widget.RecyclerView;
244f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKayimport android.util.Log;
254f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
264f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKayimport com.android.documentsui.dirlist.DocumentsAdapter;
274f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
284f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKayimport java.lang.annotation.Retention;
294f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKayimport java.lang.annotation.RetentionPolicy;
304f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKayimport java.util.ArrayList;
314f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKayimport java.util.List;
324f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
334f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKayimport javax.annotation.Nullable;
344f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
354f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay/**
364f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * MultiSelectManager provides support traditional multi-item selection support to RecyclerView.
374f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * Additionally it can be configured to restrict selection to a single element, @see
384f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay * #setSelectMode.
394f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay */
404f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKaypublic final class SelectionManager {
414f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
424f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    @IntDef(flag = true, value = {
434f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            MODE_MULTIPLE,
444f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            MODE_SINGLE
454f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    })
464f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    @Retention(RetentionPolicy.SOURCE)
474f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public @interface SelectionMode {}
484f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public static final int MODE_MULTIPLE = 0;
494f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public static final int MODE_SINGLE = 1;
504f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
514f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    @IntDef({
524f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            RANGE_REGULAR,
534f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            RANGE_PROVISIONAL
544f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    })
554f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    @Retention(RetentionPolicy.SOURCE)
564f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public @interface RangeType {}
574f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public static final int RANGE_REGULAR = 0;
584f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public static final int RANGE_PROVISIONAL = 1;
594f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
604f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    static final String TAG = "SelectionManager";
614f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
624f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private final Selection mSelection = new Selection();
634f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
644f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private final List<Callback> mCallbacks = new ArrayList<>(1);
654f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private final List<ItemCallback> mItemCallbacks = new ArrayList<>(1);
664f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
674f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private @Nullable DocumentsAdapter mAdapter;
684f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private @Nullable Range mRanger;
694f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private boolean mSingleSelect;
704f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
714f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private RecyclerView.AdapterDataObserver mAdapterObserver;
724f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private SelectionPredicate mCanSetState;
734f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
744f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public SelectionManager(@SelectionMode int mode) {
754f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mSingleSelect = mode == MODE_SINGLE;
764f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
774f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
784f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public SelectionManager reset(DocumentsAdapter adapter, SelectionPredicate canSetState) {
794f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
804f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mCallbacks.clear();
814f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mItemCallbacks.clear();
824f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (mAdapter != null && mAdapterObserver != null) {
834f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            mAdapter.unregisterAdapterDataObserver(mAdapterObserver);
844f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
854f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
864f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        clearSelectionQuietly();
874f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
884f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        assert(adapter != null);
894f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        assert(canSetState != null);
904f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
914f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mAdapter = adapter;
924f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mCanSetState = canSetState;
934f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
944f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mAdapterObserver = new RecyclerView.AdapterDataObserver() {
954f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
964f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            private List<String> mModelIds;
974f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
984f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            @Override
994f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            public void onChanged() {
1004f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                mModelIds = mAdapter.getModelIds();
1014f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1024f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                // Update the selection to remove any disappeared IDs.
1034f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                mSelection.cancelProvisionalSelection();
1044f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                mSelection.intersect(mModelIds);
105ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann
106ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann                notifyDataChanged();
1074f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            }
1084f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1094f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            @Override
1104f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            public void onItemRangeChanged(
1114f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                    int startPosition, int itemCount, Object payload) {
1124f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                // No change in position. Ignoring.
1134f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            }
1144f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1154f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            @Override
1164f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            public void onItemRangeInserted(int startPosition, int itemCount) {
1174f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                mSelection.cancelProvisionalSelection();
1184f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            }
1194f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1204f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            @Override
1214f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            public void onItemRangeRemoved(int startPosition, int itemCount) {
1224f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                assert(startPosition >= 0);
1234f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                assert(itemCount > 0);
1244f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1254f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                mSelection.cancelProvisionalSelection();
1264f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                // Remove any disappeared IDs from the selection.
1274f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                mSelection.intersect(mModelIds);
1284f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            }
1294f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1304f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            @Override
1314f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
1324f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                throw new UnsupportedOperationException();
1334f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            }
1344f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        };
1354f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1364f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mAdapter.registerAdapterDataObserver(mAdapterObserver);
1374f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        return this;
1384f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
1394f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1404f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    void bindContoller(BandController controller) {
1414f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        // Provides BandController with access to private mSelection state.
1424f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        controller.bindSelection(mSelection);
1434f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
1444f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1454f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
1464f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Adds {@code callback} such that it will be notified when {@code MultiSelectManager}
1474f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * events occur.
1484f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     *
1494f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param callback
1504f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
1514f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public void addCallback(Callback callback) {
1524f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        assert(callback != null);
1534f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mCallbacks.add(callback);
1544f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
1554f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1564f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public void addItemCallback(ItemCallback itemCallback) {
1574f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        assert(itemCallback != null);
1584f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mItemCallbacks.add(itemCallback);
1594f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
1604f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1614f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public boolean hasSelection() {
1624f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        return !mSelection.isEmpty();
1634f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
1644f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1654f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
1664f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Returns a Selection object that provides a live view
1674f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * on the current selection.
1684f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     *
1694f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @see #getSelection(Selection) on how to get a snapshot
1704f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     *     of the selection that will not reflect future changes
1714f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     *     to selection.
1724f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     *
1734f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @return The current selection.
1744f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
1754f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public Selection getSelection() {
1764f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        return mSelection;
1774f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
1784f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1794f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
1804f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Updates {@code dest} to reflect the current selection.
1814f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param dest
1824f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     *
1834f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @return The Selection instance passed in, for convenience.
1844f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
1854f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public Selection getSelection(Selection dest) {
1864f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        dest.copyFrom(mSelection);
1874f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        return dest;
1884f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
1894f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
190ed895580275101312d7d6fa6ebf78b79b4905a1eJon Mann    @VisibleForTesting
1914f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public void replaceSelection(Iterable<String> ids) {
1924f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        clearSelection();
1934f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        setItemsSelected(ids, true);
1944f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
1954f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
1964f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
1974f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Restores the selected state of specified items. Used in cases such as restore the selection
1984f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * after rotation etc.
1994f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
2004f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public void restoreSelection(Selection other) {
2014f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        setItemsSelectedQuietly(other.mSelection, true);
2024f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        // NOTE: We intentionally don't restore provisional selection. It's provisional.
2034f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        notifySelectionRestored();
2044f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
2054f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2064f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
2074f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Sets the selected state of the specified items. Note that the callback will NOT
2084f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * be consulted to see if an item can be selected.
2094f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     *
2104f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param ids
2114f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param selected
2124f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @return
2134f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
2144f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public boolean setItemsSelected(Iterable<String> ids, boolean selected) {
2154f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        final boolean changed = setItemsSelectedQuietly(ids, selected);
2164f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        notifySelectionChanged();
2174f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        return changed;
2184f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
2194f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2204f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private boolean setItemsSelectedQuietly(Iterable<String> ids, boolean selected) {
2214f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        boolean changed = false;
2224f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        for (String id: ids) {
2234f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            final boolean itemChanged =
2244f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                    selected
2254f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                    ? canSetState(id, true) && mSelection.add(id)
2264f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                    : canSetState(id, false) && mSelection.remove(id);
2274f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            if (itemChanged) {
2284f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                notifyItemStateChanged(id, selected);
2294f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            }
2304f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            changed |= itemChanged;
2314f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
2324f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        return changed;
2334f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
2344f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2354f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
2364f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Clears the selection and notifies (if something changes).
2374f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
2384f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public void clearSelection() {
2394f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (!hasSelection()) {
2404f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            return;
2414f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
2424f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2434f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        clearSelectionQuietly();
2444f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        notifySelectionChanged();
2454f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
2464f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2474f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
2484f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Clears the selection, without notifying selection listeners. UI elements still need to be
2494f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * notified about state changes so that they can update their appearance.
2504f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
2514f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private void clearSelectionQuietly() {
2524f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mRanger = null;
2534f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2544f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (!hasSelection()) {
2554f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            return;
2564f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
2574f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2584f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        Selection oldSelection = getSelection(new Selection());
2594f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mSelection.clear();
2604f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2614f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        for (String id: oldSelection.mSelection) {
2624f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            notifyItemStateChanged(id, false);
2634f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
2644f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        for (String id: oldSelection.mProvisionalSelection) {
2654f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            notifyItemStateChanged(id, false);
2664f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
2674f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
2684f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2694f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
2704f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Toggles selection on the item with the given model ID.
2714f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     *
2724f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param modelId
2734f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
2744f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public void toggleSelection(String modelId) {
2754f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        assert(modelId != null);
2764f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2774f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        final boolean changed = mSelection.contains(modelId)
2784f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                ? attemptDeselect(modelId)
2794f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                : attemptSelect(modelId);
2804f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2814f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (changed) {
2824f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            notifySelectionChanged();
2834f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
2844f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
2854f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2864f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
2874f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Starts a range selection. If a range selection is already active, this will start a new range
2884f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * selection (which will reset the range anchor).
2894f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     *
2904f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param pos The anchor position for the selection range.
2914f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
2924f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public void startRangeSelection(int pos) {
2934f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        attemptSelect(mAdapter.getModelId(pos));
2944f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        setSelectionRangeBegin(pos);
2954f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
2964f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
2974f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public void snapRangeSelection(int pos) {
2984f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        snapRangeSelection(pos, RANGE_REGULAR);
2994f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
3004f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
3014f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    void snapProvisionalRangeSelection(int pos) {
3024f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        snapRangeSelection(pos, RANGE_PROVISIONAL);
3034f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
3044f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
30575b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin    /*
30675b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin     * Starts and extends range selection in one go. This assumes item at startPos is not selected
30775b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin     * beforehand.
30875b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin     */
30975b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin    public void formNewSelectionRange(int startPos, int endPos) {
31075b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        assert(!mSelection.contains(mAdapter.getModelId(startPos)));
31175b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        startRangeSelection(startPos);
31275b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        snapRangeSelection(endPos);
31375b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin    }
31475b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin
3154f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
3164f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Sets the end point for the current range selection, started by a call to
3174f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * {@link #startRangeSelection(int)}. This function should only be called when a range selection
3184f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * is active (see {@link #isRangeSelectionActive()}. Items in the range [anchor, end] will be
3194f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * selected or in provisional select, depending on the type supplied. Note that if the type is
3204f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * provisional select, one should do {@link Selection#applyProvisionalSelection()} at some point
3214f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * before calling on {@link #endRangeSelection()}.
3224f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     *
3234f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param pos The new end position for the selection range.
3244f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param type The type of selection the range should utilize.
3254f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
3264f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private void snapRangeSelection(int pos, @RangeType int type) {
3274f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (!isRangeSelectionActive()) {
3284f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            throw new IllegalStateException("Range start point not set.");
3294f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
3304f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
3314f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mRanger.snapSelection(pos, type);
3324f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
3334f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        // We're being lazy here notifying even when something might not have changed.
3344f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        // To make this more correct, we'd need to update the Ranger class to return
3354f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        // information about what has changed.
3364f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        notifySelectionChanged();
3374f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
3384f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
339f7df715eb33216405ccd338056b876b02d3911e8Garfield Tan    void cancelProvisionalSelection() {
340f7df715eb33216405ccd338056b876b02d3911e8Garfield Tan        for (String id : mSelection.mProvisionalSelection) {
341f7df715eb33216405ccd338056b876b02d3911e8Garfield Tan            notifyItemStateChanged(id, false);
342f7df715eb33216405ccd338056b876b02d3911e8Garfield Tan        }
343f7df715eb33216405ccd338056b876b02d3911e8Garfield Tan        mSelection.cancelProvisionalSelection();
344f7df715eb33216405ccd338056b876b02d3911e8Garfield Tan    }
345f7df715eb33216405ccd338056b876b02d3911e8Garfield Tan
3464f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
3474f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Stops an in-progress range selection. All selection done with
3484f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * {@link #snapRangeSelection(int, int)} with type RANGE_PROVISIONAL will be lost if
3494f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * {@link Selection#applyProvisionalSelection()} is not called beforehand.
3504f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
3514f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public void endRangeSelection() {
3524f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mRanger = null;
3534f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        // Clean up in case there was any leftover provisional selection
354f7df715eb33216405ccd338056b876b02d3911e8Garfield Tan        cancelProvisionalSelection();
3554f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
3564f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
3574f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
3584f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @return Whether or not there is a current range selection active.
3594f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
3604f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public boolean isRangeSelectionActive() {
3614f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        return mRanger != null;
3624f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
3634f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
3644f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
3654f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Sets the magic location at which a selection range begins (the selection anchor). This value
3664f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * is consulted when determining how to extend, and modify selection ranges. Calling this when a
3674f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * range selection is active will reset the range selection.
3684f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
3694f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public void setSelectionRangeBegin(int position) {
3704f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (position == RecyclerView.NO_POSITION) {
3714f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            return;
3724f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
3734f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
3744f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (mSelection.contains(mAdapter.getModelId(position))) {
3754f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            mRanger = new Range(this::updateForRange, position);
3764f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
3774f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
3784f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
3794f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
3804f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param modelId
3814f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @return True if the update was applied.
3824f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
3834f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private boolean selectAndNotify(String modelId) {
3844f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        boolean changed = mSelection.add(modelId);
3854f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (changed) {
3864f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            notifyItemStateChanged(modelId, true);
3874f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
3884f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        return changed;
3894f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
3904f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
3914f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
3924f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param id
3934f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @return True if the update was applied.
3944f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
3954f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private boolean attemptDeselect(String id) {
3964f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        assert(id != null);
3974f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (canSetState(id, false)) {
3984f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            mSelection.remove(id);
3994f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            notifyItemStateChanged(id, false);
400ed895580275101312d7d6fa6ebf78b79b4905a1eJon Mann
401ed895580275101312d7d6fa6ebf78b79b4905a1eJon Mann            // if there's nothing in the selection and there is an active ranger it results
402ed895580275101312d7d6fa6ebf78b79b4905a1eJon Mann            // in unexpected behavior when the user tries to start range selection: the item
403ed895580275101312d7d6fa6ebf78b79b4905a1eJon Mann            // which the ranger 'thinks' is the already selected anchor becomes unselectable
404ed895580275101312d7d6fa6ebf78b79b4905a1eJon Mann            if (mSelection.isEmpty() && isRangeSelectionActive()) {
405ed895580275101312d7d6fa6ebf78b79b4905a1eJon Mann                endRangeSelection();
406ed895580275101312d7d6fa6ebf78b79b4905a1eJon Mann            }
4074f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection);
4084f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            return true;
4094f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        } else {
4104f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            if (DEBUG) Log.d(TAG, "Select cancelled by listener.");
4114f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            return false;
4124f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
4134f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
4144f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
4154f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
4164f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @param id
4174f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * @return True if the update was applied.
4184f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
4194f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private boolean attemptSelect(String id) {
4204f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        assert(id != null);
4214f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        boolean canSelect = canSetState(id, true);
4224f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (!canSelect) {
4234f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            return false;
4244f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
4254f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        if (mSingleSelect && hasSelection()) {
4264f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            clearSelectionQuietly();
4274f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
4284f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
4294f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        selectAndNotify(id);
4304f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        return true;
4314f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
4324f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
4334f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    boolean canSetState(String id, boolean nextState) {
4344f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        return mCanSetState.test(id, nextState);
4354f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
4364f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
437ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann    private void notifyDataChanged() {
438b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan        final int lastListener = mItemCallbacks.size() - 1;
439b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan
440b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan        for (int i = lastListener; i >= 0; i--) {
441b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan            mItemCallbacks.get(i).onSelectionReset();
442b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan        }
443b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan
444ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann        for (String id : mSelection) {
445ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann            if (!canSetState(id, true)) {
446ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann                attemptDeselect(id);
447ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann            } else {
448ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann                for (int i = lastListener; i >= 0; i--) {
449ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann                    mItemCallbacks.get(i).onItemStateChanged(id, true);
450ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann                }
451ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann            }
452ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann        }
453ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann    }
454ee9d85ca6b8ed9f330951eeadfc1107caa8f55a2Jon Mann
4554f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
4564f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Notifies registered listeners when the selection status of a single item
4574f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * (identified by {@code position}) changes.
4584f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
4594f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    void notifyItemStateChanged(String id, boolean selected) {
4604f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        assert(id != null);
4614f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        int lastListener = mItemCallbacks.size() - 1;
4624f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        for (int i = lastListener; i >= 0; i--) {
4634f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            mItemCallbacks.get(i).onItemStateChanged(id, selected);
4644f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
4654f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        mAdapter.onItemSelectionChanged(id);
4664f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
4674f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
4684f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    /**
4694f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * Notifies registered listeners when the selection has changed. This
4704f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * notification should be sent only once a full series of changes
4714f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * is complete, e.g. clearingSelection, or updating the single
4724f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     * selection from one item to another.
4734f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay     */
4744f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    void notifySelectionChanged() {
4754f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        int lastListener = mCallbacks.size() - 1;
4764f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        for (int i = lastListener; i > -1; i--) {
4774f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            mCallbacks.get(i).onSelectionChanged();
4784f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
4794f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
4804f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
4814f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private void notifySelectionRestored() {
4824f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        int lastListener = mCallbacks.size() - 1;
4834f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        for (int i = lastListener; i > -1; i--) {
4844f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            mCallbacks.get(i).onSelectionRestored();
4854f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
4864f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
4874f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
4884f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    void updateForRange(int begin, int end, boolean selected, @RangeType int type) {
4894f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        switch (type) {
4904f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            case RANGE_REGULAR:
4914f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                updateForRegularRange(begin, end, selected);
4924f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                break;
4934f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            case RANGE_PROVISIONAL:
4944f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                updateForProvisionalRange(begin, end, selected);
4954f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                break;
4964f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            default:
4974f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                throw new IllegalArgumentException("Invalid range type: " + type);
4984f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
4994f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
5004f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
5014f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private void updateForRegularRange(int begin, int end, boolean selected) {
5024f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        assert(end >= begin);
5034f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        for (int i = begin; i <= end; i++) {
5044f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            String id = mAdapter.getModelId(i);
5054f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            if (id == null) {
5064f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                continue;
5074f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            }
5084f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
5094f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            if (selected) {
5104f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                boolean canSelect = canSetState(id, true);
5114f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                if (canSelect) {
5124f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                    if (mSingleSelect && hasSelection()) {
5134f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                        clearSelectionQuietly();
5144f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                    }
5154f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                    selectAndNotify(id);
5164f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                }
5174f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            } else {
5184f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                attemptDeselect(id);
5194f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            }
5204f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
5214f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
5224f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
5234f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    private void updateForProvisionalRange(int begin, int end, boolean selected) {
5244f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        assert (end >= begin);
5254f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        for (int i = begin; i <= end; i++) {
5264f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            String id = mAdapter.getModelId(i);
5274f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            if (id == null) {
5284f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                continue;
5294f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            }
530b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan
531b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan            boolean changedState = false;
5324f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            if (selected) {
5334f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                boolean canSelect = canSetState(id, true);
53495422be069897f419e7c6ca03eb21259540372d2Jon Mann                if (canSelect && !mSelection.mSelection.contains(id)) {
5354f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                    mSelection.mProvisionalSelection.add(id);
536b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan                    changedState = true;
5374f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                }
5384f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            } else {
5394f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay                mSelection.mProvisionalSelection.remove(id);
540b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan                changedState = true;
541b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan            }
542b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan
543b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan            // Only notify item callbacks when something's state is actually changed in provisional
544b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan            // selection.
545b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan            if (changedState) {
546b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan                notifyItemStateChanged(id, selected);
5474f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay            }
5484f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        }
5494f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        notifySelectionChanged();
5504f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
5514f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
5524f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public interface ItemCallback {
5534f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        void onItemStateChanged(String id, boolean selected);
554b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan
555b8874fd0df55113d472c6704b91bd493c577caebGarfield Tan        void onSelectionReset();
5564f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
5574f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
5584f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public interface Callback {
5594f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        /**
5604f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay         * Called immediately after completion of any set of changes.
5614f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay         */
5624f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        void onSelectionChanged();
5634f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
5644f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        /**
5654f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay         * Called immediately after selection is restored.
5664f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay         */
5674f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        void onSelectionRestored();
5684f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
5694f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay
5704f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    @FunctionalInterface
5714f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    public interface SelectionPredicate {
5724f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay        boolean test(String id, boolean nextState);
5734f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay    }
5744f78ba643270b9d84da1952d8e408220b25ec6fdSteve McKay}
575