| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.supportv4.app; |
| |
| //BEGIN_INCLUDE(complete) |
| |
| import android.content.ContentProvider; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.UriMatcher; |
| import android.database.Cursor; |
| import android.database.SQLException; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteOpenHelper; |
| import android.database.sqlite.SQLiteQueryBuilder; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.provider.BaseColumns; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| import android.view.View; |
| import android.widget.ListView; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.core.database.DatabaseUtilsCompat; |
| import androidx.cursoradapter.widget.SimpleCursorAdapter; |
| import androidx.fragment.app.FragmentActivity; |
| import androidx.fragment.app.FragmentManager; |
| import androidx.fragment.app.ListFragment; |
| import androidx.loader.app.LoaderManager; |
| import androidx.loader.content.CursorLoader; |
| import androidx.loader.content.Loader; |
| |
| import java.util.HashMap; |
| |
| /** |
| * Demonstration of bottom to top implementation of a content provider holding |
| * structured data through displaying it in the UI, using throttling to reduce |
| * the number of queries done when its data changes. |
| */ |
| public class LoaderThrottleSupport extends FragmentActivity { |
| // Debugging. |
| static final String TAG = "LoaderThrottle"; |
| |
| /** |
| * The authority we use to get to our sample provider. |
| */ |
| public static final String AUTHORITY = "com.example.android.apis.supportv4.app.LoaderThrottle"; |
| |
| /** |
| * Definition of the contract for the main table of our provider. |
| */ |
| public static final class MainTable implements BaseColumns { |
| |
| // This class cannot be instantiated |
| private MainTable() {} |
| |
| /** |
| * The table name offered by this provider |
| */ |
| public static final String TABLE_NAME = "main"; |
| |
| /** |
| * The content:// style URL for this table |
| */ |
| public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/main"); |
| |
| /** |
| * The content URI base for a single row of data. Callers must |
| * append a numeric row id to this Uri to retrieve a row |
| */ |
| public static final Uri CONTENT_ID_URI_BASE |
| = Uri.parse("content://" + AUTHORITY + "/main/"); |
| |
| /** |
| * The MIME type of {@link #CONTENT_URI}. |
| */ |
| public static final String CONTENT_TYPE |
| = "vnd.android.cursor.dir/vnd.example.api-demos-throttle"; |
| |
| /** |
| * The MIME type of a {@link #CONTENT_URI} sub-directory of a single row. |
| */ |
| public static final String CONTENT_ITEM_TYPE |
| = "vnd.android.cursor.item/vnd.example.api-demos-throttle"; |
| /** |
| * The default sort order for this table |
| */ |
| public static final String DEFAULT_SORT_ORDER = "data COLLATE LOCALIZED ASC"; |
| |
| /** |
| * Column name for the single column holding our data. |
| * <P>Type: TEXT</P> |
| */ |
| public static final String COLUMN_NAME_DATA = "data"; |
| } |
| |
| /** |
| * This class helps open, create, and upgrade the database file. |
| */ |
| static class DatabaseHelper extends SQLiteOpenHelper { |
| |
| private static final String DATABASE_NAME = "loader_throttle.db"; |
| private static final int DATABASE_VERSION = 2; |
| |
| DatabaseHelper(Context context) { |
| |
| // calls the super constructor, requesting the default cursor factory. |
| super(context, DATABASE_NAME, null, DATABASE_VERSION); |
| } |
| |
| /** |
| * |
| * Creates the underlying database with table name and column names taken from the |
| * NotePad class. |
| */ |
| @Override |
| public void onCreate(SQLiteDatabase db) { |
| db.execSQL("CREATE TABLE " + MainTable.TABLE_NAME + " (" |
| + MainTable._ID + " INTEGER PRIMARY KEY," |
| + MainTable.COLUMN_NAME_DATA + " TEXT" |
| + ");"); |
| } |
| |
| /** |
| * |
| * Demonstrates that the provider must consider what happens when the |
| * underlying datastore is changed. In this sample, the database is upgraded the database |
| * by destroying the existing data. |
| * A real application should upgrade the database in place. |
| */ |
| @Override |
| public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { |
| |
| // Logs that the database is being upgraded |
| Log.w(TAG, "Upgrading database from version " + oldVersion + " to " |
| + newVersion + ", which will destroy all old data"); |
| |
| // Kills the table and existing data |
| db.execSQL("DROP TABLE IF EXISTS notes"); |
| |
| // Recreates the database with a new version |
| onCreate(db); |
| } |
| } |
| |
| /** |
| * A very simple implementation of a content provider. |
| */ |
| public static class SimpleProvider extends ContentProvider { |
| // A projection map used to select columns from the database |
| private final HashMap<String, String> mNotesProjectionMap; |
| // Uri matcher to decode incoming URIs. |
| private final UriMatcher mUriMatcher; |
| |
| // The incoming URI matches the main table URI pattern |
| private static final int MAIN = 1; |
| // The incoming URI matches the main table row ID URI pattern |
| private static final int MAIN_ID = 2; |
| |
| // Handle to a new DatabaseHelper. |
| private DatabaseHelper mOpenHelper; |
| |
| /** |
| * Global provider initialization. |
| */ |
| public SimpleProvider() { |
| // Create and initialize URI matcher. |
| mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); |
| mUriMatcher.addURI(AUTHORITY, MainTable.TABLE_NAME, MAIN); |
| mUriMatcher.addURI(AUTHORITY, MainTable.TABLE_NAME + "/#", MAIN_ID); |
| |
| // Create and initialize projection map for all columns. This is |
| // simply an identity mapping. |
| mNotesProjectionMap = new HashMap<String, String>(); |
| mNotesProjectionMap.put(MainTable._ID, MainTable._ID); |
| mNotesProjectionMap.put(MainTable.COLUMN_NAME_DATA, MainTable.COLUMN_NAME_DATA); |
| } |
| |
| /** |
| * Perform provider creation. |
| */ |
| @Override |
| public boolean onCreate() { |
| mOpenHelper = new DatabaseHelper(getContext()); |
| // Assumes that any failures will be reported by a thrown exception. |
| return true; |
| } |
| |
| /** |
| * Handle incoming queries. |
| */ |
| @Override |
| public Cursor query(Uri uri, String[] projection, String selection, |
| String[] selectionArgs, String sortOrder) { |
| |
| // Constructs a new query builder and sets its table name |
| SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); |
| qb.setTables(MainTable.TABLE_NAME); |
| |
| switch (mUriMatcher.match(uri)) { |
| case MAIN: |
| // If the incoming URI is for main table. |
| qb.setProjectionMap(mNotesProjectionMap); |
| break; |
| |
| case MAIN_ID: |
| // The incoming URI is for a single row. |
| qb.setProjectionMap(mNotesProjectionMap); |
| qb.appendWhere(MainTable._ID + "=?"); |
| selectionArgs = DatabaseUtilsCompat.appendSelectionArgs(selectionArgs, |
| new String[] { uri.getLastPathSegment() }); |
| break; |
| |
| default: |
| throw new IllegalArgumentException("Unknown URI " + uri); |
| } |
| |
| |
| if (TextUtils.isEmpty(sortOrder)) { |
| sortOrder = MainTable.DEFAULT_SORT_ORDER; |
| } |
| |
| SQLiteDatabase db = mOpenHelper.getReadableDatabase(); |
| |
| Cursor c = qb.query(db, projection, selection, selectionArgs, |
| null /* no group */, null /* no filter */, sortOrder); |
| |
| c.setNotificationUri(getContext().getContentResolver(), uri); |
| return c; |
| } |
| |
| /** |
| * Return the MIME type for an known URI in the provider. |
| */ |
| @Override |
| public String getType(Uri uri) { |
| switch (mUriMatcher.match(uri)) { |
| case MAIN: |
| return MainTable.CONTENT_TYPE; |
| case MAIN_ID: |
| return MainTable.CONTENT_ITEM_TYPE; |
| default: |
| throw new IllegalArgumentException("Unknown URI " + uri); |
| } |
| } |
| |
| /** |
| * Handler inserting new data. |
| */ |
| @Override |
| public Uri insert(Uri uri, ContentValues initialValues) { |
| if (mUriMatcher.match(uri) != MAIN) { |
| // Can only insert into to main URI. |
| throw new IllegalArgumentException("Unknown URI " + uri); |
| } |
| |
| ContentValues values; |
| |
| if (initialValues != null) { |
| values = new ContentValues(initialValues); |
| } else { |
| values = new ContentValues(); |
| } |
| |
| if (values.containsKey(MainTable.COLUMN_NAME_DATA) == false) { |
| values.put(MainTable.COLUMN_NAME_DATA, ""); |
| } |
| |
| SQLiteDatabase db = mOpenHelper.getWritableDatabase(); |
| |
| long rowId = db.insert(MainTable.TABLE_NAME, null, values); |
| |
| // If the insert succeeded, the row ID exists. |
| if (rowId > 0) { |
| Uri noteUri = ContentUris.withAppendedId(MainTable.CONTENT_ID_URI_BASE, rowId); |
| getContext().getContentResolver().notifyChange(noteUri, null); |
| return noteUri; |
| } |
| |
| throw new SQLException("Failed to insert row into " + uri); |
| } |
| |
| /** |
| * Handle deleting data. |
| */ |
| @Override |
| public int delete(Uri uri, String where, String[] whereArgs) { |
| SQLiteDatabase db = mOpenHelper.getWritableDatabase(); |
| String finalWhere; |
| |
| int count; |
| |
| switch (mUriMatcher.match(uri)) { |
| case MAIN: |
| // If URI is main table, delete uses incoming where clause and args. |
| count = db.delete(MainTable.TABLE_NAME, where, whereArgs); |
| break; |
| |
| // If the incoming URI matches a single note ID, does the delete based on the |
| // incoming data, but modifies the where clause to restrict it to the |
| // particular note ID. |
| case MAIN_ID: |
| // If URI is for a particular row ID, delete is based on incoming |
| // data but modified to restrict to the given ID. |
| finalWhere = DatabaseUtilsCompat.concatenateWhere( |
| MainTable._ID + " = " + ContentUris.parseId(uri), where); |
| count = db.delete(MainTable.TABLE_NAME, finalWhere, whereArgs); |
| break; |
| |
| default: |
| throw new IllegalArgumentException("Unknown URI " + uri); |
| } |
| |
| getContext().getContentResolver().notifyChange(uri, null); |
| |
| return count; |
| } |
| |
| /** |
| * Handle updating data. |
| */ |
| @Override |
| public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { |
| SQLiteDatabase db = mOpenHelper.getWritableDatabase(); |
| int count; |
| String finalWhere; |
| |
| switch (mUriMatcher.match(uri)) { |
| case MAIN: |
| // If URI is main table, update uses incoming where clause and args. |
| count = db.update(MainTable.TABLE_NAME, values, where, whereArgs); |
| break; |
| |
| case MAIN_ID: |
| // If URI is for a particular row ID, update is based on incoming |
| // data but modified to restrict to the given ID. |
| finalWhere = DatabaseUtilsCompat.concatenateWhere( |
| MainTable._ID + " = " + ContentUris.parseId(uri), where); |
| count = db.update(MainTable.TABLE_NAME, values, finalWhere, whereArgs); |
| break; |
| |
| default: |
| throw new IllegalArgumentException("Unknown URI " + uri); |
| } |
| |
| getContext().getContentResolver().notifyChange(uri, null); |
| |
| return count; |
| } |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| FragmentManager fm = getSupportFragmentManager(); |
| |
| // Create the list fragment and add it as our sole content. |
| if (fm.findFragmentById(android.R.id.content) == null) { |
| ThrottledLoaderListFragment list = new ThrottledLoaderListFragment(); |
| fm.beginTransaction().add(android.R.id.content, list).commit(); |
| } |
| } |
| |
| public static class ThrottledLoaderListFragment extends ListFragment |
| implements LoaderManager.LoaderCallbacks<Cursor> { |
| |
| // Menu identifiers |
| static final int POPULATE_ID = Menu.FIRST; |
| static final int CLEAR_ID = Menu.FIRST+1; |
| |
| // This is the Adapter being used to display the list's data. |
| SimpleCursorAdapter mAdapter; |
| |
| // If non-null, this is the current filter the user has provided. |
| String mCurFilter; |
| |
| // Task we have running to populate the database. |
| AsyncTask<Void, Void, Void> mPopulatingTask; |
| |
| @Override |
| public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { |
| super.onViewCreated(view, savedInstanceState); |
| |
| setEmptyText("No data. Select 'Populate' to fill with data from Z to A at a rate of 4 per second."); |
| setHasOptionsMenu(true); |
| |
| // Create an empty adapter we will use to display the loaded data. |
| mAdapter = new SimpleCursorAdapter(getActivity(), |
| android.R.layout.simple_list_item_1, null, |
| new String[] { MainTable.COLUMN_NAME_DATA }, |
| new int[] { android.R.id.text1 }, 0); |
| setListAdapter(mAdapter); |
| |
| // Start out with a progress indicator. |
| setListShown(false); |
| |
| // Prepare the loader. Either re-connect with an existing one, |
| // or start a new one. |
| getLoaderManager().initLoader(0, null, this); |
| } |
| |
| @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |
| MenuItem populateItem = menu.add(Menu.NONE, POPULATE_ID, 0, "Populate"); |
| populateItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); |
| MenuItem clearItem = menu.add(Menu.NONE, CLEAR_ID, 0, "Clear"); |
| clearItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); |
| } |
| |
| @Override public boolean onOptionsItemSelected(MenuItem item) { |
| final ContentResolver cr = getActivity().getContentResolver(); |
| |
| switch (item.getItemId()) { |
| case POPULATE_ID: |
| if (mPopulatingTask != null) { |
| mPopulatingTask.cancel(false); |
| } |
| mPopulatingTask = new AsyncTask<Void, Void, Void>() { |
| @Override protected Void doInBackground(Void... params) { |
| for (char c='Z'; c>='A'; c--) { |
| if (isCancelled()) { |
| break; |
| } |
| StringBuilder builder = new StringBuilder("Data "); |
| builder.append(c); |
| ContentValues values = new ContentValues(); |
| values.put(MainTable.COLUMN_NAME_DATA, builder.toString()); |
| cr.insert(MainTable.CONTENT_URI, values); |
| // Wait a bit between each insert. |
| try { |
| Thread.sleep(250); |
| } catch (InterruptedException e) { |
| } |
| } |
| return null; |
| } |
| }; |
| mPopulatingTask.execute((Void[]) null); |
| return true; |
| |
| case CLEAR_ID: |
| if (mPopulatingTask != null) { |
| mPopulatingTask.cancel(false); |
| mPopulatingTask = null; |
| } |
| AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { |
| @Override protected Void doInBackground(Void... params) { |
| cr.delete(MainTable.CONTENT_URI, null, null); |
| return null; |
| } |
| }; |
| task.execute((Void[])null); |
| return true; |
| |
| default: |
| return super.onOptionsItemSelected(item); |
| } |
| } |
| |
| @Override public void onListItemClick(ListView l, View v, int position, long id) { |
| // Insert desired behavior here. |
| Log.i(TAG, "Item clicked: " + id); |
| } |
| |
| // These are the rows that we will retrieve. |
| static final String[] PROJECTION = new String[] { |
| MainTable._ID, |
| MainTable.COLUMN_NAME_DATA, |
| }; |
| |
| @NonNull |
| @Override |
| public Loader<Cursor> onCreateLoader(int id, Bundle args) { |
| CursorLoader cl = new CursorLoader(getActivity(), MainTable.CONTENT_URI, |
| PROJECTION, null, null, null); |
| cl.setUpdateThrottle(2000); // update at most every 2 seconds. |
| return cl; |
| } |
| |
| @Override |
| public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) { |
| mAdapter.swapCursor(data); |
| |
| // The list should now be shown. |
| if (isResumed()) { |
| setListShown(true); |
| } else { |
| setListShownNoAnimation(true); |
| } |
| } |
| |
| @Override |
| public void onLoaderReset(@NonNull Loader<Cursor> loader) { |
| mAdapter.swapCursor(null); |
| } |
| } |
| } |
| //END_INCLUDE(complete) |