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) 20 21import android.content.ContentProvider; 22import android.content.ContentResolver; 23import android.content.ContentUris; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.UriMatcher; 27import android.database.Cursor; 28import android.database.SQLException; 29import android.database.sqlite.SQLiteDatabase; 30import android.database.sqlite.SQLiteOpenHelper; 31import android.database.sqlite.SQLiteQueryBuilder; 32import android.net.Uri; 33import android.os.AsyncTask; 34import android.os.Bundle; 35import android.provider.BaseColumns; 36import android.support.v4.app.FragmentActivity; 37import android.support.v4.app.FragmentManager; 38import android.support.v4.app.ListFragment; 39import android.support.v4.app.LoaderManager; 40import android.support.v4.content.CursorLoader; 41import android.support.v4.content.Loader; 42import android.support.v4.database.DatabaseUtilsCompat; 43import android.support.v4.widget.SimpleCursorAdapter; 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.supportv4.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 = DatabaseUtilsCompat.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 = DatabaseUtilsCompat.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 = DatabaseUtilsCompat.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 // Start out with a progress indicator. 414 setListShown(false); 415 416 // Prepare the loader. Either re-connect with an existing one, 417 // or start a new one. 418 getLoaderManager().initLoader(0, null, this); 419 } 420 421 @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 422 MenuItem populateItem = menu.add(Menu.NONE, POPULATE_ID, 0, "Populate"); 423 populateItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 424 MenuItem clearItem = menu.add(Menu.NONE, CLEAR_ID, 0, "Clear"); 425 clearItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 426 } 427 428 @Override public boolean onOptionsItemSelected(MenuItem item) { 429 final ContentResolver cr = getActivity().getContentResolver(); 430 431 switch (item.getItemId()) { 432 case POPULATE_ID: 433 if (mPopulatingTask != null) { 434 mPopulatingTask.cancel(false); 435 } 436 mPopulatingTask = new AsyncTask<Void, Void, Void>() { 437 @Override protected Void doInBackground(Void... params) { 438 for (char c='Z'; c>='A'; c--) { 439 if (isCancelled()) { 440 break; 441 } 442 StringBuilder builder = new StringBuilder("Data "); 443 builder.append(c); 444 ContentValues values = new ContentValues(); 445 values.put(MainTable.COLUMN_NAME_DATA, builder.toString()); 446 cr.insert(MainTable.CONTENT_URI, values); 447 // Wait a bit between each insert. 448 try { 449 Thread.sleep(250); 450 } catch (InterruptedException e) { 451 } 452 } 453 return null; 454 } 455 }; 456 mPopulatingTask.execute((Void[]) null); 457 return true; 458 459 case CLEAR_ID: 460 if (mPopulatingTask != null) { 461 mPopulatingTask.cancel(false); 462 mPopulatingTask = null; 463 } 464 AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { 465 @Override protected Void doInBackground(Void... params) { 466 cr.delete(MainTable.CONTENT_URI, null, null); 467 return null; 468 } 469 }; 470 task.execute((Void[])null); 471 return true; 472 473 default: 474 return super.onOptionsItemSelected(item); 475 } 476 } 477 478 @Override public void onListItemClick(ListView l, View v, int position, long id) { 479 // Insert desired behavior here. 480 Log.i(TAG, "Item clicked: " + id); 481 } 482 483 // These are the rows that we will retrieve. 484 static final String[] PROJECTION = new String[] { 485 MainTable._ID, 486 MainTable.COLUMN_NAME_DATA, 487 }; 488 489 @Override 490 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 491 CursorLoader cl = new CursorLoader(getActivity(), MainTable.CONTENT_URI, 492 PROJECTION, null, null, null); 493 cl.setUpdateThrottle(2000); // update at most every 2 seconds. 494 return cl; 495 } 496 497 @Override 498 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 499 mAdapter.swapCursor(data); 500 501 // The list should now be shown. 502 if (isResumed()) { 503 setListShown(true); 504 } else { 505 setListShownNoAnimation(true); 506 } 507 } 508 509 @Override 510 public void onLoaderReset(Loader<Cursor> loader) { 511 mAdapter.swapCursor(null); 512 } 513 } 514} 515//END_INCLUDE(complete) 516