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