1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.widget;
15
16import android.database.Cursor;
17import android.support.v17.leanback.database.CursorMapper;
18import android.util.LruCache;
19
20/**
21 * An ObjectAdapter implemented with a {@link Cursor}.
22 */
23public class CursorObjectAdapter extends ObjectAdapter {
24    private static final int CACHE_SIZE = 100;
25    private Cursor mCursor;
26    private CursorMapper mMapper;
27    private final LruCache<Integer, Object> mItemCache = new LruCache<Integer, Object>(CACHE_SIZE);
28
29    /**
30     * Construct an adapter with the given {@link PresenterSelector}.
31     */
32    public CursorObjectAdapter(PresenterSelector presenterSelector) {
33        super(presenterSelector);
34    }
35
36    /**
37     * Construct an adapter that uses the given {@link Presenter} for all items.
38     */
39    public CursorObjectAdapter(Presenter presenter) {
40        super(presenter);
41    }
42
43    /**
44     * Construct an adapter.
45     */
46    public CursorObjectAdapter() {
47        super();
48    }
49
50    /**
51     * Change the underlying cursor to a new cursor. If there is
52     * an existing cursor it will be closed if it is different than the new
53     * cursor.
54     *
55     * @param cursor The new cursor to be used.
56     */
57    public void changeCursor(Cursor cursor) {
58        if (cursor == mCursor) {
59            return;
60        }
61        if (mCursor != null) {
62            mCursor.close();
63        }
64        mCursor = cursor;
65        mItemCache.trimToSize(0);
66        onCursorChanged();
67    }
68
69    /**
70     * Swap in a new Cursor, returning the old Cursor. Unlike changeCursor(Cursor),
71     * the returned old Cursor is not closed.
72     *
73     * @param cursor The new cursor to be used.
74     */
75    public Cursor swapCursor(Cursor cursor) {
76        if (cursor == mCursor) {
77            return mCursor;
78        }
79        Cursor oldCursor = mCursor;
80        mCursor = cursor;
81        mItemCache.trimToSize(0);
82        onCursorChanged();
83        return oldCursor;
84    }
85
86    /**
87     * Called whenever the cursor changes.
88     */
89    protected void onCursorChanged() {
90        notifyChanged();
91    }
92
93    /**
94     * Gets the {@link Cursor} backing the adapter.
95     */
96     public final Cursor getCursor() {
97        return mCursor;
98    }
99
100    /**
101     * Sets the {@link CursorMapper} used to convert {@link Cursor} rows into
102     * Objects.
103     */
104    public final void setMapper(CursorMapper mapper) {
105        boolean changed = mMapper != mapper;
106        mMapper = mapper;
107
108        if (changed) {
109            onMapperChanged();
110        }
111    }
112
113    /**
114     * Called when {@link #setMapper(CursorMapper)} is called and a different
115     * mapper is provided.
116     */
117    protected void onMapperChanged() {
118    }
119
120    /**
121     * Gets the {@link CursorMapper} used to convert {@link Cursor} rows into
122     * Objects.
123     */
124    public final CursorMapper getMapper() {
125        return mMapper;
126    }
127
128    @Override
129    public int size() {
130        if (mCursor == null) {
131            return 0;
132        }
133        return mCursor.getCount();
134    }
135
136    @Override
137    public Object get(int index) {
138        if (mCursor == null) {
139            return null;
140        }
141        if (!mCursor.moveToPosition(index)) {
142            throw new ArrayIndexOutOfBoundsException();
143        }
144        Object item = mItemCache.get(index);
145        if (item != null) {
146            return item;
147        }
148        item = mMapper.convert(mCursor);
149        mItemCache.put(index, item);
150        return item;
151    }
152
153    /**
154     * Closes this adapter, closing the backing {@link Cursor} as well.
155     */
156    public void close() {
157        if (mCursor != null) {
158            mCursor.close();
159            mCursor = null;
160        }
161    }
162
163    /**
164     * Checks whether the adapter, and hence the backing {@link Cursor}, is closed.
165     */
166    public boolean isClosed() {
167        return mCursor == null || mCursor.isClosed();
168    }
169
170    /**
171     * Remove an item from the cache. This will force the item to be re-read
172     * from the data source the next time (@link #get(int)} is called.
173     */
174    protected final void invalidateCache(int index) {
175        mItemCache.remove(index);
176    }
177
178    /**
179     * Remove {@code count} items starting at {@code index}.
180     */
181    protected final void invalidateCache(int index, int count) {
182        for (int limit = count + index; index < limit; index++) {
183            invalidateCache(index);
184        }
185    }
186}
187