1e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik/*
2bdc4c86d3dff74f6634a38e2f7b316b0e823a2c8Alan Viverette * Copyright 2018 The Android Open Source Project
3e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik *
4e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik * Licensed under the Apache License, Version 2.0 (the "License");
5e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik * you may not use this file except in compliance with the License.
6e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik * You may obtain a copy of the License at
7e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik *
8e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik *      http://www.apache.org/licenses/LICENSE-2.0
9e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik *
10e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik * Unless required by applicable law or agreed to in writing, software
11e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik * distributed under the License is distributed on an "AS IS" BASIS,
12e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik * See the License for the specific language governing permissions and
14e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik * limitations under the License.
15e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik */
16e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
17bdc4c86d3dff74f6634a38e2f7b316b0e823a2c8Alan Viverettepackage androidx.paging;
18e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
19bdc4c86d3dff74f6634a38e2f7b316b0e823a2c8Alan Viveretteimport androidx.annotation.NonNull;
20bdc4c86d3dff74f6634a38e2f7b316b0e823a2c8Alan Viveretteimport androidx.annotation.Nullable;
21e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
22e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craikimport java.util.AbstractList;
23e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craikimport java.util.ArrayList;
24e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craikimport java.util.List;
25e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
265dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craikfinal class PagedStorage<T> extends AbstractList<T> {
275dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    /**
285dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik     * Lists instances are compared (with instance equality) to PLACEHOLDER_LIST to check if an item
295dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik     * in that position is already loading. We use a singleton placeholder list that is distinct
305dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik     * from Collections.EMPTY_LIST for safety.
315dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik     */
325dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
335dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    private static final List PLACEHOLDER_LIST = new ArrayList();
345dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik
35e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    // Always set
36e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    private int mLeadingNullCount;
37e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    /**
38e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     * List of pages in storage.
39e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     *
40e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     * Two storage modes:
41e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     *
42e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     * Contiguous - all content in mPages is valid and loaded, but may return false from isTiled().
43e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     *     Safe to access any item in any page.
44e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     *
45e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     * Non-contiguous - mPages may have nulls or a placeholder page, isTiled() always returns true.
46e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     *     mPages may have nulls, or placeholder (empty) pages while content is loading.
47e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     */
485dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    private final ArrayList<List<T>> mPages;
49e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    private int mTrailingNullCount;
50e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
51e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    private int mPositionOffset;
52e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    /**
53e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     * Number of items represented by {@link #mPages}. If tiling is enabled, unloaded items in
54e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     * {@link #mPages} may be null, but this value still counts them.
55e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     */
56e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    private int mStorageCount;
57e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
58e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    // If mPageSize > 0, tiling is enabled, 'mPages' may have gaps, and leadingPages is set
59e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    private int mPageSize;
60e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
61e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    private int mNumberPrepended;
62e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    private int mNumberAppended;
63e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
64e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    PagedStorage() {
65e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mLeadingNullCount = 0;
66e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPages = new ArrayList<>();
67e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mTrailingNullCount = 0;
68e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPositionOffset = 0;
69e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mStorageCount = 0;
70e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPageSize = 1;
71e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mNumberPrepended = 0;
72e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mNumberAppended = 0;
73e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
74e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
755dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    PagedStorage(int leadingNulls, List<T> page, int trailingNulls) {
76e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        this();
77e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        init(leadingNulls, page, trailingNulls, 0);
78e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
79e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
805dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    private PagedStorage(PagedStorage<T> other) {
81e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mLeadingNullCount = other.mLeadingNullCount;
82e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPages = new ArrayList<>(other.mPages);
83e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mTrailingNullCount = other.mTrailingNullCount;
84e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPositionOffset = other.mPositionOffset;
85e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mStorageCount = other.mStorageCount;
86e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPageSize = other.mPageSize;
87e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mNumberPrepended = other.mNumberPrepended;
88e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mNumberAppended = other.mNumberAppended;
89e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
90e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
915dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    PagedStorage<T> snapshot() {
92e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return new PagedStorage<>(this);
93e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
94e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
955dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    private void init(int leadingNulls, List<T> page, int trailingNulls, int positionOffset) {
96e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mLeadingNullCount = leadingNulls;
97e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPages.clear();
98e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPages.add(page);
99e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mTrailingNullCount = trailingNulls;
100e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
101e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPositionOffset = positionOffset;
1025dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        mStorageCount = page.size();
103e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
104e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        // initialized as tiled. There may be 3 nulls, 2 items, but we still call this tiled
105e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        // even if it will break if nulls convert.
1065dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        mPageSize = page.size();
107e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
108e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mNumberPrepended = 0;
109e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mNumberAppended = 0;
110e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
111e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
1125dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    void init(int leadingNulls, @NonNull List<T> page, int trailingNulls, int positionOffset,
113e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            @NonNull Callback callback) {
114e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        init(leadingNulls, page, trailingNulls, positionOffset);
115e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        callback.onInitialized(size());
116e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
117e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
118e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    @Override
1195dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    public T get(int i) {
120e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (i < 0 || i >= size()) {
121e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size());
122e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
123e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
124e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        // is it definitely outside 'mPages'?
125e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int localIndex = i - mLeadingNullCount;
126e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (localIndex < 0 || localIndex >= mStorageCount) {
127e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            return null;
128e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
129e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
130e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int localPageIndex;
131e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int pageInternalIndex;
132e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
133e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (isTiled()) {
134e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // it's inside mPages, and we're tiled. Jump to correct tile.
135e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            localPageIndex = localIndex / mPageSize;
136e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            pageInternalIndex = localIndex % mPageSize;
137e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        } else {
138e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // it's inside mPages, but page sizes aren't regular. Walk to correct tile.
139e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // Pages can only be null while tiled, so accessing page count is safe.
140e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            pageInternalIndex = localIndex;
141e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            final int localPageCount = mPages.size();
142e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            for (localPageIndex = 0; localPageIndex < localPageCount; localPageIndex++) {
1435dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik                int pageSize = mPages.get(localPageIndex).size();
144e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                if (pageSize > pageInternalIndex) {
145e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                    // stop, found the page
146e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                    break;
147e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                }
148e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                pageInternalIndex -= pageSize;
149e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
150e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
151e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
1525dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        List<T> page = mPages.get(localPageIndex);
1535dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        if (page == null || page.size() == 0) {
154e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // can only occur in tiled case, with untouched inner/placeholder pages
155e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            return null;
156e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
1575dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        return page.get(pageInternalIndex);
158e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
159e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
160e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    /**
161e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     * Returns true if all pages are the same size, except for the last, which may be smaller
162e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik     */
163e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    boolean isTiled() {
164e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return mPageSize > 0;
165e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
166e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
167e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    int getLeadingNullCount() {
168e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return mLeadingNullCount;
169e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
170e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
171e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    int getTrailingNullCount() {
172e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return mTrailingNullCount;
173e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
174e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
175e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    int getStorageCount() {
176e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return mStorageCount;
177e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
178e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
179e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    int getNumberAppended() {
180e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return mNumberAppended;
181e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
182e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
183e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    int getNumberPrepended() {
184e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return mNumberPrepended;
185e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
186e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
187e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    int getPageCount() {
188e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return mPages.size();
189e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
190e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
191e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    interface Callback {
192e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        void onInitialized(int count);
193e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        void onPagePrepended(int leadingNulls, int changed, int added);
194e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        void onPageAppended(int endPosition, int changed, int added);
195e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        void onPagePlaceholderInserted(int pageIndex);
196e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        void onPageInserted(int start, int count);
197e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
198e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
199e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    int getPositionOffset() {
200e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return mPositionOffset;
201e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
202e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
203e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    @Override
204e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    public int size() {
205e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return mLeadingNullCount + mStorageCount + mTrailingNullCount;
206e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
207e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
208e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    int computeLeadingNulls() {
209e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int total = mLeadingNullCount;
210e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        final int pageCount = mPages.size();
211e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        for (int i = 0; i < pageCount; i++) {
2125dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            List page = mPages.get(i);
2135dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            if (page != null && page != PLACEHOLDER_LIST) {
214e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                break;
215e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
216e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            total += mPageSize;
217e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
218e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return total;
219e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
220e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
221e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    int computeTrailingNulls() {
222e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int total = mTrailingNullCount;
223e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        for (int i = mPages.size() - 1; i >= 0; i--) {
2245dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            List page = mPages.get(i);
2255dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            if (page != null && page != PLACEHOLDER_LIST) {
226e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                break;
227e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
228e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            total += mPageSize;
229e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
230e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return total;
231e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
232e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
233e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    // ---------------- Contiguous API -------------------
234e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
2355dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    T getFirstLoadedItem() {
236e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        // safe to access first page's first item here:
237e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty
2385dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        return mPages.get(0).get(0);
239e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
240e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
2415dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    T getLastLoadedItem() {
242e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        // safe to access last page's last item here:
243e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty
2445dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        List<T> page = mPages.get(mPages.size() - 1);
2455dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        return page.get(page.size() - 1);
246e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
247e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
2485dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    void prependPage(@NonNull List<T> page, @NonNull Callback callback) {
2495dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        final int count = page.size();
250e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (count == 0) {
251e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // Nothing returned from source, stop loading in this direction
252e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            return;
253e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
254e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (mPageSize > 0 && count != mPageSize) {
255e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            if (mPages.size() == 1 && count > mPageSize) {
256e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                // prepending to a single item - update current page size to that of 'inner' page
257e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                mPageSize = count;
258e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            } else {
259e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                // no longer tiled
260e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                mPageSize = -1;
261e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
262e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
263e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
264e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPages.add(0, page);
265e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mStorageCount += count;
266e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
267e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        final int changedCount = Math.min(mLeadingNullCount, count);
268e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        final int addedCount = count - changedCount;
269e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
270e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (changedCount != 0) {
271e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            mLeadingNullCount -= changedCount;
272e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
273e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPositionOffset -= addedCount;
274e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mNumberPrepended += count;
275e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
276e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        callback.onPagePrepended(mLeadingNullCount, changedCount, addedCount);
277e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
278e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
2795dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    void appendPage(@NonNull List<T> page, @NonNull Callback callback) {
2805dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        final int count = page.size();
281e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (count == 0) {
282e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // Nothing returned from source, stop loading in this direction
283e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            return;
284e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
285e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
286e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (mPageSize > 0) {
287e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // if the previous page was smaller than mPageSize,
288e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // or if this page is larger than the previous, disable tiling
2895dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            if (mPages.get(mPages.size() - 1).size() != mPageSize
290e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                    || count > mPageSize) {
291e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                mPageSize = -1;
292e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
293e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
294e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
295e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPages.add(page);
296e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mStorageCount += count;
297e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
298e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        final int changedCount = Math.min(mTrailingNullCount, count);
299e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        final int addedCount = count - changedCount;
300e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
301e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (changedCount != 0) {
302e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            mTrailingNullCount -= changedCount;
303e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
304e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mNumberAppended += count;
305e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        callback.onPageAppended(mLeadingNullCount + mStorageCount - count,
306e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                changedCount, addedCount);
307e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
308e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
309e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    // ------------------ Non-Contiguous API (tiling required) ----------------------
310e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
3115dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    void initAndSplit(int leadingNulls, @NonNull List<T> multiPageList,
3125dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            int trailingNulls, int positionOffset, int pageSize, @NonNull Callback callback) {
3135dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik
3145dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        int pageCount = (multiPageList.size() + (pageSize - 1)) / pageSize;
3155dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        for (int i = 0; i < pageCount; i++) {
3165dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            int beginInclusive = i * pageSize;
3175dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            int endExclusive = Math.min(multiPageList.size(), (i + 1) * pageSize);
3185dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik
3195dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            List<T> sublist = multiPageList.subList(beginInclusive, endExclusive);
3205dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik
3215dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            if (i == 0) {
3225dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik                // Trailing nulls for first page includes other pages in multiPageList
3235dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik                int initialTrailingNulls = trailingNulls + multiPageList.size() - sublist.size();
3245dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik                init(leadingNulls, sublist, initialTrailingNulls, positionOffset);
3255dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            } else {
3265dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik                int insertPosition = leadingNulls + beginInclusive;
3275dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik                insertPage(insertPosition, sublist, null);
3285dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            }
3295dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        }
3305dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        callback.onInitialized(size());
3315dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    }
3325dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik
3335dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik    public void insertPage(int position, @NonNull List<T> page, @Nullable Callback callback) {
3345dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        final int newPageSize = page.size();
335e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (newPageSize != mPageSize) {
336e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // differing page size is OK in 2 cases, when the page is being added:
337e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // 1) to the end (in which case, ignore new smaller size)
338e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // 2) only the last page has been added so far (in which case, adopt new bigger size)
339e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
340e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            int size = size();
341e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            boolean addingLastPage = position == (size - size % mPageSize)
342e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                    && newPageSize < mPageSize;
343e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            boolean onlyEndPagePresent = mTrailingNullCount == 0 && mPages.size() == 1
344e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                    && newPageSize > mPageSize;
345e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
346e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            // OK only if existing single page, and it's the last one
347e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            if (!onlyEndPagePresent && !addingLastPage) {
348e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                throw new IllegalArgumentException("page introduces incorrect tiling");
349e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
350e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            if (onlyEndPagePresent) {
351e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                mPageSize = newPageSize;
352e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
353e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
354e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
355e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int pageIndex = position / mPageSize;
356e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
357e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        allocatePageRange(pageIndex, pageIndex);
358e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
359e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int localPageIndex = pageIndex - mLeadingNullCount / mPageSize;
360e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
3615dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        List<T> oldPage = mPages.get(localPageIndex);
3625dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        if (oldPage != null && oldPage != PLACEHOLDER_LIST) {
363e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            throw new IllegalArgumentException(
364e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                    "Invalid position " + position + ": data already loaded");
365e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
366e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        mPages.set(localPageIndex, page);
3675dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        if (callback != null) {
3685dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik            callback.onPageInserted(position, page.size());
369e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
370e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
371e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
372e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    private void allocatePageRange(final int minimumPage, final int maximumPage) {
373e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int leadingNullPages = mLeadingNullCount / mPageSize;
374e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
375e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (minimumPage < leadingNullPages) {
376e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            for (int i = 0; i < leadingNullPages - minimumPage; i++) {
377e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                mPages.add(0, null);
378e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
379e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            int newStorageAllocated = (leadingNullPages - minimumPage) * mPageSize;
380e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            mStorageCount += newStorageAllocated;
381e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            mLeadingNullCount -= newStorageAllocated;
382e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
383e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            leadingNullPages = minimumPage;
384e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
385e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (maximumPage >= leadingNullPages + mPages.size()) {
386e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            int newStorageAllocated = Math.min(mTrailingNullCount,
387e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                    (maximumPage + 1 - (leadingNullPages + mPages.size())) * mPageSize);
388e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            for (int i = mPages.size(); i <= maximumPage - leadingNullPages; i++) {
389e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                mPages.add(mPages.size(), null);
390e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
391e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            mStorageCount += newStorageAllocated;
392e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            mTrailingNullCount -= newStorageAllocated;
393e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
394e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
395e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
396e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    public void allocatePlaceholders(int index, int prefetchDistance,
397e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            int pageSize, Callback callback) {
398e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (pageSize != mPageSize) {
399e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            if (pageSize < mPageSize) {
400e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                throw new IllegalArgumentException("Page size cannot be reduced");
401e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
402e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            if (mPages.size() != 1 || mTrailingNullCount != 0) {
403e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                // not in single, last page allocated case - can't change page size
404e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                throw new IllegalArgumentException(
405e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                        "Page size can change only if last page is only one present");
406e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
407e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            mPageSize = pageSize;
408e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
409e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
410e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
411e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
412e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
413e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
414e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        allocatePageRange(minimumPage, maximumPage);
415e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int leadingNullPages = mLeadingNullCount / mPageSize;
416e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
417e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            int localPageIndex = pageIndex - leadingNullPages;
418e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            if (mPages.get(localPageIndex) == null) {
4195dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik                //noinspection unchecked
4205dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik                mPages.set(localPageIndex, PLACEHOLDER_LIST);
421e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                callback.onPagePlaceholderInserted(pageIndex);
422e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            }
423e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
424e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
425e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
426e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    public boolean hasPage(int pageSize, int index) {
427e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        // NOTE: we pass pageSize here to avoid in case mPageSize
428e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        // not fully initialized (when last page only one loaded)
429e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        int leadingNullPages = mLeadingNullCount / pageSize;
430e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
431e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        if (index < leadingNullPages || index >= leadingNullPages + mPages.size()) {
432e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            return false;
433e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
434e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
4355dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        List<T> page = mPages.get(index - leadingNullPages);
436e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
4375dc2fd49c2887578d8b76a9014e1b43d088c7fdaChris Craik        return page != null && page != PLACEHOLDER_LIST;
438e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
439e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
440e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    @Override
441e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    public String toString() {
442e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        StringBuilder ret = new StringBuilder("leading " + mLeadingNullCount
443e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                + ", storage " + mStorageCount
444e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik                + ", trailing " + getTrailingNullCount());
445e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik
446e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        for (int i = 0; i < mPages.size(); i++) {
447e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik            ret.append(" ").append(mPages.get(i));
448e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        }
449e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik        return ret.toString();
450e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik    }
451e1178edf8a3082ca7dde8477bb43d001f67db11aChris Craik}
452