Chapter 13

Getting Persistent with Data Storage

In This Chapter

arrow Discovering data storage

arrow Creating an SQLite database

arrow Querying your database

arrow Using loaders and adapters

In certain types of applications, Android requires application developers to use data persistence, where information about a user’s preferences, such as favorite background colors or radio stations, is saved on the device for reuse later, after the device is turned off and then on again. For example, the Tasks application wouldn’t be useful if it didn’t save tasks, would it? Thankfully, the Android platform provides a robust set of tools that you can use to store user data.

This chapter delves deeply into creating and updating an SQLite database and producing a ContentProvider to access it. You need to be familiar with a certain level of database theory to tackle the data storage tasks in this ­chapter.

If you’re unfamiliar with SQL (Structured Query Language) or the SQL database, see the SQLite website at www.sqlite.org for more information.

This chapter is code intensive — if you start feeling lost, you can download the completed application source code from this book’s website.

Finding Places to Put Data

Depending on the requirements of your application, you may need to store data in a variety of places. For example, if an application interacts with music files and a user wants to play them in more than one music program, you have to store them in a location where all applications can access them. An application that needs to store sensitive data, such as encrypted usernames and password details, shouldn’t share data — placing it in a secure, local ­storage environment is the best strategy. Regardless of your situation, Android ­provides various options for storing data.

Viewing your storage options

The Android ecosystem provides various locations where data can be ­persisted:

  • Shared preferences: Private data stored in key‐value pairs. (See Chapter 15 to find out how to handle shared preferences.)
  • Internal storage: A location for saving files on the device. Files stored in internal storage are private to your application by default, and other applications cannot access them. (Neither can the user, except by using your application.) When the application is uninstalled, the private files are deleted as well.
  • Local cache: The internal data directory for caching data rather than storing it persistently. Cached files may be deleted at any time. You use the getCacheDir() method, available on the Activity or Context objects in Android.

    If you store data in an internal data directory and the internal storage space begins to run low, Android may delete files to reclaim space. Don’t rely on Android to delete your files for you though! You should delete your cache files yourself to stay within a reasonable limit (for example, around 1MB) of space consumed in the cache directory.

  • External storage: Every Android device supports shared external storage for files — either removable storage, such as a Secure Digital card (SD card) or non‐removable storage. Files saved to external storage are public (any person or application can alter them), and no level of security is enforced. Users can modify files by either using a file manager application or connecting the device to a computer via a USB cable and mounting the device as external storage. Before you work with external storage, check the current state of the external storage with the Environment object, using a call to getExternalStorageState() to check whether the media is available.

    The main method is a call on the Context object — getExternalFilesDir(). This call takes a string parameter as a key to help define the type of media you’re saving, such as ringtones, music, or photos. For more information, view the external data storage examples and documents at http://d.android.com/guide/topics/data/data‐storage.html#filesExternal.

  • SQLite database: A lightweight SQL database implementation that’s available across various platforms (including Android, iPhone, Windows, Linux, and Mac) and fully supported by Android. You can create tables and perform SQL queries against the tables accordingly. You implement an SQLite database in this chapter to handle the persistence of the tasks in the Tasks application.
  • Content provider: A “wrapper” around another storage mechanism. A content provider is used by an app to read and write application data that can be stored in preferences, files, or SQLite databases, for example. ContentProviders are smart in that they also keep track of when your data is modified, and automatically notify any listeners to changes. In this chapter, you will implement a ContentProvider to wrap your database access.
  • Network connection: (Also known as remote storage.) Any remote data source that you have access to. For example, because Flickr exposes an API that allows you to store images on its servers, your application might work with Flickr to store images. If your application works with a popular tool on the Internet (such as Twitter, Facebook, or Basecamp), your app might send information via HTTP — or any other protocol you deem necessary — to third‐party APIs to store the data.
  • Storage Access Framework: The SAF makes it simple for users to browse and open documents, images, and other files across all their preferred document storage providers. A standard, easy‐to‐use UI lets users browse files and access recents in a consistent way across apps and providers. For example, you can use the SAF to provide access to a remote cloud storage provider for documents.

Choosing a storage option

The various data storage locations offer quite the palette of options. However, you have to figure out which one to use, and you may even want to use ­multiple storage mechanisms.

Suppose that your application communicates with a third‐party remote API such as Twitter, and network communication is slow and less than 100 ­percent reliable. You may want to retain a local copy of all data since the last update from the server, to allow the application to remain usable (in some fashion) until the next update. When you store the data in a local copy of an SQLite database and the user initiates an update, the new updates refresh the SQLite database with the new data.

If your application relies solely on network communication for information retrieval and storage, use the SQLite database (or any other storage mechanism) to make the application remain usable when the user cannot connect to a network and must work offline — a common occurrence. If your application doesn’t function when a network connection is unavailable, you’ll likely receive negative reviews in the Google Play Store — as well as feature requests to make your app work offline. This strategy introduces quite a bit of extra work into the application development process, but it’s worth your time tenfold in user experience.

Understanding How the SQLite ContentProvider Works

The two fragments in the Tasks application need to perform various duties to operate. TaskEditFragment needs to complete these steps:

  1. Create a new record.
  2. Read a record so that it can display the details for editing.
  3. Update the existing record.

The TaskListFragment needs to perform these duties:

  1. Read all tasks to show them onscreen.
  2. Delete a task by responding to the click event from the context menu after a user has long‐pressed an item.

To work with an SQLite database, you communicate with the database via a ContentProvider. Programmers commonly remove as much of the database communication as possible from the Activity and Fragment objects. The database mechanisms are placed into a ContentProvider to help ­separate the application into layers of functionality. Therefore, if you need to alter code that affects the database, you know that you need to change the code in only one location to do so.

Creating Your Application’s SQLite Database

The first step to creating a new SQLite database ContentProvider is to create the SQLite database that it will use.

Visualizing the SQL table

