1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.messaging.ui;
18 
19 import android.content.Context;
20 import android.database.ContentObserver;
21 import android.database.Cursor;
22 import android.database.DataSetObserver;
23 import android.os.Handler;
24 import androidx.recyclerview.widget.RecyclerView;
25 import android.util.Log;
26 import android.view.ViewGroup;
27 import 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  */
35 public 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      */
CursorRecyclerAdapter(final Context context, final Cursor c, final int flags)109     public CursorRecyclerAdapter(final Context context, final Cursor c, final int flags) {
110         init(context, c, flags);
111     }
112 
init(final Context context, final Cursor c, int flags)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      */
getCursor()147     public Cursor getCursor() {
148         return mCursor;
149     }
150 
151     @Override
getItemCount()152     public int getItemCount() {
153         if (mDataValid && mCursor != null) {
154             return mCursor.getCount();
155         } else {
156             return 0;
157         }
158     }
159 
160     /**
161      * @see androidx.recyclerview.widget.RecyclerView.Adapter#getItem(int)
162      */
getItem(final int position)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 androidx.recyclerview.widget.RecyclerView.Adapter#getItemId(int)
174      */
175     @Override
getItemId(final int position)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
onCreateViewHolder(final ViewGroup parent, final int viewType)189     public VH onCreateViewHolder(final ViewGroup parent, final int viewType) {
190         return createViewHolder(mContext, parent, viewType);
191     }
192 
193     @Override
onBindViewHolder(final VH holder, final int position)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      */
bindViewHolder(VH holder, Context context, Cursor cursor)210     public abstract void bindViewHolder(VH holder, Context context, Cursor cursor);
211 
212     /**
213      * @see androidx.recyclerview.widget.RecyclerView.Adapter#createViewHolder(Context, ViewGroup, int)
214      */
createViewHolder(Context context, ViewGroup parent, int viewType)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      */
changeCursor(final Cursor cursor)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      */
swapCursor(final Cursor newCursor)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      */
convertToString(final Cursor cursor)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      */
onContentChanged()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 {
ChangeObserver()304         public ChangeObserver() {
305             super(new Handler());
306         }
307 
308         @Override
deliverSelfNotifications()309         public boolean deliverSelfNotifications() {
310             return true;
311         }
312 
313         @Override
onChange(final boolean selfChange)314         public void onChange(final boolean selfChange) {
315             onContentChanged();
316         }
317     }
318 
319     private class MyDataSetObserver extends DataSetObserver {
320         @Override
onChanged()321         public void onChanged() {
322             mDataValid = true;
323             notifyDataSetChanged();
324         }
325 
326         @Override
onInvalidated()327         public void onInvalidated() {
328             mDataValid = false;
329             notifyDataSetChanged();
330         }
331     }
332 
333 }
334