1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.messaging.ui;
18
19import android.content.Context;
20import android.database.ContentObserver;
21import android.database.Cursor;
22import android.database.DataSetObserver;
23import android.os.Handler;
24import android.support.v7.widget.RecyclerView;
25import android.util.Log;
26import android.view.ViewGroup;
27import android.widget.FilterQueryProvider;
28
29/**
30 * Copy of CursorAdapter suited for RecyclerView.
31 *
32 * TODO: BUG 16327984. Replace this with a framework supported CursorAdapter for
33 * RecyclerView when one is available.
34 */
35public abstract class CursorRecyclerAdapter<VH extends RecyclerView.ViewHolder>
36        extends RecyclerView.Adapter<VH> {
37    /**
38     * This field should be made private, so it is hidden from the SDK.
39     * {@hide}
40     */
41    protected boolean mDataValid;
42    /**
43     * This field should be made private, so it is hidden from the SDK.
44     * {@hide}
45     */
46    protected boolean mAutoRequery;
47    /**
48     * This field should be made private, so it is hidden from the SDK.
49     * {@hide}
50     */
51    protected Cursor mCursor;
52    /**
53     * This field should be made private, so it is hidden from the SDK.
54     * {@hide}
55     */
56    protected Context mContext;
57    /**
58     * This field should be made private, so it is hidden from the SDK.
59     * {@hide}
60     */
61    protected int mRowIDColumn;
62    /**
63     * This field should be made private, so it is hidden from the SDK.
64     * {@hide}
65     */
66    protected ChangeObserver mChangeObserver;
67    /**
68     * This field should be made private, so it is hidden from the SDK.
69     * {@hide}
70     */
71    protected DataSetObserver mDataSetObserver;
72    /**
73     * This field should be made private, so it is hidden from the SDK.
74     * {@hide}
75     */
76    protected FilterQueryProvider mFilterQueryProvider;
77
78    /**
79     * If set the adapter will call requery() on the cursor whenever a content change
80     * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
81     *
82     * @deprecated This option is discouraged, as it results in Cursor queries
83     * being performed on the application's UI thread and thus can cause poor
84     * responsiveness or even Application Not Responding errors.  As an alternative,
85     * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
86     */
87    @Deprecated
88    public static final int FLAG_AUTO_REQUERY = 0x01;
89
90    /**
91     * If set the adapter will register a content observer on the cursor and will call
92     * {@link #onContentChanged()} when a notification comes in.  Be careful when
93     * using this flag: you will need to unset the current Cursor from the adapter
94     * to avoid leaks due to its registered observers.  This flag is not needed
95     * when using a CursorAdapter with a
96     * {@link android.content.CursorLoader}.
97     */
98    public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
99
100    /**
101     * Recommended constructor.
102     *
103     * @param c The cursor from which to get the data.
104     * @param context The context
105     * @param flags Flags used to determine the behavior of the adapter; may
106     * be any combination of {@link #FLAG_AUTO_REQUERY} and
107     * {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
108     */
109    public CursorRecyclerAdapter(final Context context, final Cursor c, final int flags) {
110        init(context, c, flags);
111    }
112
113    void init(final Context context, final Cursor c, int flags) {
114        if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
115            flags |= FLAG_REGISTER_CONTENT_OBSERVER;
116            mAutoRequery = true;
117        } else {
118            mAutoRequery = false;
119        }
120        final boolean cursorPresent = c != null;
121        mCursor = c;
122        mDataValid = cursorPresent;
123        mContext = context;
124        mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
125        if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
126            mChangeObserver = new ChangeObserver();
127            mDataSetObserver = new MyDataSetObserver();
128        } else {
129            mChangeObserver = null;
130            mDataSetObserver = null;
131        }
132
133        if (cursorPresent) {
134            if (mChangeObserver != null) {
135                c.registerContentObserver(mChangeObserver);
136            }
137            if (mDataSetObserver != null) {
138                c.registerDataSetObserver(mDataSetObserver);
139            }
140        }
141    }
142
143    /**
144     * Returns the cursor.
145     * @return the cursor.
146     */
147    public Cursor getCursor() {
148        return mCursor;
149    }
150
151    @Override
152    public int getItemCount() {
153        if (mDataValid && mCursor != null) {
154            return mCursor.getCount();
155        } else {
156            return 0;
157        }
158    }
159
160    /**
161     * @see android.support.v7.widget.RecyclerView.Adapter#getItem(int)
162     */
163    public Object getItem(final int position) {
164        if (mDataValid && mCursor != null) {
165            mCursor.moveToPosition(position);
166            return mCursor;
167        } else {
168            return null;
169        }
170    }
171
172    /**
173     * @see android.support.v7.widget.RecyclerView.Adapter#getItemId(int)
174     */
175    @Override
176    public long getItemId(final int position) {
177        if (mDataValid && mCursor != null) {
178            if (mCursor.moveToPosition(position)) {
179                return mCursor.getLong(mRowIDColumn);
180            } else {
181                return 0;
182            }
183        } else {
184            return 0;
185        }
186    }
187
188    @Override
189    public VH onCreateViewHolder(final ViewGroup parent, final int viewType) {
190        return createViewHolder(mContext, parent, viewType);
191    }
192
193    @Override
194    public void onBindViewHolder(final VH holder, final int position) {
195        if (!mDataValid) {
196            throw new IllegalStateException("this should only be called when the cursor is valid");
197        }
198        if (!mCursor.moveToPosition(position)) {
199            throw new IllegalStateException("couldn't move cursor to position " + position);
200        }
201        bindViewHolder(holder, mContext, mCursor);
202    }
203    /**
204     * Bind an existing view to the data pointed to by cursor
205     * @param view Existing view, returned earlier by newView
206     * @param context Interface to application's global information
207     * @param cursor The cursor from which to get the data. The cursor is already
208     * moved to the correct position.
209     */
210    public abstract void bindViewHolder(VH holder, Context context, Cursor cursor);
211
212    /**
213     * @see android.support.v7.widget.RecyclerView.Adapter#createViewHolder(Context, ViewGroup, int)
214     */
215    public abstract VH createViewHolder(Context context, ViewGroup parent, int viewType);
216
217    /**
218     * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
219     * closed.
220     *
221     * @param cursor The new cursor to be used
222     */
223    public void changeCursor(final Cursor cursor) {
224        final Cursor old = swapCursor(cursor);
225        if (old != null) {
226            old.close();
227        }
228    }
229
230    /**
231     * Swap in a new Cursor, returning the old Cursor.  Unlike
232     * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
233     * closed.
234     *
235     * @param newCursor The new cursor to be used.
236     * @return Returns the previously set Cursor, or null if there wasa not one.
237     * If the given new Cursor is the same instance is the previously set
238     * Cursor, null is also returned.
239     */
240    public Cursor swapCursor(final Cursor newCursor) {
241        if (newCursor == mCursor) {
242            return null;
243        }
244        final Cursor oldCursor = mCursor;
245        if (oldCursor != null) {
246            if (mChangeObserver != null) {
247                oldCursor.unregisterContentObserver(mChangeObserver);
248            }
249            if (mDataSetObserver != null) {
250                oldCursor.unregisterDataSetObserver(mDataSetObserver);
251            }
252        }
253        mCursor = newCursor;
254        if (newCursor != null) {
255            if (mChangeObserver != null) {
256                newCursor.registerContentObserver(mChangeObserver);
257            }
258            if (mDataSetObserver != null) {
259                newCursor.registerDataSetObserver(mDataSetObserver);
260            }
261            mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
262            mDataValid = true;
263            // notify the observers about the new cursor
264            notifyDataSetChanged();
265        } else {
266            mRowIDColumn = -1;
267            mDataValid = false;
268            // notify the observers about the lack of a data set
269            notifyDataSetChanged();
270        }
271        return oldCursor;
272    }
273
274    /**
275     * <p>Converts the cursor into a CharSequence. Subclasses should override this
276     * method to convert their results. The default implementation returns an
277     * empty String for null values or the default String representation of
278     * the value.</p>
279     *
280     * @param cursor the cursor to convert to a CharSequence
281     * @return a CharSequence representing the value
282     */
283    public CharSequence convertToString(final Cursor cursor) {
284        return cursor == null ? "" : cursor.toString();
285    }
286
287    /**
288     * Called when the {@link ContentObserver} on the cursor receives a change notification.
289     * The default implementation provides the auto-requery logic, but may be overridden by
290     * sub classes.
291     *
292     * @see ContentObserver#onChange(boolean)
293     */
294    protected void onContentChanged() {
295        if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
296            if (false) {
297                Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
298            }
299            mDataValid = mCursor.requery();
300        }
301    }
302
303    private class ChangeObserver extends ContentObserver {
304        public ChangeObserver() {
305            super(new Handler());
306        }
307
308        @Override
309        public boolean deliverSelfNotifications() {
310            return true;
311        }
312
313        @Override
314        public void onChange(final boolean selfChange) {
315            onContentChanged();
316        }
317    }
318
319    private class MyDataSetObserver extends DataSetObserver {
320        @Override
321        public void onChanged() {
322            mDataValid = true;
323            notifyDataSetChanged();
324        }
325
326        @Override
327        public void onInvalidated() {
328            mDataValid = false;
329            notifyDataSetChanged();
330        }
331    }
332
333}
334