The table in SQL is what holds the data you manage. Visualizing a table in SQLite is similar to looking at a spreadsheet: Each row consists of data, and each column represents the data inside the row. Listing 13-1 defines column names for the database. These column names equate to the header values in a spreadsheet, as shown in Figure 13-1. Each row contains a value for each column, which is how data is stored in SQLite.

image

Figure 13‐1: Visualizing data in the Tasks ­application.

The SQL script to create a table like the one in the previous figure is shown in Listing 13-1 :

Listing 13‐1: Creating an SQL Database Table

create table tasks ( →1
    _id integer primary key autoincrement, →2
    title text not null, →3
    notes text not null, →4
    task_date_time integer not null ); →5

Getting into the details about SQL is beyond the scope of the book, but here’s a brief synopsis about what this SQL script does:

1 Creates a table named tasks.

2 Adds a primary key to that table named _id. Android assumes that the id field for every table begins with an underscore.

3 Adds a non‐null field named title to the table. This field can be any length.

4 Adds a non‐null notes field.

5 Adds a date/time field to the table. In this table, the date/time is stored as an integer.

For more information on dates and times in SQLite, visit www.sqlite.org/datatype3.html.

Creating the database table

Android apps create SQLite databases using an SQLiteOpenHelper. Because this database is going to be used exclusively from a ContentProvider, you’re going to create an SQLiteOpenHelper class nested inside a ContentProvider.

Create a new class named TaskProvider in the directory com/dummies/tasks/provider. Add the following code to it:

public class TaskProvider extends ContentProvider { →1

    // Database Columns →3
    public static final String COLUMN_TASKID = "_id";
    public static final String COLUMN_DATE_TIME = "task_date_time";
    public static final String COLUMN_NOTES = "notes";
    public static final String COLUMN_TITLE = "title";

    // Database Related Constants →9
    private static final int DATABASE_VERSION = 1; →10
    private static final String DATABASE_NAME = "data"; →11
    private static final String DATABASE_TABLE = "tasks"; →12

    // The database itself
    SQLiteDatabase db; →15


    @Override   →18
    public boolean onCreate() {
        // Grab a connection to our database
        db = new DatabaseHelper(getContext()).getWritableDatabase(); →21
        return true;
    }

    /**
     * A helper class which knows how to create and update our database.
     */
    protected static class DatabaseHelper extends SQLiteOpenHelper {

        static final String DATABASE_CREATE = →30
            "create table " + DATABASE_TABLE + " (" +
                COLUMN_TASKID + " integer primary key autoincrement, " +
                COLUMN_TITLE + " text not null, " +
                COLUMN_NOTES + " text not null, " +
                COLUMN_DATE_TIME + " integer not null);";


        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION); →39
        }

        @Override   →42
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(DATABASE_CREATE);
        }


        @Override   →48
        public void onUpgrade(SQLiteDatabase db, int oldVersion,
                             int newVersion) {
            throw new UnsupportedOperationException(); →51
        }
    }
}

The numbered lines are described in this list:

1 A ContentProvider that knows how to read and write tasks from your tasks database. For now, it’s practically empty. The only thing it does is create a database using a SQLiteOpenHelper.

3 The names of the various columns in the Task table. These ­correspond to the columns in Listing 13-1 and Figure 13-1. These column names are going to be needed outside of this class, so make them public.

9 Various database‐related constants. These constants will not be needed outside this class, so they are private.

10 The version number for this database. Because it’s the first version, give it a version of 1. Whenever you change the database, increment this number by one so that Android knows that the database has changed. This allows you to know whether you need to upgrade the database schema in onUpgrade on line 48 (an advanced topic outside of the scope of this book).

11 The database name. This is the name of the file on the file system.

12 The name of the database table. This table is named "tasks".

15 The database object that will be created in onCreate using your SQLiteOpenHelper below. This is the object that your ContentProvider will use to read and write from your database.

18 This method is called when the ContentProvider is created. This is usually done once on app startup.

21 Creates the database object by using a DatabaseHelper. First you create a new DatabaseHelper, passing in the current context. Then, you call getWritableDatabase(). Some apps might want to use getReadableDatabase() instead, but because this app is reading and writing, it needs a writable database.

30 The database creation script from Listing 13-1.

39 The constructor for the database helper. It must call the super’s constructor, and pass in the current context, the database name, an optional CursorFactory for advanced usages, and the ­version of the database.

42 This method is called when the app is first installed and no database has yet been created. This is where the magic happens and your database creation SQL script is executed.

48 This method will be called in the future when version 2.0 of the Tasks app is released. At that point, you’ll need to upgrade the database from version 1.0 to version 2.0. For now, there’s nothing you need to do here.

51 Because this method will never be called (because there was no version 0 of the database before version 1), just throw an UnsupportedOperationException here. You will need to change this code before you release version 2 of the database.

Using ContentProvider URIs

Now that you’ve created the basic SQLite table, you need to start ­providing all the methods you’ll need to read and write from that table using your ContentProvider. But first, you need to understand how ContentProviders use URIs.

An Android ContentProvider uses URIs to identify data. Typically, you can use a URI to identify a specific piece of data, such as a single task, or all the tasks in your database. If you store other types of data there, you can use URIs for them, too.

In your application, you use two kinds of URIs — content://com.dummies.tasks.provider.TaskProvider/task to retrieve a list of all tasks in your database, or content://com.dummies.tasks.provider.TaskProvider/task/9 to retrieve a specific task from the ­database (in this case the task with the ID of 9).

These ContentProvider URIs are undoubtedly similar to the URIs you’re already familiar with. Their main differences are described in this list:

  • content://: A ContentProvider begins with content:// rather than with http://.
  • com.dummies.tasks.provider.TaskProvider: The second part of the URI identifies the authority (the TaskProvider) of the content. Though this string can be virtually anything, convention dictates using the fully qualified class name of your ContentProvider.
  • task: The third part of the URI identifies the path — in this case, the type of data you’re looking up. This string identifies which table in the database to read. If the application stores multiple types in the database (say, a list of users in addition to a list of tasks), a second type of path might be named user, for example.
  • 9: In the first URI, the path ends with task. However, in the second URI, the path continues to include the specific ID of the task being requested.

