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