What You’ll Learn in This Hour
Retrieving data with a URI
Using a content provider in your app
Hour 13, “Using SQLite and File Storage,” introduced SQLite, and you used cursors in your app to access data from the database. You can think of content providers as a bridge between the database and the app. Content providers work with any type of structured data. One reason to use a content provider is that it provides a way for other apps to access the data. That is what makes it a content provider. Another reason, covered in detail in Hour 15, “Loaders, CursorLoaders, and CustomAdapters,” is that you can use content providers with the CursorLoader
class to make updating data in fragments and activities easy. Content providers simplify providing data to the Android UI in your app.
The focus of this hour is to create your own ContentProvider(android.content.ContentProvider)
class for the Flickr photo SQLite database that you created in Hour 13. In this hour, you use a content provider to populate the PhotoListFragment
and to get the selected image to display. Content providers can return cursors for data or streams that can contain a file.
Content providers use Uniform Resource Identifiers (URIs) to identify the data resources that they provide. We are all familiar with URLs of the format http://www.amazon.com. A URL, or Uniform Resource Locator, is an example of a URI that defines the network location of a specific resource.
The format of the URI should represent the data being returned. Content providers return cursors that may point to a single item or to a list of items.
URIs for Android content providers start with content://. To create the full URI, you add the authority and a meaningful path. The authority is the creator of the content provider, and the Android package name and class should be used here. That will ensure uniqueness. In this chapter’s example, the package name is com.bffmedia.hour14App
. By convention, to create the full authority name, provider
is appended, so you get an authority of com.bffmedia.hour14App.provider
. In this hour, you work with Flickr photos, so in your base path, you will use flickrphoto
. When you put it all together, you get the following:
content://com.bffmedia.hour14App.provider/flickrphoto/
You can also create a URI to access an individual photo that you identify by the Flickr photo id: content://com.bffmedia.hour14App.provider/flickrphoto/12345
.
In this case, 12345
represents the id of a particular Flickr photo.
You have defined the URIs to use in our content provider. In this section, you examine the process to build a content provider in detail. You then use the new content provider in your app to get a cursor to a list of photo records. You’ll create the new class FlickrPhotoProvider
as an extension of ContentProvider
and build on it in Hours 15 and 16.
The Hour14app project contains the source code that accompanies this hour. The content provider work is done in FlickrPhotoProvider.java.
When you define a new class as an extension of ContentProvider
, you are required to implement six methods. Four methods interact with the data managed by the content provider: insert()
, update()
, delete()
, and query()
. All must be implemented, but creating a provider without providing full functionality to these methods is possible. That is, you can have stub methods if that fits your purpose. The two other methods are onCreate()
and getType()
. The onCreate()
method is used to initialize the content provider. That initialization often means opening a database. The getType()
method indicates the type of data that the content provider returns.
Listing 14.1 shows an empty content provider that is not useful, but implements a stub for all required methods.
1: package com.bffmedia.example;
2: import android.content.ContentProvider;
3: import android.content.ContentValues;
4: import android.database.Cursor;
5: import android.net.Uri;
6: public class EmptyProvider extends ContentProvider {
7: @Override
8: public int delete(Uri uri, String selection, String[] selectionArgs) {
9: return 0;
10: }
11: @Override
12: public String getType(Uri uri) {
13: return null;
14: }
15: @Override
16: public Uri insert(Uri uri, ContentValues values) {
17: return null;
18: }
19: @Override
20: public boolean onCreate() {
21: return false;
22: }
23: @Override
24: public Cursor query(Uri uri, String[] projection, String selection,
25: String[] selectionArgs, String sortOrder) {
26: return null;
27: }
28: @Override
29: public int update(Uri uri, ContentValues values,
30: String selection,String[] selectionArgs) {
31: return 0;
32: }
33: }
You will examine each section of the FlickrPhotoProvider
class. Often, field definitions are self-explanatory, but in the case of FlickrPhotoProvider
, it is beneficial to examine the declarations and definitions carefully. There are static fields that correspond to the parts of the URI definition. There are static fields that will be used in the getType()
method and elsewhere to make the code more readable.
A static UriMatcher
will be declared. The job of the UriMatcher
is to match the string pattern of a URI to a specific constant value as a convenience for development. Instead of string and pattern-matching logic to determine what action should be taken with a URI, you can use the UriMatcher
and a switch statement to simplify the code. The UriMatcher
is a convenience class created for this purpose.
When you create a content provider, you’ll see that there are many interconnected pieces. Listing 14.2 shows the FlickrPhotoProvider
declarations and definitions.
1: public class FlickrPhotoProvider extends ContentProvider {
2: private FlickrPhotoDbAdapter mPhotoDbAdapter;
3: private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_
MATCH);
4: static {
5: sUriMatcher.addURI("com.bffmedia.hour14app.provider", "flickrphoto", 1);
6: sUriMatcher.addURI("com.bffmedia.hour14app.provider", "flickrphoto/#", 2);
7: }
8: public static final Uri CONTENT_URI =
9: Uri.parse("content://com.bffmedia.hour14app.provider/
flickrphoto");
Lines 3 through 7 define the UriMatcher
. In lines 5 and 6, the URIs for the content are represented along with an integer value to return when there is a matching URI. Line 5 represents getting all Flickr photos and line 6 represents getting a single Flickr photo that is specified by passing a number as an appended id. The # indicates that.
Line 8 creates a static variable called CONTENT_URI
that will be used by apps that use the FlickrPhotoProvider
.
Given a URI, the UriMatcher
returns the integer code that you associated with the URI. This example has URIs for getting a single photo based on id, and a URI for getting a list of photos based on page id. Lines 5 and 6 add these URIs to the UriMatcher
and specify the integer values to return when a match is found.
You’ll use the UriMatcher
for two of the methods that you are required to implement when creating a content provider.
The getType()
method returns a string representing the type of data you are returning. In that case, you will use the UriMatcher
to determine whether the request for the content type is for a single photo based on a Flickr photo id or for a list of photos.
You will also use the UriMatcher
for the query()
method. In this query()
method, you either return a cursor to all the photos or a single photo. The URI passed determines that.
The string constants like "flickrphoto"
in Listing 14.2 help make the example clear. Using static variables to define these strings is recommended. For example, if you used:
private static final String AUTHORITY = "com.bffmedia.hour14app.provider";
private static final String BASE_PATH = "flickrphoto";
public static final int PHOTOS = 1;
public static final int PHOTO_ID = 2;
Then you would create the UriMatcher
using:
sUriMatcher.addURI(AUTHORITY, BASE_PATH, PHOTOS );
sUriMatcher.addURI(AUTHORITY, BASE_PATH+"/#", PHOTO_ID);
Adding content provider information to the AndroidManifest.xml file is critical because the app will not work if this is not done. The following snippet shows the definition for this provider. The authority is provided. The android:name
attribute is the name of the class that defined the provider. The android:exported
attribute specifies whether or not third-party apps can use this content provider. When set to false
, it indicates that only the current app can use the provider.
The name and authorities values must be set. You should make a decision on whether the data should be exported. If you are not sure, set exported
to "false"
; not exporting data is better than exporting it by mistake. Leaving multiprocess
set to "true"
is fine:
<provider
android:authorities="com.bffmedia.hour14app.provider"
android:multiprocess="true"
android:exported="false"
android:name="com.bffmedia.hour14app.FlickrPhotoProvider">
</provider>
For additional details and other options, see http://developer.android.com/guide/topics/manifest/provider-element.html.
The plan is to support two URIs in the FlickrPhotoProvider
content provider. When requested from FlickrPhotoProvider
, these URIs will be fulfilled from the query()
method. You learn how to implement the specific response required by these URIs. This section then covers the parameters that are passed to this method and considers alternative implementations.
Listing 14.3 implements the query()
method for FlickrPhotoProvider
. It checks the URI that was passed and fulfills the request based on the URI. If the URI is not recognized, an exception is thrown.
1: @Override
2: public Cursor query(Uri uri, String[] projection, String selection,
3: String[] selectionArgs, String sortOrder) {
4: Cursor cursor;
5: int uriType = sUriMatcher.match(uri);
6: switch (uriType) {
7: case 1:
8: cursor = mPhotoDbAdapter.mDb.query(true, FlickrPhotoDbAdapter.DATABASE_TABLE,
9: projection, selection, selectionArgs, null,null,sortOrder, null);
10: cursor.setNotificationUri(getContext().getContentResolver(), uri);
11: break;
12: case 2:
13: cursor = mPhotoDbAdapter.fetchByFlickrId(uri.getLastPathSegment());
14: cursor.setNotificationUri(getContext().getContentResolver(), uri);
15: break;
16: default:
17: throw new IllegalArgumentException("Unknown URI");
18: }
19: return cursor;
20: }
Three tasks are being handled in Listing 14.3:
Identify the URI to handle using UriMatcher
.
Create a Cursor
that fulfills the request for data.
Set a notification URI for the cursor.
Lines 10 and 14 set the notification URI for the cursor. The URI will be watched for changes. The ContentResolver
associated with the context will be notified when a change occurs. That means that the ContentResolver
associated with the activity is observing this URI and will be notified of changes. This hour covers this topic more later.
This implementation of FlickrPhotoProvider
handles two URIs. The uriType
specifies whether to return a single photo or a list of photos. When retrieving a single photo, the call to uri.getLastPath()
segment returns the value of the specific photo id to use.
The cursor for returning a single photo is created on line 13 and the cursor for fulfilling a list of photos occurs on line 8. You use the FlickrPhotoDbAdapter
class to do this for a single photo. To get a list of photos, you create the query directly in the content provider.
By examining the parameters passed to the query
method, you can see how to do a database query. Lines 2 and 3 of Listing 14.3 show these parameters. We’ll define each one:
String[] projection, String selection, String[] selectionArgs, String sortOrder
The projection
parameter identifies the columns in a database table that should be returned. A null
projection means to return all columns. To create a SQL query, you would use selection
and optionally selectionArgs
parameters. The sortOrder
parameter defines the order of the data being returned.
These are the parameters that you would pass to a SQLDatabase query()
method. See Hour 13 to see how to use a query in the FlickrPhotoDBAdapter
class.
The parameters from the query method in the content provider will be passed to a SQLiteDatabase query()
method. That method is available to use because the SQLiteDatabase
is available through FlickrPhotoDbAdapter
. The parameters form a query and return a cursor.
The parameters passed determine the data returned. If you wanted to put more restrictions on the data that can be returned, you could define URIs that only return data from a hardcoded query in the query()
method. That is, all the passed parameters would be ignored and a specific query would be performed. To make that more flexible for apps using the content provider, defining multiple URIs to call would make sense.
As a review, this hour implements support for two URIs in the ContentProvider query()
method. The URIs are used to get a list of photos and get an individual photo.
When called from an activity, you use the content provider via a managedQuery
. This calls the content provider’s query method with the parameters discussed earlier.
To get a single photo, you need to create a URI with an appended path of the flickr_id
. The URI is
content://com.bffmedia.hour14app.provider/flickrphoto/12345
where 12345
is the id. To call this from an Activity
with a managedQuery
, you use the following:
managedQuery(Uri.withAppendedPath(FlickrPhotoProvider.CONTENT_URI ,"12345"), null,
null, null, null);
You can access content providers using getContentResolver()
, managedQuery()
, or CursorLoader
s. The benefit of using managedQuery()
is that the Activity
takes care of maintaining the cursor. Hour 15 covers the advantages in using CursorLoader
s.
GO TO HOUR 15, “LOADERS, CURSORLOADERS, AND CUSTOMADAPTERS,” to learn about using content providers with CursorLoader
s.
To retrieve multiple photos, you would create a query as follows:
Cursor photoCursor = managedQuery(FlickrPhotoProvider.CONTENT_URI, null, null,
null, null);
That would get all photos. To get just favorites, you could use:
Cursor photoCursor = managedQuery(FlickrPhotoProvider.CONTENT_URI, null,
"isFavorite='1'", null, null);
To get the favorite photos using this URI, you add a selection parameter in the call to manageQuery
.
The purpose of the getType()
method is to show the type of data that will be returned from the content provider. The ContentResolver
class offers two Android MIME types to use for this purpose. One MIME type represents a cursor that can have multiple objects, and the other MIME type is used for when a single item is returned in the cursor. You use ContentResolver.CURSOR_DIR_BASE_TYPE
for multiple items and ContentResolver.CURSOR_ITEM_BASE_TYPE
for a single item.
Because this example uses FlickrPhoto
objects, the types will be a string, like this:
ContentResolver.CURSOR_DIR_BASE_TYPE + "/com.bffmedia.hour15app.FlickrPhoto"
Listing 14.4 shows the full getType()
method.
On lines 5 and 6, the type for a list of items is returned, and on lines 7 and 8, the type for a single item is returned. Recall that type for the URI was defined in the UriMatcher
. Refer to Listing 14.2 for review.
1: @Override
2: public String getType(Uri uri) {
3: int uriType = sUriMatcher.match(uri);
4: switch (uriType) {
5: case 1:
6: return ContentResolver.CURSOR_DIR_BASE_TYPE+"/com.bffmedia.hour15app.
FlickrPhoto";
7: case 2:
8: return ContentResolver.CURSOR_ITEM_BASE_TYPE+"/com.bffmedia.hour15app.
FlickrPhoto";
9: default:
10: return null;
11: }
12: }
To create a content provider, you must implement insert()
, update()
, and delete()
methods. This provides a consistent way to keep the app in synch and aware of updates as they happen.
These methods must exist in the content provider, but making a read-only content provider is possible as well. To create a read-only content provider, the query methods would be fully implemented to return results. The insert()
, update()
, and delete()
methods would exist, but would not actually change the underlying data.
Listing 14.5 shows a complete insert()
method.
Listing 14.5 inserts the passed ContentValues
into the database by directly accessing the mDb
field in the FlickrPhotoDbAdapter
class.
1: @Override
2: public Uri insert(Uri uri, ContentValues values) {
3: long newID=mPhotoDbAdapter.mDb.insert(FlickrPhotoDBHelper.DATABASE_
TABLE,null,values);
4: if (newID > 0) {
5: Uri newUri = ContentUris.withAppendedId(uri, newID);
6: getContext().getContentResolver().notifyChange(uri, null);
7: return newUri;
8: } else {
9: throw new SQLException("Failed to insert row into " + uri);
10: }
11: }
Listing 14.6 shows the database update. With an update, the selection criteria are passed to the argument so that the rows to be updated can be identified.
1: @Override
2: public int update(Uri uri, ContentValues values, String selection,
3: String[] selectionArgs) {
4: return mPhotoDbAdapter.mDb.update(FlickrPhotoDBHelper.DATABASE_TABLE,
5: values, selection, selectionArgs);
6: }
Listing 14.7 shows the delete()
method. Like the update()
method, it takes selection criteria as parameters. When updating or deleting, you must specify the rows to act upon via these parameters.
1: @Override
2: public int delete(Uri uri, String selection, String[] selectionArgs) {
3: return mPhotoDbAdapter.mDb.delete(PhotoDBHelper.DATABASE_TABLE,
4: selection, selectionArgs);
5: }
Hour 13 showed a Cursor
and SimpleCursorAdapter
in the PhotoListFragment
used to display the list of titles. The Cursor
was created by making direct database calls via FlickrPhotoDbAdapter
. Modifying this code in PhotoListFragment
to use in the new content provider is possible and has several advantages. By using a managedQuery
, you do not have to use the FlickrPhotoDbAdapter
class directly or have to manage the cursor. That means you can completely remove the onDestroy()
method that you were using to close the cursor.
Listing 14.8 shows the new onActivityCreated()
method for PhotoListFragment
.
1: @Override
2: public void onActivityCreated(Bundle savedInstanceState) {
3: super.onActivityCreated(savedInstanceState);
4: mPhotoCursor = getActivity().managedQuery(FlickrPhotoProvider.CONTENT_URI,
5: null, null, null, null);
6: mAdapter = new SimpleCursorAdapter(getActivity(),
7: android.R.layout.simple_list_item_1,
8: mPhotoCursor, //Cursor
9: new String[] {"title"},
10: new int[] { android.R.id.text1 }, 0);
11: setListAdapter(mAdapter);
12:}
The FlickrPhotoProvider
is also used to insert records in the MainActivity
class.
Listing 14.9 revises the portion of the doInBackground()
method where you currently insert or update photo objects with FlickrPhotoDbAdapter
. You use the FlickrPhotoProviderinsert()
and update()
methods. You take an available FlickrPhoto
object and populate a ContentValues
object with name/value pairs as required by the insert()
and update()
methods.
You query the database to determine whether a record already exists for the photo. If it does exist, it is updated. If it does not exist, it is inserted. The query to check the database is on lines 8–10. If you can do a moveToFirst()
on the cursor it means that the record exists. The update()
and insert()
methods occur on lines 11–18. Note that the full set of name/value pairs for the ContentValues
object defined on line 2 is not included.
1: for (FlickrPhoto currentPhoto : mPhotos) {
2: ContentValues newValues = new ContentValues();
3: if (currentPhoto.id!=null)
4: newValues.put("flickr_id", currentPhoto.id);
5: ...
6: if (currentPhoto.isFavorite!=null)
7: newValues.put("isFavorite", currentPhoto.isFavorite);
8: Cursor photoCursor = managedQuery(Uri.withAppendedPath(
9: FlickrPhotoProvider.CONTENT_URI,currentPhoto.id), null, null, null,
null);
10: if (photoCursor.moveToFirst()){
11: FlickrPhoto existingPhoto = FlickrPhotoDbAdapter.getPhotoFromCursor(photoCurs
or);
12: photoDbAdapter.updatePhoto(existingPhoto.id, currentPhoto);
13: Log.d(TAG,"Updated " + existingPhoto.id);
14: }else{
15: Uri newUri = getContentResolver().insert(
16: FlickrPhotoProvider.CONTENT_URI,
17: newValues);
18: }
19: }
You have learned to update an app to retrieve, insert, and update data using a content provider. You can take an additional step and incorporate the retrieval of an image file into the FlickrPhotoProvider
class. That will cleanly separate all content retrieval in the app from presentation and logic.
The job of the content provider is to retrieve content. That content may be data returned in a Cursor
or an inputStream
. The logic of the app and the visual presentation of the app are independent from the content provider.
Currently, the code to retrieve the image is in the ImageViewFragment
class. Hour 13 showed how to pass a full image URL to the ImageViewFragment
. You modify that to pass the id instead. When you retrieve a file from a content provider, you are retrieving a file that is associated with a single item. The URI to use is the same as for retrieving the data for a single item from that database.
It will look like the following:
content://com.bffmedia.hour14App.provider/flickrphoto/12345
The request to the content provider will be for the file associated with the record specified by this URI. The return value is actually an InputStream
that points to the file.
You can use the logic covered in Hour 13 for retrieving an image. You can retrieve the image contents from Flickr and write the image to the cache file system for faster retrieval on subsequent calls.
The following sections introduce the concept of a content observer. The class ContentObserver
watches for changes in a given URI. This is important because you might request an image that has not yet been written as a file. In that case, you set a content observer to display the image file when it becomes available.
Listings 14.10 to 14.14 show the process of retrieving data from a file and using a ContentObserver
to track whether the file has been written.
Start with the changes to the FlickrPhotoProvider
class. You implement the openFile
method. The FlickrPhotoProvider
’s openFile
method is called when you request an InputStream
from the provider. That is, a request in your app to open an InputStream
from FlickrPhotoProvider
like the following results in a call to the openFile
method in your FlickrPhotoProvider
class:
InputStream is=getActivity().getContentResolver(.
openInputStream(Uri.withAppendedPath(PhotoProvider.CONTENT_URI ,mPhotoId));
The URI to use to grab the file is the CONTENT_URI
with an appended photo id:
content://com.bffmedia.hour14App.provider/flickrphoto/12345
Listing 14.10 shows the implementation of the openFile
method within the FlickrPhotoProvider
class.
1: @Override
2: public ParcelFileDescriptor openFile(Uri uri, String mode)
3: throws FileNotFoundException {
4: File root = new File(getContext().getApplicationContext().getCacheDir(),
5: uri.getEncodedPath());
6: root.mkdirs();
7: File imageFile = new File(root, "image.jpg");
8: final OutputStream imageOS;
9: final int imode = ParcelFileDescriptor.MODE_READ_ONLY;
10: if (imageFile.exists()) {
11: return ParcelFileDescriptor.open(imageFile, imode);
12: }
13: Cursor photoCursor = query(uri, null, null, null, null);
14: if (photoCursor == null) return null;
15: if (photoCursor.getCount()==0) return null;
16: FlickrPhoto currentPhoto = FlickrPhotoDbAdapter.getPhotoFromCursor(photoCurs
or);
17: final String imageString = currentPhoto.getPhotoUrl(true);
18: imageOS = new BufferedOutputStream(new FileOutputStream(imageFile));
19: RetrieveImage ri = new RetrieveImage (uri, imageString, imageOS);
20: ri.execute();
21: throw ( new FileNotFoundException());
22: }
Listing 14.10 does two things. If the requested file exists, it is returned as a ParcelFileDescriptor
.
If the file does not exist several things happen. The codes begins retrieving the image in the background and, in the meantime, throws a FileNotFoundException
so that the requesting activity can show something else. In a subsequent refresh, after the image has been downloaded, the content provider returns the file descriptor for the file.
The activity that requests the file you need to handle the exception when the file is not present.
Lines 4 to 7 of Listing 14.10 create a directory for the file based on the URI and then access a file called image.jpg in that directory. If the file exists, it is used to create a ParcelFileDescriptor
that is returned. ParcelFileDescriptor
s provide a way to work on the original file. On line 9, the ParcelFileDescriptor
is set to read-only. It is returned on line 11.
The underlying logic is to create a folder for each image and to include a file called image.jpg in that folder.
If the file is not found, the code gets a FlickrPhoto
object based on the photo id. You use the query()
method of the provider to get the FlickrPhoto
object. With the FlickrPhoto
object, you can create a photo URL to retrieve the image from Flickr. You use the AsyncTaskRetrieveImage
shown in Listing 14.11.
1:private class RetrieveImage extends AsyncTask<String , String , Long > {
2: String mImageString;
3: OutputStream mImageOS;
4: Uri mUri;
5: public RetrieveImage ( Uri uri, String imageString, OutputStream imageOS){
6: mImageString = imageString;
7: mImageOS = imageOS;
8: mUri = uri;
9: }
10: @Override
11: protected Long doInBackground(String... params) {
12: try {
13: URL imageUrl = new URL(mImageString);
14: URLConnection connection = imageUrl.openConnection();
15: connection.connect();
16: InputStream is = connection.getInputStream();
17: Bitmap mBitmap = BitmapFactory.decodeStream(is);
18: mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, mImageOS);
19: mImageOS.flush();
20: mImageOS.close();
21: getContext().getContentResolver().notifyChange(mUri, null);
22: return (0l);
23: } catch (MalformedURLException e) {
24: e.printStackTrace();
25: return (1l);
26: }
27: catch (IOException e) {
28: e.printStackTrace();
29: return (1l);
30: }
31: }
32:}
The image is retrieved from the URL as you have done previously, but instead of creating a bitmap to return, you write the URL to the file that you created in the openFile()
method. You pass the RetrieveImage
class a URI, a string containing the Flickr photo URL, and the OutputStream
for writing the file.
Line 21 calls the notifyChange
method of ContentResolver
. A notification is sent to any observers of the URI that a change has occurred. You must set up an observer in the ImageViewFragment
class to act on this notification.
Let’s review the logic for the case where the file does not exist. The ImageViewFragment
requests the file. Because the file does not exist at that moment, the FlickrPhotoProvider
throws a FileNotFoundException
. On getting that exception, the ImageViewFragment
sets an observer to wait for the URI associated with the file to change. From the perspective of the ImageViewFragment
, there is no file, but if it becomes available, a notification occurs.
Meanwhile, in addition to throwing the FileNotFoundException
, the FlickrPhotoProvider
is retrieving the image from Flickr. After the image is received and it is written to file, a notification occurs. The ImageViewFragment
receives that notification, opens the file, and creates a bitmap.
Listing 14.12 shows the onActivityCreated
method of ImageViewFragment
.
1: @Override
2: public void onActivityCreated(Bundle savedInstanceState) {
3: super.onActivityCreated(savedInstanceState);
4: Bundle b = this.getArguments();
5: if (b!=null){
6: mPhotoId=b.getString("PHOTO_ID");
7: }
8: Handler handler = new Handler();
9: InputStream is;
10: try {
11: is = getActivity().getContentResolver().openInputStream
12: (Uri.withAppendedPath(FlickrPhotoProvider.CONTENT_URI
,mPhotoId));
13: mBitmap = BitmapFactory.decodeStream(is);
14: mImageView.setImageBitmap(mBitmap);
15: } catch (FileNotFoundException e) {
16: getActivity().getContentResolver().registerContentObserver(
17: Uri.withAppendedPath(FlickrPhotoProvider.CONTENT_URI
,mPhotoId),
18: false, new PhotoContentObserver(handler));
19: }
20: }
Instead of passing the contents of the source field in the bundle, you are now passing the id of the photo. You use that ID to form a URI and use that URI on lines 11 and 12 when you call openInputStream
. Calling openInputStream
will call the FlickrPhotoProvider
’s openFile
method. If no exception is thrown, lines 13 and 14 execute, creating a bitmap and displaying that bitmap in an ImageView
.
If an exception is thrown, you register a content observer called PhotoContentObserver
to watch the specified URI for changes.
Listing 14.13 defines a class called PhotoContentObserver
of type ContentObserver
. The purpose of this class is to perform an action when the content being observed changes. In this case, the content being observed is the file you are trying to use.
1: class PhotoContentObserver extends ContentObserver {
2: public PhotoContentObserver(Handler handler) {
3: super(handler);
4: }
5: @Override
6: public void onChange(boolean selfChange) {
7: InputStream is;
8: try {
9: if (ImageViewFragment.this.isAdded()){
10: is = getActivity().getContentResolver().openInputStream
11: Uri.withAppendedPath(FlickrPhotoProvider.CONTENT_URI ,mPhotoId));
12: mBitmap = BitmapFactory.decodeStream(is);
13: mImageView.setImageBitmap(mBitmap);
14: }
15: } catch (FileNotFoundException e) {
16: }
17: }
18: }
The PhotoContentObserver
is notified when a new file is created. Line 21 of Listing 14.11 sends that notification. When that occurs, the onChange()
method of the ContentObserver
is called. Line 9 of Listing 14.13 checks to see whether the fragment is still attached to the activity.
This possibility exists that the notification of file availability will occur at a point when the ImageViewFragment
is no longer in the current activity. In that case, a call to getActivity()
returns null
. This check prevents that issue. Lines 10 to 12 get the InputStream
for the file, create a Bitmap
, and display it.
You have now created and used a content provider for both data retrieval for a Cursor
representing a database and to get an image represented by a file.
In this hour, you created your own content provider by implementing insert()
, query()
, update()
, and delete()
methods. You saw how to use a UriMatcher
to examine a URI and direct you to an action. After the FlickrPhotoProvider
was in place, you used it in the app for both data and file retrieval.
Q. What kinds of content can be returned from a content provider?
A. A content provider can return data from a database in a Cursor
, and it can return a file via the openFile()
method.
Q. How do content providers use URIs?
A. In content providers, URIs are used to identify the data to return. In this hour, you used URIs to get data for a single item, results from a query, and an input stream.
Q. What is the purpose of a read-only content provider?
A. Content providers must include insert()
, update()
, and delete()
methods, but those methods are not required to change the underlying data. The purpose of a read-only content provider is to make data from an app available to third-party apps, but to restrict those apps from changing the underlying data.
1. What method is used to call a content provider’s query method from an app?
2. What is a ContentObserver
?
3. What method is called from an app to trigger a call to a content provider’s openFile
method?
1. An activity contains a managedQuery
method for this purpose.
2. A ContentObserver
is a class that responds to changes in a given URI. When the URI changes, the onChange
method of the ContentObserver
fires.
3. A call to openInputStream
does this:
getActivity().getContentResolver().openInputStream
1. The example in this hour retrieves a list of photos. Change the PhotoListFragment
to retrieve a list of favorite images. Continue to display the titles, but sort the results by putting the titles in alphabetical order. Optionally, create a new URI for retrieving the list of favorites. To do that, you need to extend the content provider infrastructure by defining a new URI and using the UriMatcher
. To test this, you must set some images as favorites.
2. Create a GridView
of image titles using the content provider created in this hour. Note that you will be doing this in Hour 15.
18.220.108.111