Now you have to add the code to support these URIs in your ContentProvider. Open TaskProvider and add the following lines to the class:

    // Content Provider Uri and Authority
    public static final String AUTHORITY
        = "com.dummies.tasks.provider.TaskProvider"; →3
    public static final Uri CONTENT_URI
        = Uri.parse("content://" + AUTHORITY + "/task"); →5

    // MIME types used for listing tasks or looking up a single
    // task
    private static final String TASKS_MIME_TYPE
        = ContentResolver.CURSOR_DIR_BASE_TYPE
            + "/vnd.com.dummies.tasks.tasks"; →11
    private static final String TASK_MIME_TYPE
        = ContentResolver.CURSOR_ITEM_BASE_TYPE
            + "/vnd.com.dummies.tasks.task";
    // UriMatcher stuff
    private static final int LIST_TASK = 0; →17
    private static final int ITEM_TASK = 1;
    private static final UriMatcher URI_MATCHER = buildUriMatcher(); →19

    /**
     * Builds up a UriMatcher for search suggestion and shortcut refresh
     * queries.
     */
    private static UriMatcher buildUriMatcher() {
        UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); →26
        matcher.addURI(AUTHORITY, "task", LIST_TASK); →27
        matcher.addURI(AUTHORITY, "task/#", ITEM_TASK); →28
        return matcher;
    }

    /**
     * This method is required in order to query the supported types.
     */
    @Override
    public String getType(Uri uri) {
        switch (URI_MATCHER.match(uri)) { →37
            case LIST_TASK:
                return TASKS_MIME_TYPE;
            case ITEM_TASK:
                return TASK_MIME_TYPE;
            default:
                throw new IllegalArgumentException("Unknown Uri: " + uri);
        }
    }

This chunk of code may seem intimidating, but it consists mostly of constants with one useful method (getType()). Here’s how the numbered lines work:

3 The authority for the ContentProvider — by convention, the same as the fully qualified class name. This value must match the value you will add to your AndroidManifest.xml file for the provider authorities.

5 The base URI for the ContentProvider. Every time your application asks for data for this URI, Android routes the request to this ContentProvider.

The ContentProvider supports two types of URIs: one for ­listing all tasks and one for listing a specific task.

The first type of URI is the CONTENT_URI, and the second one is the CONTENT_URI with the task ID appended to the end.

11 Because the ContentProvider supports two types of data, it defines two types (or MIME types) for this data. MIME types are simply strings commonly used on the web to identify data types. For example, web HTML content typically has a MIME type of text/html, and audio MP3 files have audio/mpeg3. Because the tasks are of no known standard type, you can make up MIME type strings as long as you follow Android and MIME conventions.

The list MIME type begins with ContentResolver.CURSOR_DIR_BASE_TYPE, and the individual task MIME type begins with ContentResolver.CURSOR_ITEM_BASE_TYPE. DIR represents the list, and ITEM represents the item — simple enough.

The subtype (which follows the /) must begin with vnd. The subtype is followed by the fully qualified class name and the type of data — in this case, com.dummies.tasks and task. Visit http://developer.android.com/reference/android/content/ContentResolver.html for more information about the Android conventions for MIME types.

You use "task" singular almost everywhere in the ContentProvider. The only places where it is plural is in the MIME type for lists, in the name of the database file, and when referring to the name of the app. Everywhere else it is singular.

17 Uses another constant to identify list types versus item types, which are ints.

19 The UriMatcher is used to determine the URI type: list or item. You build a UriMatcher using the method named buildUriMatcher() on line 25.

26 Creates the UriMatcher, which can indicate whether a given URI is the list type or item type. The UriMatcher.NO_MATCH parameter tells the application which default value to return for a match.

27 Defines the list type. Any URI that uses the com.dummies.tasks.provider.TaskProvider authority and has a path named "task" returns the value LIST_TASK.

28 Defines the item type. Any URI that uses the com.dummies.tasks.TaskProvider authority and has a path that looks like task/# (where # is a number) returns the value ITEM_TASK.

37 Uses the UriMatcher on line 19 to determine which MIME type to return. If the URI is a list URI, it returns TASKS_MIME_TYPE. If it’s an item URI, it returns TASK_MIME_TYPE.

Before you can use the TaskProvider, make sure that it’s listed in the AndroidManifest.xml file, by adding this code before the </application> tag:

    <provider
        android:name=".provider.TaskProvider"
        android:authorities="com.dummies.tasks.provider.TaskProvider"
        android:exported="false" />

It tells Android that a ContentProvider named TaskProvider will handle URIs that use the specific authority of com.dummies.tasks.TaskProvider. It also indicates that the data in the provider is not exported to other apps on the user’s phone. In general, you should set exported="false" unless you want to make your provider available to other apps.

There are two main times when you need a ContentProvider instead of just using a database directly. The first is when you want to export your ­content to other apps. The Android Calendar app allows you to browse your calendar from other apps using this mechanism. The second case is when you need to use a CursorLoader, which you will use later in this chapter.

Dealing with CRUD

Your ContentProvider needs to be able to deal with CRUD. Specifically, it needs to handle the following operations:

  • Create
  • Read
  • Update
  • Delete

To do this, you must add the necessary methods to support these four ­operations to the TaskProvider. I’ll tackle these slightly out of order.

Create

