Download Content Providers

Document related concepts
no text concepts found
Transcript
Content Providers
Akhilesh Tyagi
Content Providers
• manage access to a structured set of data
• Content providers are the standard interface that connects data in one process with code running in another process.
• Content Providers provide an interface for publishing data that will be consumed using a Content Resolver.
• The ContentResolver object communicates with the provider object, an instance of a class that implements ContentProvider.
• don't need to develop your own provider if you don't intend to share your data with other applications
• you do need your own provider to provide custom search suggestions in your own application
• also need your own provider if you want to copy and paste complex data or files from your application to other applications
• Android itself includes content providers that manage data such as audio, video, images, and personal contact information
• other applications access the provider using a provider client object
• A content provider presents data to external applications as one or more tables that are similar to the tables found in a relational database.
word
app id
frequency
locale
_ID
mapreduce
user1
100
en_US 1
precompiler
user14
200
fr_FR
2
applet
user2
225
fr_CA
3
const
user1
255
pt_BR
4
int
user5
100
en_UK 5
• To create a new Content Provider, extend the abstract ContentProvider class:
public class MyContentProvider extends ContentProvider
• good practice to include static database constants — particularly column names and the Content Provider authority — that will be required for transacting with, and querying, the database.
• override the onCreate handler to initialize the underlying data source, as well as the query, update, delete, insert, and getType methods to implement the interface used by the Content Resolver
• A content provider offers data in two ways:
• File data ‐‐‐ Data that normally goes into files, such as photos, audio, or videos. Store the files in your application's private space. In response to a request for a file from another application, your provider can offer a handle to the file.
• "Structured" data ‐‐‐ Data that normally goes into a database, array, or similar structure. Store the data in a form that's compatible with tables of rows and columns.
Accessing a provider
• An application accesses the data from a content provider with a ContentResolver client object.
• This object has methods that call identically‐
named methods in the provider object
• The ContentResolver methods provide the basic "CRUD" (create, retrieve, update, and delete) functions of persistent storage
// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // The content
URI of the words table
mProjection,
// The columns to return for
each row
mSelectionClause
// Selection criteria
mSelectionArgs,
// Selection criteria
mSortOrder);
// The sort order for the
returned rows
Registering Content Providers
• Like Activities and Services, Content Providers must be registered in your application manifest before the Content Resolver can discover them.
• This is done using a provider tag that includes a name attribute describing the Provider’s class name and an authorities tag
• Use the authorities tag to define the base URI of the Provider’s authority. A Content Provider’s authority is used by the Content Resolver as an address and used to find the database.
<provider android:name=”.MyContentProvider”
android:authorities=”edu.iastate.transcriptprovider”/>
Content URIs
• A content URI is a URI that identifies data in a provider. • Content URIs include the symbolic name of the entire provider (its authority) and a name that points to a table (a path)
• When you call a client method to access a table in a provider, the content URI for the table is one of the arguments
content://user_dictionary/words
ContentURIs class
• content://authority/path/id
• The scheme portion of the URI. This is always
set to
ContentResolver.SCHEME_CONTENT (value co
ntent://)
• authority: A string that identifies the entire content provider
• path: Zero or more segments, separated by a forward
slash (/), that identify some subset of the provider's
data -table name
• id: a row of a table or subset of data.
ContentURIs Public Methods
public static Uri.Builder appendId (Uri.Builder build
er, long id)
//Appends the given ID to the end of the path.
• Returns the given builder
public static long parseId (Uri contentURI)
//Converts the last path segment to a long.
Returns the long conversion of the last segment or ‐1 if the path is empty
public static Uri withAppendedId (Uri contentUri, long id)
//Appends the given ID to the end of the path.
Returns a new URI with the given ID appended to the end of the path
public static final Uri CONTENT_URI =
Uri.parse(“content://com.paad.skeletondatabaseprovid
er/elements”);
• A query made using this form represents a request for all rows, whereas an appended trailing /<rownumber>, represents a request for a single record:
content://com.paad.skeletondatabaseprovider/elements
/5
• To retrieve a row whose _ID is 4 from user dictionary, you can use this content URI:
Uri singleUri =
ContentUris.withAppendedId(UserDictionary.Words.C
ONTENT_URI, 4);
• You often use id values when you've retrieved a set of rows and then want to update or delete one of them
URI Matcher
• Utility class to aid in matching URIs in content providers.
• build up a tree of UriMatcher objects.
• Then when you need to match against a URI, call match(Uri), providing the URL that you have been given. You can use the result to build a query, return a type, insert or delete a row
URI Matcher public methods
void addURI(String authority, String path, int code)
Add a URI to match, and the code to return when this URI is matched.
int match(Uri uri)
Try to match against the path in a url.
URI Matcher
private static final int
private static final
private static final
private static final
private static final
private static final
PEOPLE = 1;
int PEOPLE_ID = 2;
int PEOPLE_PHONES = 3;
int PEOPLE_PHONES_ID = 4;
int PEOPLE_CONTACTMETHODS = 7;
int PEOPLE_CONTACTMETHODS_ID = 8;
private static final int DELETED_PEOPLE = 20;
private static final int PHONES = 9;
private static final int PHONES_ID = 10;
private static final int PHONES_FILTER = 14;
private static final int CONTACTMETHODS = 18;
private static final int CONTACTMETHODS_ID = 19;
private static final int CALLS = 11;
private static final int CALLS_ID = 12;
private static final int CALLS_FILTER = 15;
private static final UriMatcher sURIMatcher =
new UriMatcher(UriMatcher.NO_MATCH);
static
{
sURIMatcher.addURI("contacts", "people", PEOPLE);
sURIMatcher.addURI("contacts", "people/#", PEOPLE_ID);
sURIMatcher.addURI("contacts", "people/#/phones",
PEOPLE_PHONES);
sURIMatcher.addURI("contacts", "people/#/phones/#",
PEOPLE_PHONES_ID);
…
sURIMatcher.addURI("contacts", "contact_methods",
CONTACTMETHODS);
sURIMatcher.addURI("contacts", "contact_methods/#",
CONTACTMETHODS_ID);
sURIMatcher.addURI("call_log", "calls", CALLS);
}
public String getType(Uri url)
{
int match = sURIMatcher.match(url);
switch (match)
{
case PEOPLE:
return "vnd.android.cursor.dir/person";
case PEOPLE_ID:
return "vnd.android.cursor.item/person";
... snip ...
return "vnd.android.cursor.dir/snail‐mail";
case PEOPLE_ADDRESS_ID:
return "vnd.android.cursor.item/snail‐mail";
default:
return null;
}
}
Instead of
public String getType(Uri url)
{
List pathSegments = url.getPathSegments();
if (pathSegments.size() >= 2) {
if ("people".equals(pathSegments.get(1))) {
if (pathSegments.size() == 2) {
return "vnd.android.cursor.dir/person";
} else if (pathSegments.size() == 3) {
return "vnd.android.cursor.item/person";
... snip ...
return "vnd.android.cursor.dir/snail‐mail";
} else if (pathSegments.size() == 3) {
return "vnd.android.cursor.item/snail‐mail";
}
}
}
return null;
}
URIMatcher – All rows/single row
// Create the constants used to differentiate between the different URI
// requests.
private static final int ALLROWS = 1;
private static final int SINGLE_ROW = 2;
private static final UriMatcher uriMatcher;
// Populate the UriMatcher object, where a URI ending in
// ‘elements’ will correspond to a request for all items,
// and ‘elements/[rowID]’ represents a single row.
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(“com.paad.skeletondatabaseprovider”,
“elements”, ALLROWS);
uriMatcher.addURI(“com.paad.skeletondatabaseprovider”,
“elements/#”, SINGLE_ROW);
}
private static final int PEOPLE = 1;
private static final int PEOPLE_ID = 2;
private static final int PEOPLE_PHONES = 3;
private static final int PEOPLE_PHONES_ID = 4;
private static final int PEOPLE_CONTACTMETHODS = 7;
private static final int PEOPLE_CONTACTMETHODS_ID = 8;
private static final int DELETED_PEOPLE = 20;
private static final int PHONES = 9;
private static final int PHONES_ID = 10;
private static final int PHONES_FILTER = 14;
private static final int CONTACTMETHODS = 18;
private static final int CONTACTMETHODS_ID = 19;
private static final int CALLS = 11;
private static final int CALLS_ID = 12;
private static final int CALLS_FILTER = 15;
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static
{
sURIMatcher.addURI("contacts", "people", PEOPLE);
sURIMatcher.addURI("contacts", "people/#", PEOPLE_ID);
sURIMatcher.addURI("contacts", "people/#/phones", PEOPLE_PHONES);
sURIMatcher.addURI("contacts", "people/#/phones/#", PEOPLE_PHONES_ID);
sURIMatcher.addURI("contacts", "people/#/contact_methods", PEOPLE_CONTACTMETHODS);
sURIMatcher.addURI("contacts", "people/#/contact_methods/#", PEOPLE_CONTACTMETHODS_ID);
sURIMatcher.addURI("contacts", "deleted_people", DELETED_PEOPLE);
sURIMatcher.addURI("contacts", "phones", PHONES);
sURIMatcher.addURI("contacts", "phones/filter/*", PHONES_FILTER);
sURIMatcher.addURI("contacts", "phones/#", PHONES_ID);
sURIMatcher.addURI("contacts", "contact_methods", CONTACTMETHODS);
sURIMatcher.addURI("contacts", "contact_methods/#", CONTACTMETHODS_ID);
sURIMatcher.addURI("call_log", "calls", CALLS);
sURIMatcher.addURI("call_log", "calls/filter/*", CALLS_FILTER);
sURIMatcher.addURI("call_log", "calls/#", CALLS_ID);
}
Content Provider Contract class
• Good practice to build a contract class for a content provider – UserDicitionary
• Class for each table exposed by the content provider: UserDicitionary.Words
• Expose its authority through CONTENT_URI property:
Public static final CONTENT_URI = content://user_dictionary/words
Building a query
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
// If this is a row query, limit the result set to the passed in row.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
queryBuilder.appendWhere(KEY_ID + “=” + rowID);
default: break;
}
More queries on Words Table
// A "projection" defines the columns that will be returned for each row
String[] mProjection =
{
UserDictionary.Words._ID, // Contract class constant for the _ID column name
UserDictionary.Words.WORD, // Contract class constant for the word column name
UserDictionary.Words.LOCALE // Contract class constant for the locale column name
};
// Defines a string to contain the selection clause
String mSelectionClause = null;
// Initializes an array to contain selection arguments
String[] mSelectionArgs = {""};
UserDictionary Query contd.
/*
* This defines a one‐element String array to contain the selection argument.
*/
String[] mSelectionArgs = {""};
// Gets a word from the UI – user is looking for a word
mSearchString = mSearchWord.getText().toString();
// Remember to insert code here to check for invalid or malicious input.
// If the word is the empty string, gets everything
if (TextUtils.isEmpty(mSearchString)) {
// Setting the selection clause to null will return all words
mSelectionClause = null;
mSelectionArgs[0] = "";
} else {
// Constructs a selection clause that matches the word that the user entered.
mSelectionClause = UserDictionary.Words.WORD + " = ?";
// Moves the user's input string to the selection arguments.
mSelectionArgs[0] = mSearchString;
}
// Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // The content URI of the words table
mProjection, // The columns to return for each row
mSelectionClause
// Either null, or the word the user entered
mSelectionArgs, // Either empty, or the string the user entered
mSortOrder); // The sort order for the returned rows
// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
/*
* Insert code here to handle the error. Be sure not to use the cursor! You may want to
* call android.util.Log.e() to log this error.
*
*/
// If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {
/*
* Insert code here to notify the user that the search was unsuccessful. This isn't necessarily
* an error. You may want to offer the user the option to insert a new row, or re‐type the
* search term.
*/
} else {
// Insert code here to do something with the results
}
• Query analogous to:
SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
• Separation of selectionClause and selectionArgs
prevents WQL injection attacks.
• // Constructs a selection clause with a replaceable parameter
String mSelectionClause = "var = ?";
• // Defines an array to contain the selection arguments
String[] selectionArgs = {""};
• // Sets the selection argument to the user's input
selectionArgs[0] = mUserInput;
• user could enter "nothing; DROP TABLE *;“
• results in the selection clause var = nothing; DROP TABLE *;
Display results with cursor
• Since a Cursor is a "list" of rows, a good way to display the contents of a Cursor is to link it to a ListView via a SimpleCursorAdapter.
// Defines a list of columns to retrieve from the Cursor and load into an output
row
String[] mWordListColumns =
{
UserDictionary.Words.WORD, // Contract class constant containing the
word column name
UserDictionary.Words.LOCALE // Contract class constant containing the
locale column name
};
// Defines a list of View IDs that will receive the Cursor columns for each row
int[] mWordListItems = { R.id.dictWord, R.id.locale};
// Creates a new SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(
getApplicationContext(), // The application's Context object
R.layout.wordlistrow, // A layout in XML for one row in the ListView
mCursor, // The result from the query
mWordListColumns, // A string array of column names in the cursor
mWordListItems, // An integer array of view IDs in the row layout
0); // Flags (usually none are needed)
// Sets the adapter for the ListView
mWordList.setAdapter(mCursorAdapter);
Content Provider Permissions
• A provider's application can specify permissions that other applications must have in order to access the provider's data. (in Manifest file)
• If a provider's application doesn't specify any permissions, then other applications have no access to the provider's data. However, components in the provider's application always have full read and write access, regardless of the specified permissions.
• To get the permissions needed to access a provider, an application requests them with a <uses‐permission>
element in its manifest file. When the Android Package Manager installs the application, a user must approve all of the permissions
<uses‐permission android:name="android.permission.READ_USER_DICTION
ARY">
Inserting data
• ContentResolver.insert() method.
// Defines a new Uri object that receives the result of the insertion
Uri mNewUri;
...
// Defines an object to contain the new values to insert
ContentValues mNewValues = new ContentValues();
/*
* Sets the values of each column and inserts the word. The arguments to the "put"
* method are "column name" and "value"
*/
mNewValues.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues.put(UserDictionary.Words.WORD, "insert");
mNewValues.put(UserDictionary.Words.FREQUENCY, "100");
mNewUri = getContentResolver().insert(
UserDictionary.Word.CONTENT_URI, // the user dictionary content URI
mNewValues
// the values to insert
);
• The content URI returned in newUri identifies the newly‐added row, with the following format:
content://user_dictionary/words/<id_value>
• To get the value of _ID from the returned Uri, call ContentUris.parseId()
• Similar update and delete methods.
Provider Data Types
• The User Dictionary Provider offers only text, but providers can also offer the following formats:
– integer
– long integer (long)
– floating point
– long floating point (double)
• Another data type that providers often use is Binary Large OBject (BLOB) implemented as a 64KB byte array.
Creating the Content Provider’s Database
• To initialize the data source you plan to access through the Content Provider, override the onCreate method:
private MySQLiteOpenHelper myOpenHelper;
@Override
public boolean onCreate() {
// Construct the underlying database.
// Defer opening the database until you need to perform
// a query or transaction.
myOpenHelper = new MySQLiteOpenHelper(getContext(),
MySQLiteOpenHelper.DATABASE_NAME, null,
MySQLiteOpenHelper.DATABASE_VERSION);
return true;
}
Implementing Content Provider Queries
• must implement the query and getType methods.
• Content Resolvers use these methods to access the underlying data.
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// Open the database.
SQLiteDatabase db;
try {
db = myOpenHelper.getWritableDatabase();
} catch (SQLiteException ex) {
db = myOpenHelper.getReadableDatabase();
}
// Replace these with valid SQL statements if necessary.
String groupBy = null;
String having = null;
// Use an SQLite Query Builder to simplify constructing the
// database query.
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
// If this is a row query, limit the result set to the passed in row.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
queryBuilder.appendWhere(KEY_ID + “=” + rowID);
default: break;
}
// Specify the table on which to perform the query. // This can be a specific table or a join as required.
queryBuilder.setTables(
MySQLiteOpenHelper.DATABASE_TABLE);
// Execute the query.
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, groupBy, having, sortOrder);
// Return the result Cursor.
return cursor;
}
• specify a MIME type to identify the data returned. Override the getType method to return a string that uniquely describes your data type.
• The type returned should include two forms, one for a single entry and another for all the entries, following these forms:
• Single item:
vnd.android.cursor.item/vnd.<companyname>.<con
tenttype>
• All items:
vnd.android.cursor.dir/vnd.<companyname>.<conte
nttype>
@Override
public String getType(Uri uri) {
// Return a string that identifies the MIME type
// for a Content Provider URI
switch (uriMatcher.match(uri)) {
case ALLROWS:
return “vnd.android.cursor.dir/vnd.paad.elemental”;
case SINGLE_ROW:
return “vnd.android.cursor.item/vnd.paad.elemental”;
default:
throw new IllegalArgumentException(“Unsupported URI: “ +
uri);
}
}
/*
* Sets the code for a single row to 2. In this case, the "#" wildcard is
* used. "content://com.example.app.provider/table3/3" matches, but
* "content://com.example.app.provider/table3 doesn't.
*/
sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
...
// Implements ContentProvider.query()
public Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {
...
/*
* Choose the table to query and a sort order based on the code returned for the incoming
* URI. Here, too, only the statements for table 3 are shown.
*/
switch (sUriMatcher.match(uri)) {
// If the incoming URI was for all of table3
case 1:
if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
break;
// If the incoming URI was for a single row
case 2:
/*
* Because this URI was for a single row, the _ID value part is
* present. Get the last path segment from the URI; this is the _ID value.
* Then, append the value to the WHERE clause for the query
*/
selection = selection + "_ID = " uri.getLastPathSegment();
break;
• default:
...
// If the URI is not recognized, you should do some error handling here.
}
// call the code to actually do the query
}
Content Provider Transactions
• To expose delete, insert, and update transactions on your Content Provider, implement the corresponding delete, insert, and update methods.
• Like the query method, these methods are used by Content Resolvers to perform transactions on the underlying data without knowing its implementation
• when dataset modified, it’s good practice to call the Content Resolver’s notifyChange method. This will notify any Content Observers, registered for a given Cursor using the Cursor.registerContentObserver
method, that the underlying table (or a particular row) has been removed, added, or updated
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Open a read / write database to support the transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// If this is a row URI, limit the deletion to the specified row.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
selection = KEY_ID + “=” + rowID
+ (!TextUtils.isEmpty(selection) ?
“ AND (“ + selection + ‘)’ : “”);
default: break;
}
// To return the number of deleted items you must specify a where
// clause. To delete all rows and return a value pass in “1”.
if (selection == null)
selection = “1”;
// Perform the deletion.
int deleteCount = db.delete(MySQLiteOpenHelper.DATABASE_TABLE,
selection, selectionArgs);
// Notify any observers of the change in the data set.
getContext().getContentResolver().notifyChange(uri, null);
// Return the number of deleted items.
return deleteCount;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// Open a read / write database to support the transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// To add empty rows to your database by passing in an empty
// Content Values object you must use the null column hack
// parameter to specify the name of the column that can be
// set to null.
String nullColumnHack = null;
// Insert the values into the table
long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE,
nullColumnHack, values);
// Construct and return the URI of the newly inserted row.
if (id > ‐1) {
// Construct and return the URI of the newly inserted row.
Uri insertedId = ContentUris.withAppendedId(CONTENT_URI, id);
// Notify any observers of the change in the data set.
getContext().getContentResolver().notifyChange
(insertedId, null);
return insertedId;
}
else
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// Open a read / write database to support the transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// If this is a row URI, limit the deletion to the specified row.
Switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
selection = KEY_ID + “=” + rowID
+ (!TextUtils.isEmpty(selection) ?
“ AND (“ + selection + ‘)’ : “”);
default: break;
}
// Perform the update.
int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TA
BLE,
values, selection, selectionArgs);
// Notify any observers of the change in the data set.
getContext().getContentResolver().notifyChange(uri, null);
return updateCount;
}
Native Calendar Content Provider
URIs:
Events.CONTENT_URI
Exposed Tables
• CalendarContract.Calendars ‐ Each row in this table contains the details for a single calendar, such as the name, color, sync information
• CalendarContract.Events ‐ row in this table has the information for a single even, event title, location, start time, end time
• CalendarContract.Instances ‐ row in this table represents a single event occurrence ‐ start and end times
• CalendarContract.Attendees ‐ row represents a single guest of an event
• CalendarContract.Reminders ‐ row represents a single alert for an event
Contract class example
public class DatabaseParis { public static final class Contract { public static final String COLUMN_LATITUDE = "latitude"; public static final String COLUMN_LONGITUDE = "longitude"; public static final String COLUMN_NAME = "name"; }
Permissions
• <?xml version="1.0" encoding="utf‐8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/
android"...>
<uses‐sdk android:minSdkVersion="14" />
<uses‐permission android:name="android.permission.READ_CALENDAR" />
<uses‐permission android:name="android.permission.WRITE_CALENDAR
" />
...
</manifest>
Query a Calendar
Get calendars owned by a particular user
// Projection array. Creating indices for this array instead of doing
// dynamic lookups improves performance.
public static final String[] EVENT_PROJECTION = new
String[] {
Calendars._ID,
// 0
Calendars.ACCOUNT_NAME,
// 1
Calendars.CALENDAR_DISPLAY_NAME,
// 2
Calendars.OWNER_ACCOUNT
// 3
};
// The indices for the projection array above.
private static final int PROJECTION_ID_INDEX = 0;
private static final int PROJECTION_ACCOUNT_NAME_INDEX = 1;
private static final int PROJECTION_DISPLAY_NAME_INDEX = 2;
private static final int PROJECTION_OWNER_ACCOUNT_INDEX =
3;
• query is looking for calendars that have ACCOUNT_NAME = [email protected]
• ACCOUNT_TYPE: com.google
• OWNER_ACCOUNT: [email protected]
• f you want to see all calendars that a user has viewed, not just calendars the user owns, omit the OWNER_ACCOUNT
• The query returns a Cursor object
// Run query
Cursor cur = null;
ContentResolver cr = getContentResolver();
Uri uri = Calendars.CONTENT_URI;
String selection = "((" + Calendars.ACCOUNT_NAME + " = ?) AND ("
+ Calendars.ACCOUNT_TYPE + " = ?) AND ("
+ Calendars.OWNER_ACCOUNT + " = ?))";
String[] selectionArgs = new String[]
{"[email protected]", "com.google",
"[email protected]"};
// Submit the query and get a Cursor object back. cur = cr.query(uri, EVENT_PROJECTION, selection,
selectionArgs, null);
// Use the cursor to step through the returned records
while (cur.moveToNext()) {
long calID = 0;
String displayName = null;
String accountName = null;
String ownerName = null;
// Get the field values
calID = cur.getLong(PROJECTION_ID_INDEX);
displayName =
cur.getString(PROJECTION_DISPLAY_NAME_INDEX);
accountName =
cur.getString(PROJECTION_ACCOUNT_NAME_INDEX);
ownerName =
cur.getString(PROJECTION_OWNER_ACCOUNT_INDEX);
// Do something with the values...
...
}
Modifying a Calendar
• To perform an update of an calendar, you can provide the _ID of the calendar either as an appended ID to the Uri(withAppendedID())
• The selection should start with “_id=?” and the first selectionArg should be the _ID of the calendar
private static final String DEBUG_TAG = "MyActivity";
...
long calID = 2;
ContentValues values = new ContentValues();
// The new display name for the calendar
values.put(Calendars.CALENDAR_DISPLAY_NAME, "Trevor's Calendar");
Uri updateUri =
ContentUris.withAppendedId(Calendars.CONTENT_URI, calID);
int rows = getContentResolver().update(updateUri, values,
null, null);
Log.i(DEBUG_TAG, "Rows updated: " + rows);
Calendar Intents
• No permissions needed if using intents
ACTION
VIEW
URIs
content://com.android.calendar/ Open calendar to the time/<ms_since_epoch>
time specified You can also refer to the URI with by<ms_since_epoch>.
CalendarContract.CONTENT_URI. For an example of using this intent, see Using intents to view calendar data.
Extras
None.
VIEW
EDIT
content://com.android.calendar View the event /events/<event_id>
specified by You can also refer to the URI <event_id>.
with Events.CONTENT_URI. For an example of using this intent, see Using intents to view calendar data.
CalendarContract.EX
TRA_EVENT_BEGIN_
TIME
content://com.android.calendar Edit the event /events/<event_id>
specified by You can also refer to the URI <event_id>.
with Events.CONTENT_URI. For an example of using this intent, see Using an intent to edit an event.
CalendarContract.EX
TRA_EVENT_BEGIN_
TIME
CalendarContract.EX
TRA_EVENT_END_TI
ME
CalendarContract.EX
TRA_EVENT_END_TI
ME
Contacts Provider
Exposed Tables
• ContactsContract.Contacts
Rows representing different people, based on aggregations of raw contact rows.
• ContactsContract.RawContacts
Rows containing a summary of a person's data, specific to a user account and type.
• ContactsContract.Data
Rows containing the details for raw contact, such as email addresses or phone numbers.
Creating a Content Provider
• Design data storage – SQLite table, File?
• Define concrete implementation of ContentProvider
• Define the provider's authority string, its content URIs, and column names.
• Add other optional pieces, such as sample data or an implementation of AbstractThreadedSyncAdapter that can synchronize data between the provider and cloud‐based data.
• SQLiteOpenHelper class helps you create databases; SQLiteDatabase class is the base class for accessing databases.
• Use BaseColumns._ID to use ListView
• to provide bitmap images or other very large pieces of file‐oriented data, store the data in a file and then provide it indirectly rather than storing it directly in a table ‐
openFileDescriptor(Uri uri, String mode); openInputStream(Uri uri); openOutputStream(Uri uri) –
ContentResolver public methods.
• Use the Binary Large OBject (BLOB) data type to store data that varies in size or has a varying structure
• use a BLOB to implement a schema‐independent table. ‐ a primary key column, a MIME type column, and one or more generic columns as BLOB
Content URIs
•content://com.example.app.provider/table1: A
table called table1.
•content://com.example.app.provider/table2/data
set1: A table called dataset1.
•content://com.example.app.provider/table2/data
set2: A table called dataset2.
•content://com.example.app.provider/table3: A
table called table3.
content://com.example.app.provider/* ‐any data
content://com.example.app.provider/table2/*
content://com.example.app.provider/table3/6
public class ExampleProvider extends ContentProvider {
...
// Creates a UriMatcher object.
private static final UriMatcher sUriMatcher;
...
/* The calls to addURI() go here, for all of the content URI patterns that the provider
* should recognize. For this snippet, only the calls for table 3 are shown.
*/
...
/* Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
* in the path
*/
sUriMatcher.addURI("com.example.app.provider", "table3", 1);
/*
* Sets the code for a single row to 2. In this case, the "#" wildcard is
* used. "content://com.example.app.provider/table3/3" matches, but
* "content://com.example.app.provider/table3 doesn't. */
sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
/
* Choose the table to query and a sort order based on the code returned for the incoming
* URI. Here, too, only the statements for table 3 are shown.
*/
switch (sUriMatcher.match(uri)) {
// If the incoming URI was for all of table3
case 1:
if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
break;
// If the incoming URI was for a single row
case 2:
/*
* Because this URI was for a single row, the _ID value part is
* present. Get the last path segment from the URI; this is the _ID value.
* Then, append the value to the WHERE clause for the query
*/
selection = selection + "_ID = " uri.getLastPathSegment();
break;
default:
...
// If the URI is not recognized, you should do some error handling here.
}
// call the code to actually do the query
}
public class ExampleProvider extends ContentProvider
/*
* Defines a handle to the database helper object. The MainDatabaseHelper class is defined
* in a following snippet.
*/
private MainDatabaseHelper mOpenHelper;
// Defines the database name
private static final String DBNAME = "mydb";
// Holds the database object
private SQLiteDatabase db;
public boolean onCreate() {
/*
* Creates a new helper object. This method always returns quickly.
* Notice that the database itself isn't created or opened
* until SQLiteOpenHelper.getWritableDatabase is called
*/
mOpenHelper = new MainDatabaseHelper(
getContext(),
// the application context
DBNAME,
// the name of the database)
null,
// uses the default SQLite cursor
1
// the version number
);
return true;
}
// Implements the provider's insert method
public Cursor insert(Uri uri, ContentValues
values) {
// Insert code here to determine which table to open, handle error‐checking, and so forth
...
/*
* Gets a writeable database. This will trigger its creation if it doesn't already exist.
*
*/
db = mOpenHelper.getWritableDatabase();
}
}
// A string that defines the SQL statement for creating a table
private static final String SQL_CREATE_MAIN = "CREATE TABLE " +
"main " +
// Table's name
"(" +
// The columns in the table
" _ID INTEGER PRIMARY KEY, " +
" WORD TEXT"
" FREQUENCY INTEGER " +
" LOCALE TEXT )";
...
/**
* Helper class that actually creates and manages the provider's underlying data repository.
*/
protected static final class MainDatabaseHelper extends
SQLiteOpenHelper {
/*
* Instantiates an open helper for the provider's SQLite data repository
* Do not do database creation and upgrade here.
*/
MainDatabaseHelper(Context context) {
super(context, DBNAME, null, 1);
}
/*
* Creates the data repository. This is called when the provider attempts to open the
* repository and SQLite reports that it doesn't exist.
*/
public void onCreate(SQLiteDatabase db) {
// Creates the main table
db.execSQL(SQL_CREATE_MAIN);
}
}
Implement MIME Types
getType()
One of the required methods that you must implement for
any provider.
getStreamTypes()
A method that you're expected to implement if your
provider offers files.
Implementing Permissions
One permission that controls both read and write access to
the entire provider, specified with the
android:permission attribute of the <provider> element.
com.example.app.provider.permission.MY_PROVIDER
A read permission and a write permission for the entire
provider. You specify them with the
android:readPermission and android:writePermission
attributes of the <provider> element. They take precedence
over the permission required by android:permission.
com.example.app.provider.permission.READ_PROVIDER.
Wrapping Files in a Content Provider
• Embed them within a database table column
• Raw file hard to embed into the database table, but a content URI for the file (path name) can be used.
• Use a column by name _data.
• This column should not be used by client applications.
• Override the openFile handler to provide a ParcelFileDescriptor when the Content Resolver requests the file associated with that record.
• Common for a Content Provider to include two tables, one that is used only to store the external files, and another that includes a user‐facing column containing a URI reference to the rows in the file table.
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
// Find the row ID and use it as a filename.
String rowID = uri.getPathSegments().get(1);
// Create a file object in the application’s external
// files directory.
String picsDir = Environment.DIRECTORY_PICTURES;
File file =
new File(getContext().getExternalFilesDir(picsDir), rowID);
// If the file doesn’t exist, create it now.
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
Log.d(TAG, “File creation failed: “ + e.getMessage());
}
}
// Translate the mode parameter to the corresponding Parcel File
// Descriptor open mode.
int fileMode = 0;
if (mode.contains(“w”))
fileMode |= ParcelFileDescriptor.MODE_WRITE_ONLY;
if (mode.contains(“r”))
fileMode |= ParcelFileDescriptor.MODE_READ_ONLY;
if (mode.contains(“+”))
fileMode |= ParcelFileDescriptor.MODE_APPEND;
// Return a Parcel File Descriptor that represents the file.
return ParcelFileDescriptor.open(file, fileMode);
}
Accessing Files Stored in Content Providers
• Content Providers represent large files as fully qualified URIs rather than raw file blobs; however, this is abstracted away when using the Content Resolver
• To access a file stored in, or to insert a new file into, a Content Provider, simply use the Content Resolver’s openOutputStream or openInputStream
methods, respectively, passing in the URI to the Content Provider row containing the file you require.
public void addNewHoardWithImage(String hoardName, float hoardValue,
boolean hoardAccessible, Bitmap bitmap) {
// Create a new row of values to insert.
ContentValues newValues = new ContentValues();
// Assign values for each column.
newValues.put(MyHoardContentProvider.KEY_GOLD_HOA
RD_NAME_COLUMN,
hoardName);
newValues.put(MyHoardContentProvider.KEY_GOLD_HOA
RDED_COLUMN,
hoardValue);
newValues.put(
MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE
_COLUMN,
hoardAccessible);
// Get the Content Resolver
ContentResolver cr = getContentResolver();
// Insert the row into your table
Uri myRowUri =
cr.insert(MyHoardContentProvider.CONTENT_URI, newValues);
try {
// Open an output stream using the new row’s URI.
OutputStream outStream = cr.openOutputStream(myRowUri);
// Compress your bitmap and save it into your provider.
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
}
catch (FileNotFoundException e) {
Log.d(TAG, “No file found for this record.”);
}
}
public Bitmap getHoardImage(long rowId) {
Uri myRowUri =
ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI,
rowId);
try {
// Open an input stream using the new row’s URI.
InputStream inStream =
getContentResolver().openInputStream(myRowUri);
// Make a copy of the Bitmap.
Bitmap bitmap = BitmapFactory.decodeStream(inStream);
return bitmap;
}
catch (FileNotFoundException e) {
Log.d(TAG, “No file found for this record.”);
}
return null;
}
Content sharing
• If data needs to be shared between a small set of apps, there are easier mechanisms
Simple Data Sharing
• Use intents with ACTION_SEND to send data from one activity to another (even across process boundaries)
• Send text content ‐ the built‐in Browser app can share the URL of the currently‐displayed page
• useful for sharing an article or website with friends via email or social networking
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, "This is my text to send.");
sendIntent.setType("text/plain");
startActivity(sendIntent);
If there's an installed application with a filter that matches ACTION_SEND and MIME type text/plain, the Android system will run it
Binary content sharing
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, uriToImage);
shareIntent.setType("image/jpeg");
startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.send_to)));
Sharing files
• Apps often have a need to offer one or more of their files to another app. An image gallery may want to offer files to image editors, or a file management app may want to allow users to copy and paste files between areas in external storage.
• Use content URI for files. The Android FileProvider component provides the method getUriForFile() for generating a file's content URI
Setting Up File Sharing
• To securely offer a file from your app to another app, you need to configure your app to offer a secure handle to the file, in the form of a content URI.
• Defining a FileProvider for your app requires an entry in your manifest. This entry specifies the authority to use in generating content URIs, as well as the name of an XML file that specifies the directories your app can share.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<application
...>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.myapp.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta‐data
android:name="android.support.FILE_PROVIDER_PATHS
"
android:resource="@xml/filepaths" />
</provider>
...
</application>
</manifest>
Specify Sharable Directories
• To specify the directories, start by creating the file filepaths.xml in the res/xml/ subdirectory of your project
• specify the directories by adding an XML element for each directory
<paths>
<files‐path path="images/" name="myimages" />
</paths>
• the <files‐path> tag shares directories within the files/ directory of your app's internal storage
• The path attribute shares the images/ subdirectory of files/. The name attribute tells the FileProvider to add the path segment myimages to content URIs for files in the files/images/ subdirectory
• The <paths> element can have multiple children, each specifying a different directory to share. • In addition to the <files‐path> element, you can use the <external‐path> element to share directories in external storage, and the <cache‐
path> element to share directories in your internal cache directory.
• if you define a FileProvider according to the snippets here, and you request a content URI for the file default_image.jpg, FileProvider returns the following URI: content://com.example.myapp.fileprovider/myi
mages/default_image.jpg
Receive File Requests
• To receive requests for files from client apps and respond with a content URI, your app should provide a file selection Activity.
• Client apps start this Activity by calling startActivityForResult() with an Intent containing the action ACTION_PICK.
File Selection Activity in Manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<application>
...
<activity
android:name=".FileSelectActivity"
android:label="@"File Selector" >
<intent‐filter>
<action
android:name="android.intent.action.PICK"/>
<category
android:name="android.intent.category.DEFAULT"/>
<category
android:name="android.intent.category.OPENABLE"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="image/*"/>
</intent‐filter>
</activity>
Define the file selection Activity in code
public class MainActivity extends Activity {
// The path to the root of this app's internal storage
private File mPrivateRootDir;
// The path to the "images" subdirectory
private File mImagesDir;
// Array of files in the images subdirectory
File[] mImageFiles;
// Array of filenames corresponding to mImageFiles
String[] mImageFilenames;
// Initialize the Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Set up an Intent to send back to apps that request a file
mResultIntent =
new Intent("com.example.myapp.ACTION_RETURN_FILE");
// Get the files/ subdirectory of internal storage
mPrivateRootDir = getFilesDir();
// Get the files/images subdirectory;
mImagesDir = new File(mPrivateRootDir, "images");
// Get the files in the images subdirectory
mImageFiles = mImagesDir.listFiles();
// Set the Activity's result to null to begin with
setResult(Activity.RESULT_CANCELED, null);
/*
* Display the file names in the ListView mFileListView.
* Back the ListView with the array mImageFilenames, which
* you can create by iterating through mImageFiles and
* calling File.getAbsolutePath() for each File
*/
...
}
...
}
Respond to a File Selection
• Once a user selects a shared file, your application must determine what file was selected and then generate a content URI for the file. • Since the Activity displays the list of available files in a ListView, when the user clicks a file name the system calls the method onItemClick(), in which you can get the selected file.
• In onItemClick(), get a File object for the file name of the selected file and pass it as an argument to getUriForFile(), along with the authority that you specified in the <provider> element for the FileProvider.
protected void onCreate(Bundle savedInstanceState) {
...
// Define a listener that responds to clicks on a file in the ListView
mFileListView.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
/*
* When a filename in the ListView is clicked, get its
* content URI and send it to the requesting app
*/
public void onItemClick(AdapterView<?> adapterView,
View view,
int position,
long rowId) {
/*
* Get a File for the selected file name.
* Assume that the file names are in the
* mImageFilename array.
*/
File requestFile = new File(mImageFilename[position]);
}
}
});
...
/*
* Most file‐related method calls need to be in
* try‐catch blocks.
*/
// Use the FileProvider to get a content URI
try {
fileUri = FileProvider.getUriForFile(
MainActivity.this,
"com.example.myapp.fileprovider",
requestFile);
} catch (IllegalArgumentException e) {
Log.e("File Selector",
"The selected file can't be shared: " +
clickedFilename);
}
...
Grant Permissions for the File
• Now that you have a content URI for the file you want to share with another app, you need to allow the client app to access the file. • To allow access, grant permissions to the client app by adding the content URI to an Intent and then setting permission flags on the Intent
• The permissions you grant are temporary and expire automatically when the receiving app's task stack is finished.
protected void onCreate(Bundle savedInstanceState) {
...
// Define a listener that responds to clicks in the ListView
mFileListView.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view,
int position,
long rowId) {
...
if (fileUri != null) {
// Grant temporary read permission to the content URI
mResultIntent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
...
}
...
});
...
}
Share the File with the Requesting App
• To share the file with the app that requested it, pass the Intent containing the content URI and permissions to setResult(). When the Activity you have just defined is finished, the system sends the Intent containing the content URI to the client app
protected void onCreate(Bundle savedInstanceState) {
...
// Define a listener that responds to clicks on a file in the ListView
mFileListView.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view,
int position,
long rowId) {
...
if (fileUri != null) {
...
// Put the Uri and MIME type in the result Intent
mResultIntent.setDataAndType(
fileUri,
getContentResolver().getType(fileUri));
// Set the result
MainActivity.this.setResult(Activity.RESULT_OK,
mResultIntent);
} else {
mResultIntent.setDataAndType(null, "");
MainActivity.this.setResult(RESULT_CANCELED,
mResultIntent);
}
}
});
Requesting a Shared File
• When an app wants to access a file shared by another app, the requesting app (the client) usually sends a request to the app sharing the files (the server)
• the request starts an Activity in the server app that displays the files it can share. • The user picks a file, after which the server app returns the file's content URI to the client app.
Send a Request for the File
• To request a file from the server app, the client app calls startActivityForResult with an Intent containing the action such as ACTION_PICK and a MIME type that the client app can handle.
public class MainActivity extends Activity {
private Intent mRequestFileIntent;
private ParcelFileDescriptor mInputPFD;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRequestFileIntent = new Intent(Intent.ACTION_PICK);
mRequestFileIntent.setType("image/jpg");
...
}
...
}
protected void requestFile() {
/**
* When the user requests a file, send an Intent to the
* server app.
* files.
*/
startActivityForResult(mRequestFileIntent, 0);
...
}
...
Access the Requested File
• The server app sends the file's content URI back to the client app in an Intent.
• This Intent is passed to the client app in its override of onActivityResult(). Once the client app has the file's content URI, it can access the file by getting its FileDescriptor.
/*
* When the Activity of the app that hosts files sets a result and calls
* finish(), this method is invoked. The returned Intent contains the
* content URI of a selected file. The result code indicates if the
* selection worked or not.
*/
@Override
public void onActivityResult(int requestCode, int resultCode,
Intent returnIntent) {
// If the selection didn't work
if (resultCode != RESULT_OK) {
// Exit without doing anything else
return;
} else {
// Get the file's content URI from the incoming Intent
Uri returnUri = returnIntent.getData();
/*
* Try to open the file for "read" access using the
* returned URI. If the file isn't found, write to the
* error log and return.
*/
try {
/*
* Get the content resolver instance for this context, and use it
* to get a ParcelFileDescriptor for the file.
*/
mInputPFD = getContentResolver().openFileDescriptor(returnUri, "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e("MainActivity", "File not found.");
return;
}
// Get a regular file descriptor for the file
FileDescriptor fd = mInputPFD.getFileDescriptor();
...
}
}
Storage Access Framework (SAF)
• With Android 4.4 (API Level 19)
• browse and open documents, images, and other files across all of their preferred document storage providers
• SAF has Document provider—A content provider that allows a storage service (such as Google Drive) to reveal the files it manages
• Client app—A custom app that invokes the ACTION_OPEN_DOCUMENT or ACTION_CREATE_DOCUMENT
• Picker—A system UI that lets users access documents from all document providers
SAF Advantages
• Lets users browse content from all document providers, not just a single app.
• Makes it possible for your app to have long term, persistent access to documents owned by a document provider. Through this access users can add, edit, save, and delete files on the provider.
• Supports multiple user accounts and transient roots such as USB storage providers, which only appear if the drive is plugged in.
• Each document provider reports one or more "roots" which are starting points into exploring a tree of documents: COLUMN_ROOT_ID
• Under each root is a single document. That document points to 1 to N documents, each of which in turn can point to 1 to N documents.
• Indiv. Files/directories exposed through COLUMN_DOCUMENT_ID Photo App SAF
•In the SAF, providers and clients don't interact directly. A client requests
permission to interact with files (that is, to read, edit, create, or delete files).
•The interaction starts when an application (in this example, a photo app) fires
the intent ACTION_OPEN_DOCUMENT or ACTION_CREATE_DOCUMENT. The intent
may include filters to further refine the criteria—for example,
"give me all openable files that have the 'image' MIME type.“
•Once the intent fires, the system picker goes to each registered provider
and shows the user the matching content roots.
•The picker gives users a standard interface for accessing documents,
even though the underlying document providers may be very different. For
example, figure shows a Google Drive provider, a USB provider, and
a cloud provider.
A Picker (for Photos)
Client App‐ search for documents
private static final int READ_REQUEST_CODE = 42;
...
/**
* Fires an intent to spin up the "file chooser" UI and select an image.
*/
public void performFileSearch() {
// ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
// browser.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Filter to only show results that can be "opened", such as a
// file (as opposed to a list of contacts or timezones)
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Filter to show only images, using the image MIME data type.
// If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
// To search for all documents available via installed storage providers,
// it would be "*/*".
intent.setType("image/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
Client app‐ process results
@Override
public void onActivityResult(int requestCode, int resultCode,
Intent resultData) {
// The ACTION_OPEN_DOCUMENT intent was sent with the request code
// READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
// response to some other intent, and the code below shouldn't run at all.
if (requestCode == READ_REQUEST_CODE && resultCode ==
Activity.RESULT_OK) {
// The document selected by the user won't be returned in the intent.
// Instead, a URI to that document will be contained in the return intent
// provided to this method as a parameter.
// Pull that URI using resultData.getData().
Uri uri = null;
if (resultData != null) {
uri = resultData.getData();
Log.i(TAG, "Uri: " +
uri.toString());
showImage(uri);
}
}
}
Client App – Examine Document Metadata
public void dumpImageMetaData(Uri uri) {
// The query, since it only applies to a single document, will only return
// one row. There's no need to filter, sort, or select fields, since we want
// all fields for one document.
Cursor cursor = getActivity().getContentResolver()
.query(uri, null, null, null, null, null);
try {
// moveToFirst() returns false if the cursor has 0 rows. Very handy for
// "if there's anything to look at, look at it" conditionals.
if (cursor != null && cursor.moveToFirst()) {
// Note it's called "Display Name". This is
// provider‐specific, and might not necessarily be the file name.
String displayName = cursor.getString(
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAM
E));
Log.i(TAG, "Display Name: " + displayName);
int sizeIndex =
cursor.getColumnIndex(OpenableColumns.SIZE);
// If the size is unknown, the value stored is null. But since an
// int can't be null in Java, the behavior is implementation‐specific,
// which is just a fancy term for "unpredictable". So as
// a rule, check if it's null before assigning to an int. This will
// happen often: The storage API allows for remote files, whose
// size might not be locally known.
String size = null;
if (!cursor.isNull(sizeIndex)) {
// Technically the column stores an int, but cursor.getString()
// will do the conversion automatically.
size = cursor.getString(sizeIndex);
} else {
size = "Unknown";
}
Log.i(TAG, "Size: " + size);
}
} finally {
cursor.close();
}
}
Open a Document ‐ BitMap
private Bitmap getBitmapFromUri(Uri uri) throws
IOException {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(uri,
"r");
FileDescriptor fileDescriptor =
parcelFileDescriptor.getFileDescriptor();
Bitmap image =
BitmapFactory.decodeFileDescriptor(fileDescriptor);
parcelFileDescriptor.close();
return image;
}
Input Stream
private String readTextFromUri(Uri uri) throws IOException {
InputStream inputStream =
getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(new
InputStreamReader(
inputStream));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
fileInputStream.close();
parcelFileDescriptor.close();
return stringBuilder.toString();
}
Client App – Create a Document
// Here are some examples of how you might call this method.
// The first parameter is the MIME type, and the second parameter is the name
// of the file you are creating:
//
// createFile("text/plain", "foobar.txt");
// createFile("image/png", "mypicture.png");
// Unique request code.
private static final int WRITE_REQUEST_CODE = 43;
...
private void createFile(String mimeType, String fileName) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
// Filter to only show results that can be "opened", such as
// a file (as opposed to a list of contacts or timezones).
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Create a file with the requested MIME type.
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_TITLE, fileName);
startActivityForResult(intent, WRITE_REQUEST_CODE);
}
Delete a document:
DocumentsContract.deleteDocument(getContentResol
ver(), uri);
Client App – Edit Document
private static final int EDIT_REQUEST_CODE = 44;
/**
* Open a file for writing and append some text to it.
*/
private void editDocument() {
// ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
// file browser.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Filter to only show results that can be "opened", such as a
// file (as opposed to a list of contacts or timezones).
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Filter to show only text files.
intent.setType("text/plain");
startActivityForResult(intent, EDIT_REQUEST_CODE);
}
Making your own DocumentProvider
<manifest... >
...
<uses‐sdk
android:minSdkVersion="19"
android:targetSdkVersion="19" />
....
<provider
android:name="com.example.android.storageprovider.MyClou
dProvider"
android:authorities="com.example.android.storageprovider
.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS"
android:enabled="@bool/atLeastKitKat">
<intent‐filter>
<action
android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent‐filter>
</provider>
</application>
</manifest>
• android:enabled attribute set to a boolean
value defined in a resource file. The purpose
of this attribute is to disable the provider on
devices running Android 4.3 or lower.
• android:enabled="@bool/atLeastKitKat“
• In your bool.xml resources file
under res/values/, add this line:
<bool name="atLeastKitKat">false</bool>
• In your bool.xml resources file
under res/values/, add this line
<bool name="atLeastKitKat"></bool>
Android 4.3 and lower
<intent‐filter>
<action
android:name="android.intent.action.GET_CONTENT"
/>
<category
android:name="android.intent.category.OPENABLE"
/>
<category
android:name="android.intent.category.DEFAULT"
/>
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent‐filter>
Contracts
• SAF provides these contract classes for you, so you don't need to write your own:
• DocumentsContract.Document
• DocumentsContract.Root
• columns you might return in a cursor when your document provider is queried for documents or the root – next slide
private static final String[] DEFAULT_ROOT_PROJECTION =
new String[]{Root.COLUMN_ROOT_ID,
Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS, Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES,};
private static final String[] DEFAULT_DOCUMENT_PROJECTION =
new
String[]{Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};
• Subclass DocumentsProvider
• Implement –
• queryRoots()
• queryChildDocuments()
• queryDocument()
•openDocument()
queryRoots
@Override
public Cursor queryRoots(String[] projection) throws
FileNotFoundException {
// Create a cursor with either the requested fields, or the default
// projection if "projection" is null.
final MatrixCursor result =
new
MatrixCursor(resolveRootProjection(projection));
// It's possible to have multiple roots (e.g. for multiple accounts in the
// same app) ‐‐ just add multiple cursor rows.
// Construct one row for a root called "MyCloud".
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, ROOT);
row.add(Root.COLUMN_SUMMARY,
getContext().getString(R.string.root_summary));
// FLAG_SUPPORTS_CREATE means at least one directory under the root supports
// creating documents. FLAG_SUPPORTS_RECENTS means your application's most
// recently used documents will show up in the "Recents" category.
// FLAG_SUPPORTS_SEARCH allows users to search all documents the application
// shares.
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
Root.FLAG_SUPPORTS_RECENTS |
Root.FLAG_SUPPORTS_SEARCH);
// COLUMN_TITLE is the root title (e.g. Gallery, Drive).
row.add(Root.COLUMN_TITLE,
getContext().getString(R.string.title));
// This document id cannot change once it's shared.
row.add(Root.COLUMN_DOCUMENT_ID,
getDocIdForFile(mBaseDir));
// The child MIME types are used to filter the roots and only present to the
// user roots that contain the desired type somewhere in their file hierarchy.
row.add(Root.COLUMN_MIME_TYPES,
getChildMimeTypes(mBaseDir));
row.add(Root.COLUMN_AVAILABLE_BYTES,
mBaseDir.getFreeSpace());
row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
return result;
}
queryChildDocuments
@Override
public Cursor queryChildDocuments(String parentDocumentId,
String[] projection,
String sortOrder) throws
FileNotFoundException {
final MatrixCursor result = new
MatrixCursor(resolveDocumentProjection(projection
));
final File parent = getFileForDocId(parentDocumentId);
for (File file : parent.listFiles()) {
// Adds the file's display name, MIME type, size, and so on.
includeFile(result, null, file);
}
return result;
}
Making your content provider searchable
• create a new searchable metadata XML resource in your project’s res/xml folder.
<?xml version=”1.0” encoding=”utf-8”?>
<searchable
xmlns:android=”http://schemas.android.com/apk/re
s/android”
android:label=”@string/app_name”
android:hint=”@string/search_hint”>
</searchable>
• android:label attribute typically your application name
• android:hint attribute typically in the form of “Search for [content type or product name].”
• Define a search activity to display search results –
List View
• Users will not generally expect multiple searches to be added to the back stack, so it’s good practice to set a search Activity as “single top,” ensuring that the same instance will be used repeatedly rather than creating a new instance for each search.
• include an Intent Filter registered for the android.intent.action.SEARCH action and the DEFAULT category.
• include a meta‐data tag that includes a name attribute that specifies android.app.searchable, and a resource attribute that specifies a searchable XML resource
<activity android:name=”.DatabaseSkeletonSearchActivity”
android:label=”Element Search”
android:launchMode=”singleTop”>
<intent‐filter>
<action android:name=”android.intent.action.SEARCH” />
<category android:name=”android.intent.category.DEFAULT” />
</intent‐filter>
<meta‐data
android:name=”android.app.searchable”
android:resource=”@xml/searchable”
/>
</activity>
• To enable the search dialog for a given Activity, you need to specify which search results Activity should be used to handle search requests. You can do this by adding a meta‐data tag to its activity node in the manifest.
<meta-data
android:name=”android.app.default_searchable”
android:value=”.DatabaseSkeletonSearchActivity”
/>
• After users have initiated a search, your Activity will be started and their search queries will be available from within the Intent that started it, accessible through the SearchManager.QUERY
extra. • Searches initiated from within the search results Activity will result in new Intents being received — you can capture those Intents and extract the new queries from the onNewIntent handler,
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get the launch Intent
parseIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
parseIntent(getIntent());
}
private void parseIntent(Intent intent) {
// If the Activity was started to service a Search request,
// extract the search query.
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String searchQuery = intent.getStringExtra(SearchManager.QUERY);
// Perform the search
performSearch(searchQuery);
}
}
More complete listing for search activity
import android.app.ListActivity;
import android.app.LoaderManager;
import android.app.SearchManager;
import android.app.SearchableInfo;
import android.content.ContentUris;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.ListView;
import android.widget.SearchView;
import android.widget.SimpleCursorAdapter;
public class DatabaseSkeletonSearchActivity extends ListActivity
implements LoaderManager.LoaderCallbacks<Cursor> {
private static String QUERY_EXTRA_KEY = "QUERY_EXTRA_KEY";
private SimpleCursorAdapter adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a new adapter and bind it to the List View
adapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1, null,
new String[] { MyContentProvider.KEY_COLUMN_1_NAME },
new int[] { android.R.id.text1 }, 0);
setListAdapter(adapter);
// Initiate the Cursor Loader
getLoaderManager().initLoader(0, null, this);
// Get the launch Intent
parseIntent(getIntent());
/**
* Binding a Search View to your searchable Activity */
// Use the Search Manager to find the SearchableInfo related // to this Activity.
SearchManager searchManager =
(SearchManager)getSystemService(Context.SEARCH_SERVICE);
SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
// Bind the Activity's SearchableInfo to the Search View
SearchView searchView = (SearchView)findViewById(R.id.searchView);
searchView.setSearchableInfo(searchableInfo);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
parseIntent(getIntent());
}
private void parseIntent(Intent intent) {
// If the Activity was started to service a Search request,
// extract the search query.
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String searchQuery = intent.getStringExtra(SearchManager.QUERY);
// Perform the search
performSearch(searchQuery);
}
}
// Execute the search.
private void performSearch(String query) {
// Pass the search query as an argument to the Cursor Loader
Bundle args = new Bundle();
args.putString(QUERY_EXTRA_KEY, query);
// Restart the Cursor Loader to execute the new query.
getLoaderManager().restartLoader(0, args, this);
}
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String query = "0";
// Extract the search query from the arguments.
if (args != null)
query = args.getString(QUERY_EXTRA_KEY);
// Construct the new query in the form of a Cursor Loader.
String[] projection = { MyContentProvider.KEY_ID, MyContentProvider.KEY_COLUMN_1_NAME };
String where = MyContentProvider.KEY_COLUMN_1_NAME + " LIKE \"%" + query + "%\"";
String[] whereArgs = null;
String sortOrder = MyContentProvider.KEY_COLUMN_1_NAME + " COLLATE LOCALIZED ASC";
// Create the new Cursor loader.
return new CursorLoader(this, MyContentProvider.CONTENT_URI,
projection, where, whereArgs, sortOrder);
}
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
// Replace the result Cursor displayed by the Cursor Adapter with
// the new result set.
adapter.swapCursor(cursor);
}
public void onLoaderReset(Loader<Cursor> loader) {
// Remove the existing result Cursor from the List Adapter.
adapter.swapCursor(null);
}
/**
* Providing actions for search result selection */
@Override
protected void onListItemClick(ListView listView, View view, int position, long id) {
super.onListItemClick(listView, view, position, id);
// Create a URI to the selected item.
Uri selectedUri = ContentUris.withAppendedId(MyContentProvider.CONTENT_URI, id);
// Create an Intent to view the selected item.
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(selectedUri);
// Start an Activity to view the selected item.
startActivity(intent);
}
To‐Do List Database and Content Provider
• Start by creating a new ToDoContentProvider class. It will be used to host the database using an SQLiteOpenHelper
package com.paad.todolist;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class ToDoContentProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Override
public String getType(Uri url) {
return null;
}
@Override
public Cursor query(Uri url, String[] projection, String selection,
String[] selectionArgs, String sort) {
return null;
}
@Override
public Uri insert(Uri url, ContentValues initialValues) {
return null;
}
@Override
public int delete(Uri url, String where, String[] whereArgs) {
return 0;
}
@Override
public int update(Uri url, ContentValues values,
String where, String[]wArgs) {
return 0;
}
private static class MySQLiteOpenHelper extends SQLiteOpenHelper {
public MySQLiteOpenHelper(Context context, String name,
CursorFactory factory, int version) {
super(context, name, factory, version);
}
// Called when no database exists in disk and the helper class needs
// to create a new one.
@Override
public void onCreate(SQLiteDatabase db) {
// TODO Create database tables.
}
// Called when there is a database version mismatch meaning that the version
// of the database on disk needs to be upgraded to the current version.
@Override
public void onUpgrade(SQLiteDatabase db, int
oldVersion, int newVersion) {
// TODO Upgrade database.
}
}
}
• Publish the URI for this provider. This URI will be used to access this Content Provider from within other application components via the ContentResolver.
public static final Uri CONTENT_URI =
Uri.parse(“content://com.paad.todoprovider/todoitems”);
• Create public static variables that define the column names. They will be used within the SQLite Open Helper to create the database, and from other application components to extract values from your queries.
public static final String KEY_ID = “_id”;
public static final String KEY_TASK = “task”;
public static final String KEY_CREATION_DATE =
“creation_date”;
• Within the MySQLiteOpenHelper, create variables to store the database name and version, along with the table name of the to‐do list item table.
private static final String DATABASE_NAME =
“todoDatabase.db”;
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_TABLE =
“todoItemTable”;
• MySQLiteOpenHelper ‐‐ overwrite the onCreate and onUpgrade
methods to handle the database creation
// SQL statement to create a new database.
private static final String DATABASE_CREATE = “create table “ +
DATABASE_TABLE + “ (“ + KEY_ID +
“ integer primary key autoincrement, “ +
KEY_TASK + “ text not null, “ +
KEY_CREATION_DATE + “long);”;
// Called when no database exists in disk and the helper class needs
// to create a new one.
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE);
}
// Called when there is a database version mismatch, meaning that the version
// of the database on disk needs to be upgraded to the current version.
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Log the version upgrade.
Log.w(“TaskDBAdapter”, “Upgrading from version “ +
oldVersion + “ to “ +
newVersion + “, which will destroy all old data”);
// Upgrade the existing database to conform to the new version. Multiple
// previous versions can be handled by comparing oldVersion and newVersion
// values.
// The simplest case is to drop the old table and create a new one.
db.execSQL(“DROP TABLE IF IT EXISTS “ + DATABASE_TABLE);
// Create a new one.
onCreate(db);
}
• In ToDoContentProvider, add a private variable to store an instance of the MySQLiteOpenHelper class, and create it within the onCreate handler.
private MySQLiteOpenHelper myOpenHelper;
@Override
public boolean onCreate() {
// Construct the underlying database.
// Defer opening the database until you need to perform
// a query or transaction.
myOpenHelper = new MySQLiteOpenHelper(getContext(),
MySQLiteOpenHelper.DATABASE_NAME, null,
MySQLiteOpenHelper.DATABASE_VERSION);
return true;
}
• create a new UriMatcher to allow your Content Provider to differentiate between a query against the entire table and one that addresses a particular row. Use it within the getType handler to return the correct MIME type, depending on the query type.
private static final int ALLROWS = 1;
private static final int SINGLE_ROW = 2;
private static final UriMatcher uriMatcher;
//Populate the UriMatcher object, where a URI ending in ‘todoitems’ will
//correspond to a request for all items, and ‘todoitems/[rowID]’
//represents a single row.
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(“com.paad.todoprovider”, “todoitems”, ALLROWS);
uriMatcher.addURI(“com.paad.todoprovider”, “todoitems/#”, SINGLE_ROW);
}
@Override
public String getType(Uri uri) {
// Return a string that identifies the MIME type
// for a Content Provider URI
switch (uriMatcher.match(uri)) {
case ALLROWS: return “vnd.android.cursor.dir/vnd.paad.todos”;
case SINGLE_ROW: return “vnd.android.cursor.item/vnd.paad.todos”;
default: throw new IllegalArgumentException(“Unsupported URI: “ + uri);
}
}
• Implement the query method stub.
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// Open a read‐only database.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Replace these with valid SQL statements if necessary.
String groupBy = null;
String having = null;
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TAB
LE);
// If this is a row query, limit the result set to the passed in row.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
queryBuilder.appendWhere(KEY_ID + “=” + rowID);
default: break;
}
Cursor cursor = queryBuilder.query(db, projection, selection,
selectionArgs, groupBy, having, sortOrder);
return cursor;
}
• Implement the delete, insert, and update methods
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Open a read / write database to support the transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// If this is a row URI, limit the deletion to the specified row.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
selection = KEY_ID + “=” + rowID
+ (!TextUtils.isEmpty(selection) ?
“ AND (“ + selection + ‘)’ : “”);
default: break;
}
// To return the number of deleted items, you must specify a
where
// clause. To delete all rows and return a value, pass in “1”.
if (selection == null)
selection = “1”;
// Execute the deletion.
int deleteCount =
db.delete(MySQLiteOpenHelper.DATABASE_TABLE,
selection,
selectionArgs);
// Notify any observers of the change in the data set.
getContext().getContentResolver().notifyChange(uri, null);
return deleteCount;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// Open a read / write database to support the transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// To add empty rows to your database by passing in an empty Content Values
// object, you must use the null column hack parameter to specify the name of
// the column that can be set to null.
String nullColumnHack = null;
// Insert the values into the table
long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE,
nullColumnHack, values);
if (id > ‐1) {
// Construct and return the URI of the newly inserted row.
Uri insertedId = ContentUris.withAppendedId(CONTENT_URI, id);
// Notify any observers of the change in the data set.
getContext().getContentResolver().notifyChange(inse
rtedId, null);
return insertedId;
}
else
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// Open a read / write database to support the transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// If this is a row URI, limit the deletion to the specified row.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
selection = KEY_ID + “=” + rowID
+ (!TextUtils.isEmpty(selection) ?
“ AND (“ + selection + ‘)’ : “”);
default: break;
}
// Perform the update.
int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TABLE,
values, selection, selectionArgs);
// Notify any observers of the change in the data set.
getContext().getContentResolver().notifyChange(uri
, null);
return updateCount;
}
• Add it to your application Manifest, specifying
the base URI to use as its authority.
<provider android:name=”.ToDoContentProvider”
android:authorities=”com.paad.todoprovider”/>
• update ToDoList Activity to persist the to‐do list array. Start by modifying the Activity to implement LoaderManager.LoaderCallbacks<Cursor>, and then add the associated stub methods.
public class ToDoList extends Activity implements
NewItemFragment.OnNewItemAddedListener, LoaderManager.LoaderCallbacks<Cursor> {
// [... Existing ToDoList Activity code ...]
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return null;
}
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
}
public void onLoaderReset(Loader<Cursor> loader) {
}
}
• Complete the onCreateLoader handler by building and returning a Loader that queries the ToDoListContentProvider
public Loader<Cursor> onCreateLoader(int id, Bundle
args) {
CursorLoader loader = new CursorLoader(this,
ToDoContentProvider.CONTENT_URI, null, null, null,
null);
return loader;
}
• When the Loader’s query completes, the result Cursor will be returned to the onLoadFinished handler
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor)
{
int keyTaskIndex =
cursor.getColumnIndexOrThrow(ToDoContentProvider.KEY_TASK);
todoItems.clear();
while (cursor.moveToNext()) {
ToDoItem newItem = new
ToDoItem(cursor.getString(keyTaskIndex));
todoItems.add(newItem);
}
aa.notifyDataSetChanged();
}
• Update the onCreate handler to initiate the Loader when the Activity is created, and the onResume
handler to restart the Loader when the Activity is restarted.
public void onCreate(Bundle savedInstanceState) {
// [... Existing onCreate code …]
getLoaderManager().initLoader(0, null, this);
}
@Override
protected void onResume() {
super.onResume();
getLoaderManager().restartLoader(0, null, this);
}
• The final step is to modify the behavior of the onNewItemAdded handler. Rather than adding the item to the to‐do Array List directly, use the ContentResolver to add it to the Content Provider.
public void onNewItemAdded(String newItem) {
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put(ToDoContentProvider.KEY_TASK, newItem);
cr.insert(ToDoContentProvider.CONTENT_URI, values);
getLoaderManager().restartLoader(0, null, this);
}