Home | History | Annotate | Download | only in app
      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