Adding a new item to the database is easy. Add the following method to your TaskProvider:

    /**
     * This method is called when someone wants to insert something
     * into our content provider.
     */
    @Override
    public Uri insert(Uri uri, ContentValues values) { →6
        // you can't choose your own task id
        if( values.containsKey(COLUMN_TASKID))
            throw new UnsupportedOperationException(); →9

        long id = db.insertOrThrow(DATABASE_TABLE, null,
           values); →12
        getContext().getContentResolver().notifyChange(uri, null); →13
        return ContentUris.withAppendedId(uri, id); →14
    }

Here’s what the insert method is doing:

6 The insert method takes two parameters. The first is the URI that identifies which table to insert into, which will always be CONTENT_URI for this ContentProvider. The second parameter is a hashmap with keys and values that represent the data being inserted into the database. Typically, this would include the task’s title and notes.

9 When you insert something into the database, the database creates a new row and returns the ID to you. Because of this, it doesn’t make sense to allow you to specify a row id when you insert into the db. Doing so is an error, so throw an exception.

12 Calls insertOrThrow on the database object to insert the value. As the name implies, this method throws an exception if there’s any problem inserting into the database. Typically, this would only happen if the user is running out of space on his or her phone. Because this is fairly rare, you do not need to add any explicit exception handling to catch this case. The ­insertOrThrow method returns the ID of the task that was added to the db.

13 As mentioned before, one of the main responsibilities of a ContentProvider is to notify listeners of changes to their data. If a list page in your app is watching the tasks table, and the edit page adds a new item to the table, the list page needs to be notified of the change so that it can be refreshed. This is done on this line by calling notifyChange() on the context’s ContentResolver. The notifyChange() method takes the uri of the content that has changed. The second parameter of ­notifyChange() can be ignored.

14 Returns the URI for the newly added task. To do this, take the URI and append the new ID using ContentUris.withAppendedId().

Update

Editing (also known as updating) a task in the database is very similar to ­creating a new one. Add the following method to your TaskProvider:

    /**
     * This method is called when someone wants to update something
     * in our content provider.
     */
    @Override
    public int update(Uri uri, ContentValues values, String ignored1,
     String[] ignored2) {  →7
        // you can't change a task id
        if( values.containsKey(COLUMN_TASKID))
            throw new UnsupportedOperationException(); →10

        int count = db.update(   →12
            DATABASE_TABLE,
            values,
            COLUMN_TASKID + "=?", →15
            new String[]{Long.toString(ContentUris.parseId(uri))}); →16

        if (count > 0)
            getContext().getContentResolver().notifyChange(uri, null); →19

        return count; →21
    }

Here’s a description of what this listing is doing:

7 The update method takes four parameters. The first is the URI, which is the same URI as the insert method, except this URI will also have the ID of the task to be edited appended to the end. For example, the URI might be content://com.dummies.tasks.provider.TaskProvider/task/8 to edit the eighth task in the db. The second parameter is the values to be set for that task. Typically this would include the title and/or the notes. The third and fourth parameters are SQL selection arguments for advanced usages and can be ignored.

10 Just like in the insert method, it is illegal to try to change the ID of a given task, so throw an exception if anyone tries.

12 Calls the update() method on the db object. Much like in the call to insertOrThrow() in the previous section, the first two parameters to the update call are the table to be edited and the values to be set. The next parameters, however, are different.

15 Specifies the WHERE clause to the SQL query. In this case, the WHERE clause will be "_id=?", indicating that you want to update the row that has an _id of "?". The "?" will be replaced by the value on line 16.

16 Computes the id of the task to be edited. This is done by parsing it from the URI using ContentUris.parseId(), converting the resulting long into a String, and then putting that String into an array of Strings to be passed as the whereArgs for the update call. Each "?" in the where clause will be replaced by the respective entry from the String array, so there should always be exactly as many question marks in the where clause as there are items in the String array.

The lazy or enterprising among you might wonder, why do I need to use a bunch of question marks and String arrays? Can’t I just make a WHERE clause that says "_id=10" and skip the whole question mark business entirely? Don’t do it! Using a question mark is a security practice that can prevent you from getting hit from SQL injection attacks. To learn more about SQL injection, visit http://en.wikipedia.org/wiki/SQL_injection.

19 If anything in the table was changed, notify any listeners.

21 Returns the count of items update. It should only ever be zero or one.

Delete

The delete method is even easier to implement than the update method.

    /**
     * This method is called when someone wants to delete something
     * from our content provider.
     */
    @Override
    public int delete(Uri uri, String ignored1, String[] ignored2) { →6
        int count = db.delete( →7
            DATABASE_TABLE,
            COLUMN_TASKID + "=?",
            new String[]{Long.toString(ContentUris.parseId(uri))});
        if (count > 0)
            getContext().getContentResolver().notifyChange(uri, null); →13
        return count;
    }

By now, most of this should be familiar to you. However, there are some ­differences:

6 As was the case with the update method, the last two arguments (the selection and the selectionArgs) can be ignored for delete.

7 Calls the delete method, and passes in the table name, the where clause for the _id, and the _id.

13 If anything was deleted, notify any listeners. Then return the count of rows that were deleted (should be zero or one).

Read

Were insert, update, and delete too easy for you? Are you ready for a challenge? Well, let’s give you something a little trickier. Here’s how you implement the query (also known as Read) method:

    /**
     * This method is called when someone wants to read something from
     * our content provider. We'll turn around and ask our database
     * for the information, and then return it in a Cursor.
     */
    @Override
    public Cursor query(Uri uri, String[] ignored1, String selection, →7
                    String[] selectionArgs, String sortOrder) {

        String[] projection = new String[]{ →10
                COLUMN_TASKID,
                COLUMN_TITLE,
                COLUMN_NOTES,
                COLUMN_DATE_TIME};

        Cursor c;
        switch (URI_MATCHER.match(uri)) { →17

            case LIST_TASK: →19
                c = db.query(DATABASE_TABLE,  →20
                       projection, selection,
                       selectionArgs, null, null, sortOrder);
                break;

            case ITEM_TASK: →25
                c = db.query(DATABASE_TABLE, projection, →26
                        COLUMN_TASKID + "=?",
                        new String[]{Long.toString(ContentUris.parseId
                                (uri))},
                        null, null, null, null);
                if (c.getCount() > 0) {
                    c.moveToFirst(); →32
               }
               break;
            default:
              throw new IllegalArgumentException("Unknown Uri: " + uri); →36
        }
        c.setNotificationUri(getContext().getContentResolver(), uri); →39
        return c;
    }

