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