Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
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); }