Okay, that wasn’t so bad, but it still warrants some explanation:

7 The query method takes a URI that represents the content to be queried. The selection parameter specifies an optional where clause (such as title=?), and the selectionArgs parameter is an array of strings that fill in any question marks in that selection parameter. The sortOrder parameter indicates how the results should be sorted.

10 Creates a list of column names to represent the data and the order of the data that will be returned. This is called a projection to people who hold their pinkies up when they drink tea.

17 Uses the UriMatcher to see what kind of query you have and ­formats the database query accordingly.

19 You are asked to return a list of tasks.

20 Queries the database table named "tasks" with the projection specified on line 10. The selection parameter indicates which tasks will be selected. If no selection is specified, this returns ALL of the rows in this table. The result is an SQL cursor that contains each of the columns specified in the projection.

25 You are asked to return a specific task.

26 Unlike line 20, line 26 is about querying a single specific task. To do that, you construct a where clause with an _id specified in the where args, exactly like you did for the update and delete methods. The other parameters of the db.query method can be ignored.

32 If the query returned any results (for example, getCount() is larger than zero), then move the cursor to the first item in the list.

36 If the URI wasn’t a list URI and it wasn’t an item URI, then something went wrong, so throw an error.

39 Sets the notification URI for this cursor. This URI must agree with the URIs you used in insert, update, and delete. The loader (explained later in this chapter) uses this URI to watch for any changes to the data; and if the data changes, the loader automatically refreshes the UI.

Your ContentProvider is now complete! The next step is to use it in your app.

Implementing the Save Button

There are two fundamental things your ContentProvider is used for. The first is reading from your database, and the second is writing to your ­database. Let’s look at the simpler of the two first, which is writing to your database.

Open TaskEditFragment.java and add the following method:

    private void save() {
        // Put all the values the user entered into a
        // ContentValues object
        String title = titleText.getText().toString(); →4
        ContentValues values = new ContentValues();
        values.put(TaskProvider.COLUMN_TITLE, title);
        values.put(TaskProvider.COLUMN_NOTES,
                notesText.getText().toString());
        values.put(TaskProvider.COLUMN_DATE_TIME,
            taskDateAndTime.getTimeInMillis());

        // taskId==0 when we create a new task,
        // otherwise it's the id of the task being edited.
        if (taskId == 0) {

            // Create the new task and set taskId to the id of
            // the new task.
            Uri itemUri = getActivity().getContentResolver()
                 .insert(TaskProvider.CONTENT_URI, values); →19
            taskId = ContentUris.parseId(itemUri); →20

        } else {

            // Update the existing task
            Uri uri = ContentUris.withAppendedId(TaskProvider.CONTENT_URI,
                taskId); →26
            int count = getActivity().getContentResolver().update(
                uri, values, null, null); →28

            // If somehow we didn't edit exactly one task,
            // throw an error
            if (count != 1) →32
                throw new IllegalStateException(
                    "Unable to update " + taskId);

        }

        Toast.makeText( →38
            getActivity(),
            getString(R.string.task_saved_message),
            Toast.LENGTH_SHORT).show();

   }

At a high level, the save method is doing three things:

  • It’s putting all the values that the user entered into a ContentValues key‐value map.
  • It’s using a ContentResolver to insert or update those values, depending on whether the taskId is zero (to insert a new task) or non‐zero (to edit an existing task). Most of the time, you don’t access a ContentProvider directly. Instead, you use a ContentResolver to resolve an operation on a ContentProvider by using a URI.
  • It’s messaging the user that the save was successful using a Toast.

Here is the code in more detail:

4 Creates a new ContentValues map, then takes all the values that the user entered into the fragment (such as title, notes, date, and time), and puts them into the ContentValues instance. Note that you do not put the task ID into the ContentValues because it’s illegal to try to change it.

19 This line gets a ContentResolver from the activity. It then calls insert() on that ContentResolver and specifies the URI of the task table and all the values that you want to insert. The ContentResolver will inspect that URI, figure out which ContentProvider is responsible for that URI, and ultimately call into your TaskProvider.insert method to insert the data for you.

20 The call to insert() returns the URI of the data that was inserted, so parse out the ID of the newly inserted task and update the taskId field with the new value. That way, if the fragment does anything else later, the taskId will be set correctly and everything will work as it should. (In this case, it’s not strictly ­necessary because the fragment finishes itself as soon as the save is complete, but it’s usually better to leave yourself in a clean state than to open yourself up to future bugs.)

26 In this section, you are updating an existing task rather than inserting a new one, so figure out what the URI is for that task by appending it to the CONTENT_URI using ContentUris.­withAppendedId.

28 Edit the task by giving the task’s URI and new values to the ContentResolver, like you did on line 19.

32 If everything went well, then exactly one task should have been edited. If somehow more or less than one task was edited, throw an error.

38 Notifies the user of the change using a Toast.

You added a new string, so add it to strings.xml:

            <string name="task_saved_message">Task has been saved</string>

Now that you have a save() method, you need to call it. Uncomment the line you added in Chapter 11 in TaskEditFragment.onOptionsItemSelected that called save:

      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
         switch(item.getItemId()) {
            case MENU_SAVE:
               save();

               ((OnEditFinished) getActivity()).finishEditingTask();
               return true;
         }

         // If we can't handle this menu item, see if our parent can
         return super.onOptionsItemSelected(item);
     }

