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