1 /*
2  * Copyright (C) 2011 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.example.android.supportv4.app;
18 
19 //BEGIN_INCLUDE(complete)
20 
21 import android.content.ContentProvider;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.UriMatcher;
27 import android.database.Cursor;
28 import android.database.SQLException;
29 import android.database.sqlite.SQLiteDatabase;
30 import android.database.sqlite.SQLiteOpenHelper;
31 import android.database.sqlite.SQLiteQueryBuilder;
32 import android.net.Uri;
33 import android.os.AsyncTask;
34 import android.os.Bundle;
35 import android.provider.BaseColumns;
36 import android.text.TextUtils;
37 import android.util.Log;
38 import android.view.Menu;
39 import android.view.MenuInflater;
40 import android.view.MenuItem;
41 import android.view.View;
42 import android.widget.ListView;
43 
44 import androidx.annotation.NonNull;
45 import androidx.core.database.DatabaseUtilsCompat;
46 import androidx.cursoradapter.widget.SimpleCursorAdapter;
47 import androidx.fragment.app.FragmentActivity;
48 import androidx.fragment.app.FragmentManager;
49 import androidx.fragment.app.ListFragment;
50 import androidx.loader.app.LoaderManager;
51 import androidx.loader.content.CursorLoader;
52 import androidx.loader.content.Loader;
53 
54 import java.util.HashMap;
55 
56 /**
57  * Demonstration of bottom to top implementation of a content provider holding
58  * structured data through displaying it in the UI, using throttling to reduce
59  * the number of queries done when its data changes.
60  */
61 public class LoaderThrottleSupport extends FragmentActivity {
62     // Debugging.
63     static final String TAG = "LoaderThrottle";
64 
65     /**
66      * The authority we use to get to our sample provider.
67      */
68     public static final String AUTHORITY = "com.example.android.apis.supportv4.app.LoaderThrottle";
69 
70     /**
71      * Definition of the contract for the main table of our provider.
72      */
73     public static final class MainTable implements BaseColumns {
74 
75         // This class cannot be instantiated
MainTable()76         private MainTable() {}
77 
78         /**
79          * The table name offered by this provider
80          */
81         public static final String TABLE_NAME = "main";
82 
83         /**
84          * The content:// style URL for this table
85          */
86         public static final Uri CONTENT_URI =  Uri.parse("content://" + AUTHORITY + "/main");
87 
88         /**
89          * The content URI base for a single row of data. Callers must
90          * append a numeric row id to this Uri to retrieve a row
91          */
92         public static final Uri CONTENT_ID_URI_BASE
93                 = Uri.parse("content://" + AUTHORITY + "/main/");
94 
95         /**
96          * The MIME type of {@link #CONTENT_URI}.
97          */
98         public static final String CONTENT_TYPE
99                 = "vnd.android.cursor.dir/vnd.example.api-demos-throttle";
100 
101         /**
102          * The MIME type of a {@link #CONTENT_URI} sub-directory of a single row.
103          */
104         public static final String CONTENT_ITEM_TYPE
105                 = "vnd.android.cursor.item/vnd.example.api-demos-throttle";
106         /**
107          * The default sort order for this table
108          */
109         public static final String DEFAULT_SORT_ORDER = "data COLLATE LOCALIZED ASC";
110 
111         /**
112          * Column name for the single column holding our data.
113          * <P>Type: TEXT</P>
114          */
115         public static final String COLUMN_NAME_DATA = "data";
116     }
117 
118     /**
119      * This class helps open, create, and upgrade the database file.
120      */
121    static class DatabaseHelper extends SQLiteOpenHelper {
122 
123        private static final String DATABASE_NAME = "loader_throttle.db";
124        private static final int DATABASE_VERSION = 2;
125 
DatabaseHelper(Context context)126        DatabaseHelper(Context context) {
127 
128            // calls the super constructor, requesting the default cursor factory.
129            super(context, DATABASE_NAME, null, DATABASE_VERSION);
130        }
131 
132        /**
133         *
134         * Creates the underlying database with table name and column names taken from the
135         * NotePad class.
136         */
137        @Override
onCreate(SQLiteDatabase db)138        public void onCreate(SQLiteDatabase db) {
139            db.execSQL("CREATE TABLE " + MainTable.TABLE_NAME + " ("
140                    + MainTable._ID + " INTEGER PRIMARY KEY,"
141                    + MainTable.COLUMN_NAME_DATA + " TEXT"
142                    + ");");
143        }
144 
145        /**
146         *
147         * Demonstrates that the provider must consider what happens when the
148         * underlying datastore is changed. In this sample, the database is upgraded the database
149         * by destroying the existing data.
150         * A real application should upgrade the database in place.
151         */
152        @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)153        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
154 
155            // Logs that the database is being upgraded
156            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
157                    + newVersion + ", which will destroy all old data");
158 
159            // Kills the table and existing data
160            db.execSQL("DROP TABLE IF EXISTS notes");
161 
162            // Recreates the database with a new version
163            onCreate(db);
164        }
165    }
166 
167     /**
168      * A very simple implementation of a content provider.
169      */
170     public static class SimpleProvider extends ContentProvider {
171         // A projection map used to select columns from the database
172         private final HashMap<String, String> mNotesProjectionMap;
173         // Uri matcher to decode incoming URIs.
174         private final UriMatcher mUriMatcher;
175 
176         // The incoming URI matches the main table URI pattern
177         private static final int MAIN = 1;
178         // The incoming URI matches the main table row ID URI pattern
179         private static final int MAIN_ID = 2;
180 
181         // Handle to a new DatabaseHelper.
182         private DatabaseHelper mOpenHelper;
183 
184         /**
185          * Global provider initialization.
186          */
SimpleProvider()187         public SimpleProvider() {
188             // Create and initialize URI matcher.
189             mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
190             mUriMatcher.addURI(AUTHORITY, MainTable.TABLE_NAME, MAIN);
191             mUriMatcher.addURI(AUTHORITY, MainTable.TABLE_NAME + "/#", MAIN_ID);
192 
193             // Create and initialize projection map for all columns.  This is
194             // simply an identity mapping.
195             mNotesProjectionMap = new HashMap<String, String>();
196             mNotesProjectionMap.put(MainTable._ID, MainTable._ID);
197             mNotesProjectionMap.put(MainTable.COLUMN_NAME_DATA, MainTable.COLUMN_NAME_DATA);
198         }
199 
200         /**
201          * Perform provider creation.
202          */
203         @Override
onCreate()204         public boolean onCreate() {
205             mOpenHelper = new DatabaseHelper(getContext());
206             // Assumes that any failures will be reported by a thrown exception.
207             return true;
208         }
209 
210         /**
211          * Handle incoming queries.
212          */
213         @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)214         public Cursor query(Uri uri, String[] projection, String selection,
215                 String[] selectionArgs, String sortOrder) {
216 
217             // Constructs a new query builder and sets its table name
218             SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
219             qb.setTables(MainTable.TABLE_NAME);
220 
221             switch (mUriMatcher.match(uri)) {
222                 case MAIN:
223                     // If the incoming URI is for main table.
224                     qb.setProjectionMap(mNotesProjectionMap);
225                     break;
226 
227                 case MAIN_ID:
228                     // The incoming URI is for a single row.
229                     qb.setProjectionMap(mNotesProjectionMap);
230                     qb.appendWhere(MainTable._ID + "=?");
231                     selectionArgs = DatabaseUtilsCompat.appendSelectionArgs(selectionArgs,
232                             new String[] { uri.getLastPathSegment() });
233                     break;
234 
235                 default:
236                     throw new IllegalArgumentException("Unknown URI " + uri);
237             }
238 
239 
240             if (TextUtils.isEmpty(sortOrder)) {
241                 sortOrder = MainTable.DEFAULT_SORT_ORDER;
242             }
243 
244             SQLiteDatabase db = mOpenHelper.getReadableDatabase();
245 
246             Cursor c = qb.query(db, projection, selection, selectionArgs,
247                     null /* no group */, null /* no filter */, sortOrder);
248 
249             c.setNotificationUri(getContext().getContentResolver(), uri);
250             return c;
251         }
252 
253         /**
254          * Return the MIME type for an known URI in the provider.
255          */
256         @Override
getType(Uri uri)257         public String getType(Uri uri) {
258             switch (mUriMatcher.match(uri)) {
259                 case MAIN:
260                     return MainTable.CONTENT_TYPE;
261                 case MAIN_ID:
262                     return MainTable.CONTENT_ITEM_TYPE;
263                 default:
264                     throw new IllegalArgumentException("Unknown URI " + uri);
265             }
266         }
267 
268         /**
269          * Handler inserting new data.
270          */
271         @Override
insert(Uri uri, ContentValues initialValues)272         public Uri insert(Uri uri, ContentValues initialValues) {
273             if (mUriMatcher.match(uri) != MAIN) {
274                 // Can only insert into to main URI.
275                 throw new IllegalArgumentException("Unknown URI " + uri);
276             }
277 
278             ContentValues values;
279 
280             if (initialValues != null) {
281                 values = new ContentValues(initialValues);
282             } else {
283                 values = new ContentValues();
284             }
285 
286             if (values.containsKey(MainTable.COLUMN_NAME_DATA) == false) {
287                 values.put(MainTable.COLUMN_NAME_DATA, "");
288             }
289 
290             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
291 
292             long rowId = db.insert(MainTable.TABLE_NAME, null, values);
293 
294             // If the insert succeeded, the row ID exists.
295             if (rowId > 0) {
296                 Uri noteUri = ContentUris.withAppendedId(MainTable.CONTENT_ID_URI_BASE, rowId);
297                 getContext().getContentResolver().notifyChange(noteUri, null);
298                 return noteUri;
299             }
300 
301             throw new SQLException("Failed to insert row into " + uri);
302         }
303 
304         /**
305          * Handle deleting data.
306          */
307         @Override
delete(Uri uri, String where, String[] whereArgs)308         public int delete(Uri uri, String where, String[] whereArgs) {
309             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
310             String finalWhere;
311 
312             int count;
313 
314             switch (mUriMatcher.match(uri)) {
315                 case MAIN:
316                     // If URI is main table, delete uses incoming where clause and args.
317                     count = db.delete(MainTable.TABLE_NAME, where, whereArgs);
318                     break;
319 
320                     // If the incoming URI matches a single note ID, does the delete based on the
321                     // incoming data, but modifies the where clause to restrict it to the
322                     // particular note ID.
323                 case MAIN_ID:
324                     // If URI is for a particular row ID, delete is based on incoming
325                     // data but modified to restrict to the given ID.
326                     finalWhere = DatabaseUtilsCompat.concatenateWhere(
327                             MainTable._ID + " = " + ContentUris.parseId(uri), where);
328                     count = db.delete(MainTable.TABLE_NAME, finalWhere, whereArgs);
329                     break;
330 
331                 default:
332                     throw new IllegalArgumentException("Unknown URI " + uri);
333             }
334 
335             getContext().getContentResolver().notifyChange(uri, null);
336 
337             return count;
338         }
339 
340         /**
341          * Handle updating data.
342          */
343         @Override
update(Uri uri, ContentValues values, String where, String[] whereArgs)344         public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
345             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
346             int count;
347             String finalWhere;
348 
349             switch (mUriMatcher.match(uri)) {
350                 case MAIN:
351                     // If URI is main table, update uses incoming where clause and args.
352                     count = db.update(MainTable.TABLE_NAME, values, where, whereArgs);
353                     break;
354 
355                 case MAIN_ID:
356                     // If URI is for a particular row ID, update is based on incoming
357                     // data but modified to restrict to the given ID.
358                     finalWhere = DatabaseUtilsCompat.concatenateWhere(
359                             MainTable._ID + " = " + ContentUris.parseId(uri), where);
360                     count = db.update(MainTable.TABLE_NAME, values, finalWhere, whereArgs);
361                     break;
362 
363                 default:
364                     throw new IllegalArgumentException("Unknown URI " + uri);
365             }
366 
367             getContext().getContentResolver().notifyChange(uri, null);
368 
369             return count;
370         }
371     }
372 
373     @Override
onCreate(Bundle savedInstanceState)374     protected void onCreate(Bundle savedInstanceState) {
375         super.onCreate(savedInstanceState);
376 
377         FragmentManager fm = getSupportFragmentManager();
378 
379         // Create the list fragment and add it as our sole content.
380         if (fm.findFragmentById(android.R.id.content) == null) {
381             ThrottledLoaderListFragment list = new ThrottledLoaderListFragment();
382             fm.beginTransaction().add(android.R.id.content, list).commit();
383         }
384     }
385 
386     public static class ThrottledLoaderListFragment extends ListFragment
387             implements LoaderManager.LoaderCallbacks<Cursor> {
388 
389         // Menu identifiers
390         static final int POPULATE_ID = Menu.FIRST;
391         static final int CLEAR_ID = Menu.FIRST+1;
392 
393         // This is the Adapter being used to display the list's data.
394         SimpleCursorAdapter mAdapter;
395 
396         // If non-null, this is the current filter the user has provided.
397         String mCurFilter;
398 
399         // Task we have running to populate the database.
400         AsyncTask<Void, Void, Void> mPopulatingTask;
401 
onActivityCreated(Bundle savedInstanceState)402         @Override public void onActivityCreated(Bundle savedInstanceState) {
403             super.onActivityCreated(savedInstanceState);
404 
405             setEmptyText("No data.  Select 'Populate' to fill with data from Z to A at a rate of 4 per second.");
406             setHasOptionsMenu(true);
407 
408             // Create an empty adapter we will use to display the loaded data.
409             mAdapter = new SimpleCursorAdapter(getActivity(),
410                     android.R.layout.simple_list_item_1, null,
411                     new String[] { MainTable.COLUMN_NAME_DATA },
412                     new int[] { android.R.id.text1 }, 0);
413             setListAdapter(mAdapter);
414 
415             // Start out with a progress indicator.
416             setListShown(false);
417 
418             // Prepare the loader.  Either re-connect with an existing one,
419             // or start a new one.
420             getLoaderManager().initLoader(0, null, this);
421         }
422 
onCreateOptionsMenu(Menu menu, MenuInflater inflater)423         @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
424             MenuItem populateItem = menu.add(Menu.NONE, POPULATE_ID, 0, "Populate");
425             populateItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
426             MenuItem clearItem = menu.add(Menu.NONE, CLEAR_ID, 0, "Clear");
427             clearItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
428         }
429 
onOptionsItemSelected(MenuItem item)430         @Override public boolean onOptionsItemSelected(MenuItem item) {
431             final ContentResolver cr = getActivity().getContentResolver();
432 
433             switch (item.getItemId()) {
434                 case POPULATE_ID:
435                     if (mPopulatingTask != null) {
436                         mPopulatingTask.cancel(false);
437                     }
438                     mPopulatingTask = new AsyncTask<Void, Void, Void>() {
439                         @Override protected Void doInBackground(Void... params) {
440                             for (char c='Z'; c>='A'; c--) {
441                                 if (isCancelled()) {
442                                     break;
443                                 }
444                                 StringBuilder builder = new StringBuilder("Data ");
445                                 builder.append(c);
446                                 ContentValues values = new ContentValues();
447                                 values.put(MainTable.COLUMN_NAME_DATA, builder.toString());
448                                 cr.insert(MainTable.CONTENT_URI, values);
449                                 // Wait a bit between each insert.
450                                 try {
451                                     Thread.sleep(250);
452                                 } catch (InterruptedException e) {
453                                 }
454                             }
455                             return null;
456                         }
457                     };
458                     mPopulatingTask.execute((Void[]) null);
459                     return true;
460 
461                 case CLEAR_ID:
462                     if (mPopulatingTask != null) {
463                         mPopulatingTask.cancel(false);
464                         mPopulatingTask = null;
465                     }
466                     AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
467                         @Override protected Void doInBackground(Void... params) {
468                             cr.delete(MainTable.CONTENT_URI, null, null);
469                             return null;
470                         }
471                     };
472                     task.execute((Void[])null);
473                     return true;
474 
475                 default:
476                     return super.onOptionsItemSelected(item);
477             }
478         }
479 
onListItemClick(ListView l, View v, int position, long id)480         @Override public void onListItemClick(ListView l, View v, int position, long id) {
481             // Insert desired behavior here.
482             Log.i(TAG, "Item clicked: " + id);
483         }
484 
485         // These are the rows that we will retrieve.
486         static final String[] PROJECTION = new String[] {
487             MainTable._ID,
488             MainTable.COLUMN_NAME_DATA,
489         };
490 
491         @NonNull
492         @Override
onCreateLoader(int id, Bundle args)493         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
494             CursorLoader cl = new CursorLoader(getActivity(), MainTable.CONTENT_URI,
495                     PROJECTION, null, null, null);
496             cl.setUpdateThrottle(2000); // update at most every 2 seconds.
497             return cl;
498         }
499 
500         @Override
onLoadFinished(@onNull Loader<Cursor> loader, Cursor data)501         public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
502             mAdapter.swapCursor(data);
503 
504             // The list should now be shown.
505             if (isResumed()) {
506                 setListShown(true);
507             } else {
508                 setListShownNoAnimation(true);
509             }
510         }
511 
512         @Override
onLoaderReset(@onNull Loader<Cursor> loader)513         public void onLoaderReset(@NonNull Loader<Cursor> loader) {
514             mAdapter.swapCursor(null);
515         }
516     }
517 }
518 //END_INCLUDE(complete)
519