Now run your app! Click the Add button in the action bar and create a new task with whatever title you want, then click Save. A Toast message will pop up and indicate the task was saved. But how do you know for sure it was saved? The app has no way to show you the saved task yet.

You may not be able to view the data in the app, but if you are using an emulator or a rooted phone, you should be able to examine the SQLite database directly from the command line.

If you would like to consider rooting your phone (not all phones allow this), visit http://www.androidcentral.com/root.

To view the database directly:

  1. Open a terminal on your computer and type adb shell to get a login shell on your device.
  2. Type cd /data/data/com.dummies.tasks/databases.
  3. If your device has the sqlite3 command installed (most do), you can run sqlite3 data to examine and manipulate your database directly.

    "data" is the DATABASE_NAME of the database you created in the SQLiteOpenHelper.

  4. Try running select * from tasks; to get a list of your tasks.

    You should now see the task you just created.

If your device does not have sqlite3 installed, you can search the App Store to find an sqlite3 binary that you can install on rooted phones.

Implementing the List View

You might think that reading from a database should be simpler than writing to a database. After all, you don’t have to change anything when you do a read. However, reading from a database is actually more complicated than writing for this example.

The reason is that when you’re doing any kind of I/O operation, such as ­reading from a network or from disk (reading a database, for example), you must do this work from a background thread. If you work from the main thread of the user interface, you run the risk of locking it up for an unknown period, which can cause it to feel jerky and unresponsive. Under particularly bad circumstances, it can even lead to displaying the dreaded Application Not Responsive dialog box, which can leave many users believing that your application has crashed.

Because the read operation is reading a bunch of items in a list, it may take a little time. It might take a few hundred milliseconds or so, for example. That may not seem like a long time, but it’s long enough to make your app stutter, and in rare circumstances it’s possible you might see an ANR.

Technically, both reading and writing from a database should be done on a background thread. So if we’re following best practices, the previous section on implementing the Save button should have used a background thread to write to the database. However, because the save operation is writing such a small amount of data to just a single task at a time, and because the UI isn’t doing anything fancy during that time, we took a shortcut and skipped the background thread. It’s reasonably safe to do so in this case, but you may want to consider going back after reading this chapter and reimplementing save using a loader.

Android provides a system based on loaders and adapters to read a list of data from a datastore (such as a database or file system) on a background thread.

  • Loaders are objects that read data from somewhere, often a database. Loaders have two responsibilities:
    • They must be able to load data into memory. This is usually accomplished by using an SQLite cursor to read data from the database into memory a few records at a time.
    • They must watch your database table for changes, and if they are notified of a change, they will reload the data as necessary.
  • Adapters are objects that know how to create views for each item in a list. You created a simple adapter named TaskListAdapter in Chapter 9 to read data from a dummy list of strings and create CardViews for each item.

In the next sections, you are going to create a loader to load data from your database, and an adapter to create views for that data.

Using loaders

The loader provides a mechanism by which you can launch background operations (such as reading from your database) and then get a callback when those operations finish so that you can update the user interface.

A typical example of a loader is a CursorLoader. You use a CursorLoader to load data from an SQLite database using a cursor. To add a CursorLoader to one of your list fragments, you implement the LoaderCallback interface in your callback and implement the three LoaderCallback methods:

  • onCreateLoader(): This method is called in a background thread when you create a loader using initLoader(). In this method, you’re responsible for creating a CursorLoader object and returning it. The CursorLoader uses a URI to ask a ContentProvider for data.
  • onLoadFinished(): This method is called when the CursorLoader object finishes loading its data from the database. In this method, you’re responsible for updating the UI to show the new data to the user.
  • onLoaderReset(): This method is called when the loader is being reset or shut down. When this happens you’re responsible for making sure your fragment no longer uses the loader or its cursor.

To kick off a loader, you first obtain a LoaderManager from your activity by calling getLoaderManager() and then initLoader(). initLoader() starts loading data in the background by calling ­onCreateLoader(), and when it finishes it executes onLoaderFinished() in your LoaderCallback object.

You can use loaders for things other than loading data from a database, but all loaders must implement the same three methods regardless of whether they’re loading their data from a database, a network, or somewhere else entirely.

Visit http://developer.android.com/guide/components/loaders.html for more information about loaders.

Open TaskListFragment.java and add the following code in bold:

public class TaskListFragment extends Fragment
         implements LoaderManager.LoaderCallbacks<Cursor> →2
{
    @Override
    public void onCreate(Bundle savedInstanceState) {
         . . .

         getLoaderManager().initLoader(0, null, this); →8

            }

    @Override
    public Loader<Cursor> onCreateLoader(int ignored, Bundle args) {
        return new CursorLoader(getActivity(), →13
                TaskProvider.CONTENT_URI, null, null, null, null);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        adapter.swapCursor(cursor); →19
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        adapter.swapCursor(null); →24
    }
}

You will get a couple of errors when you add this code, but skip those for now. What this code is doing:

2 Adds the LoaderManager.LoaderCallbacks interface to this fragment, which is needed when we call initLoader on line 8.

8 This is where you tell Android to start up a loader for you. Get a LoaderManager by calling getLoaderManager(), then initialize a loader by calling initLoader. initLoader takes three ­parameters:

  • An ID for the loader. If you have multiple loaders, it’s handy to give them each different IDs.
  • A Bundle of args that can be used to initialize the loader. In this case, there’s nothing special we need to initialize, so we’ll pass in null for the args.
  • A LoaderManager.LoaderCallbacks implementation. Oh hey, that’s us!

13 initLoader on line 8 will call onCreateLoader to create a new loader. Because you are going to be reading data from an SQL database, you will use Android’s built‐in CursorLoader to do the heavy lifting. Create a new CursorLoader and initialize it with the URI of the ContentProvider you want to use. The other parameters are for advanced usage; see http://d.android.com/reference/android/content/CursorLoader.html for more information.

19 When the loader is finished loading data into memory, it needs to do something with that data. You can do whatever you want with the data, but the traditional thing is to give the data to an adapter so that the adapter can display it to the user. The adapter has a method called swapCursor, which replaces whatever cursor it was using with a new cursor, so call swapCursor with the new cursor you just received.

24 onLoaderReset is called when the last cursor provided to onLoadFinished() above is about to be closed. You need to make sure the adapter is no longer using it, so set it to null.

That is essentially all you need to do to use a loader. The next step is to implement the adapter that can take the data and create a view for it.

Using adapters

Adapters are objects that know how to create views for each item in a list. You created a simple adapter in Chapter 9, and now you are going to update it to read data from an SQL Cursor.

Open TaskListAdapter and add the bold lines to it:

public class TaskListAdapter
    extends RecyclerView.Adapter<TaskListAdapter.ViewHolder>
{
    static String[] fakeData = new String[] { →4
        "One",
        "Two",
        "Three",
        "Four",
        "Five",
        "Ah... ah... ah!"
    };

    Cursor cursor; →13
    int titleColumnIndex; →14
    int notesColumnIndex;
    int idColumnIndex;

    public void swapCursor(Cursor c) { →18
        cursor = c; →19
        if(cursor!=null) {
            cursor.moveToFirst(); →21
            titleColumnIndex = cursor.getColumnIndex(TaskProvider.COLUMN_TITLE); →22

            notesColumnIndex = cursor.getColumnIndex(TaskProvider.COLUMN_NOTES);
            idColumnIndex = cursor.getColumnIndex(TaskProvider.COLUMN_TASKID);
        }
        notifyDataSetChanged(); →26
    }

    @Override
    public void onBindViewHolder(final ViewHolder viewHolder,
                                 final int i) {
         final Context context = viewHolder.titleView.getContext();
         final long id = getItemId(i); →33

         // set the text
         cursor.moveToPosition(i); →36
         viewHolder.titleView.setText(cursor.getString(titleColumnIndex)); →37
         viewHolder.notesView.setText(cursor.getString(notesColumnIndex)); →38

         // set the thumbnail image
         Picasso.with(context)
             .load(getImageUrlForTask(id)) →42
             .into(viewHolder.imageView);

         // Set the click action
         viewHolder.cardView.setOnClickListener(
                  . . .
                      ((OnEditTask) context).editTask(id); →48
             });
         viewHolder.cardView.setOnLongClickListener(
              new View.OnLongClickListener()
              {
                 . . .
                                     deleteTask(context, id);→55
              });

    }

    @Override
    public long getItemId(int position) { →61
        cursor.moveToPosition(position);
        return cursor.getLong(idColumnIndex); →63
    }

    @Override
    public int getItemCount() {
        return cursor!=null ? cursor.getCount() : 0; →68
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        CardView cardView;
        TextView titleView;
        TextView notesView; →74
        ImageView imageView;

        public ViewHolder(CardView card) {
            super(card);
            cardView = card;
            titleView = (TextView)card.findViewById(R.id.text1);
            notesView = (TextView) itemView.findViewById(R.id.text2); →81
            imageView = (ImageView)card.findViewById(R.id.image);
        }
    }
}

These changes to TaskListAdapter make it possible to read the list of tasks from a cursor rather than from a hardcoded fakeData array. In more detail:

4 Remove the fakeData array; it is no longer necessary. You also need to remove the call to titleView.setText(fakeData[position]) in onBindViewHolder.

13 The TaskListAdapter is going to read data from a cursor, so add a field for the cursor here.

14 When reading through the cursor, each column of data is referred to by an index. For example, the index of the title column might be 1, the index of notes might be 2, and so on. You don’t need the index for the date/time column because the list view does not display the date/time of each task. Store the indices of each column here for quick reference; you will determine their values on line 22.

18 Creates a method named swapCursor. This method is called whenever the data in your database has changed. This might occur because someone added or deleted an item from the database, or because the app just started up and is reading all the previously created tasks for the first time. swapCursor is responsible for

  • Replacing the previous cursor (if there was one) with the new cursor
  • Figuring out the indices of the various columns of data
  • Notifying any listeners that the data has changed

19 Replaces the previous cursor with the new cursor.

21 Whenever you use a cursor, you must first move the cursor to its first location before you may attempt to read data from it. This line moves to the first position so that we can read the various column indices in the next few lines.

22 Determines the column index for the title column in the cursor. This is done by asking the cursor for the index of the column named "title". Technically, you can skip this step entirely and just ask for columns by their name rather than by their index, but it’s more efficient to ask by index. On the next two lines, do the same thing for the notes and id columns.

26 When the cursor has been swapped, that means that the data likely has changed. Notify any listeners (in particular, the RecyclerView from Chapter 9) that the data has changed so that they can refresh their displays.

33 Each task in the database has an ID associated with it. You will need the id later, so find the ID for this task by calling getItemId and passing in the position of the item in the list.

36 You are about to update the view with the data from the cursor, so make sure you move your cursor to the proper position before you begin to read.

37 Reads the title string from the cursor using the getString method, and then uses that string to set the titleView TextView.

38 Does the same for the notesView.

42–55 In the old TaskListAdapter, items in the fakeData array didn’t have an ID, so we just used the position in the index as a sort of fake ID. In the new version of TaskListAdapter, every task has an ID that is stored in the database, so make sure to use that ID when calling getImageUrlForTask, editTask, and deleteTask.

61 The implementation for getItemId which was called from line 33.

63 After moving the cursor to the appropriate row in the database, this line asks the cursor what the ID is for that row.

68 Updates getItemCount to return the count of items in the cursor, assuming that the cursor is not null. If the cursor is null, this line just returns 0.

74 Adds the notesView TextView to your ViewHolder. Return to Chapter 9 for a reminder of what a ViewHolder does.

81 Sets the notesView field by looking for the TextView named text2 in the card_task.xml layout.

If you run your app now, you should be able to add tasks! Give it a try.

Deleting a task

There is one more thing to do. You need to add the ability to delete tasks from your database.

This is pretty straightforward. Update TaskListAdapter to implement the deleteTask method as shown:

    private void deleteTask(Context context, long id) {
        context.getContentResolver()
                .delete(
                        ContentUris.withAppendedId(
                                TaskProvider.CONTENT_URI,
                                id),
                        null, null);
    }

This code gets the ContentResolver from the context, calls delete on it, and passes in the URI of the task to be deleted.

Run the app and long‐press on an item in the list to try deleting it. You should see it automatically disappear from the list after the delete is confirmed.

Reading Data into the Edit Page

The Edit page can now save data into the database, but it cannot yet read data from the database. This makes it impossible for users to edit existing tasks, so let’s wrap up this final bit of functionality now.

Now that you know how loaders work, let’s use a loader to read the task data from the database into the edit page. As you recall, loaders are the best way to perform I/O on a background thread without blocking the main UI thread.

Open TaskEditFragment and make the following changes:

public class TaskEditFragment extends Fragment
    implements DatePickerDialog.OnDateSetListener,
    TimePickerDialog.OnTimeSetListener,
    LoaderManager.LoaderCallbacks<Cursor> →4
{
    . . .

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        // Inflate the layout and set the container. The layout is the
        // view that we will return.
        View v = inflater.inflate(R.layout.fragment_task_edit,
            container, false);

        // From the layout, get a few views that we're going to work with
        rootView = v.getRootView();
        titleText = (EditText) v.findViewById(R.id.title);
        notesText = (EditText) v.findViewById(R.id.notes);
        imageView = (ImageView) v.findViewById(R.id.image);
        dateButton = (TextView) v.findViewById(R.id.task_date);
        timeButton = (TextView) v.findViewById(R.id.task_time);

        // Set the thumbnail image →25
        Picasso.with(getActivity())
            .load(TaskListAdapter.getImageUrlForTask(taskId))
            .into(. . .);

        updateDateAndTimeButtons(); →30

        // Tell the date and time buttons what to do when we click on
        // them.
        dateButton.setOnClickListener(new View.OnClickListener() {
                                          @Override
                                          public void onClick(View v) {
                                              showDatePicker();
                                          }
                                     });
        timeButton.setOnClickListener(new View.OnClickListener() {
                                          @Override
                                          public void onClick(View v) {
                                              showTimePicker();
                                          }
                                     });

        if (taskId == 0) { →47

            updateDateAndTimeButtons();

        } else {

            // Fire off a background loader to retrieve the data from the
            // database
            getLoaderManager().initLoader(0, null, this); →55

        }

        return v;
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) { →63
       Uri taskUri = ContentUris.withAppendedId( →64
           TaskProvider.CONTENT_URI, taskId);

       return new CursorLoader( →67
          getActivity(),
          taskUri, null, null, null, null);
    }
    /**
     * This method is called when the loader has finished loading its
     * data
     */
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor task) {
        if (task.getCount() == 0) { →78
            getActivity().runOnUiThread( →79
                new Runnable() {
                    @Override
                    public void run() {
                        ((OnEditFinished) getActivity())
                            .finishEditingTask(); →84
                    }
               });
           return;
        }

        titleText.setText( →90
            task.getString(
                task.getColumnIndexOrThrow(TaskProvider.COLUMN_TITLE)));
        notesText.setText(
            task.getString(
                task.getColumnIndexOrThrow(TaskProvider.COLUMN_NOTES)));

        Long dateInMillis = task.getLong( →97
            task.getColumnIndexOrThrow(TaskProvider.COLUMN_DATE_TIME));
        Date date = new Date(dateInMillis);
        taskDateAndTime.setTime(date);


        Picasso.with(getActivity()) →103
            .load(TaskListAdapter.getImageUrlForTask(taskId))
            .into(. . .);

        updateDateAndTimeButtons(); →107
    }

    @Override
    public void onLoaderReset(Loader<Cursor> arg0) { →111
         // nothing to reset for this fragment.
    }
}

This code reads the task information from the database rather than from the fakeData list. It does it using a loader to avoid blocking the main UI thread. Here’s what the code does in more detail:

4 Similar to what you did when you put a loader into the list view, you must implement LoaderManager.LoaderCallbacks in your fragment to use a loader here.

25–30 It doesn’t make sense to try to download the image or update the date and time buttons yet if you don’t know what data has been loaded from the task, so move these lines from here to lines 103–107 when the loader has finished.

47 If the task ID is 0, then you know you’re inserting a new item into the database. This means that there’s no data to load, so skip the loader and just update the time and date buttons instead.

55 If the task ID was non‐zero, then the loader needs to read data out of the database. Start it up by calling initLoader, and pass in yourself as the LoaderManager.LoaderCallbacks object.

63 onCreateLoader is called by initLoader when it is time to create the loader.

54 Computes the URI for the task you want to load.

67 Creates a cursor loader to load the specified task.

78 Sanity check. If you weren’t able to load anything, just close this activity.

79 onLoadFinished is called from a background thread. Many operations that affect the UI aren’t allowed from background threads. So make sure that you call finishEditingTask from the UI thread instead of from a background thread.

84 Calls finishEditingTask from the main UI thread. You implemented finishEditingTask in Chapter 11.

90 Sets the title and notes from the DB.

97 Sets the task date/time from the DB.

103–107 The code that you moved from lines 25–30.

111 onLoaderReset is called when a previously created loader is being reset, thus making its data unavailable. In the list view, you needed to tell the adapter to stop using the old cursor. But in this fragment, there is nothing using the old cursor, so there is nothing to be done in this method.

Now you should have a fully working Tasks app that can create, read, update, and delete tasks from its database. Congratulations! Try running the app now and test it out.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
44.200.94.150