Hour 14. Creating a Content Provider


What You’ll Learn in This Hour

Image Retrieving data with a URI

Image Building a content provider

Image 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.

Using a URI for Data Retrieval

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.

Building a Content Provider

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.

Methods Required in a Content Provider

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.

LISTING 14.1 Shell of a Content Provider


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: }


Declaring the Content Provider

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.

LISTING 14.2 FlickrPhotoProvider Declarations


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);

Updating the Android Manifest

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.

Content Provider Query Method

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.

LISTING 14.3 FlickrPhotoProvider Query Method


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:

Image Identify the URI to handle using UriMatcher.

Image Create a Cursor that fulfills the request for data.

Image 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.

Using the FlickrPhotoProvider Query

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 CursorLoaders. The benefit of using managedQuery() is that the Activity takes care of maintaining the cursor. Hour 15 covers the advantages in using CursorLoaders.

GO TO Image HOUR 15, “LOADERS, CURSORLOADERS, AND CUSTOMADAPTERS,” to learn about using content providers with CursorLoaders.

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.

Implementing the GetType() Method

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.

LISTING 14.4 Return Type of Data in GetType()


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: }


Implement Insert, Update, and Delete Methods

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.

LISTING 14.5 Content Provider insert() Method


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.

LISTING 14.6 Content Provider update() Method


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.

LISTING 14.7 Content Provider delete() Method


1: @Override
2: public int delete(Uri uri, String selection, String[] selectionArgs) {
3:    return mPhotoDbAdapter.mDb.delete(PhotoDBHelper.DATABASE_TABLE,
4:           selection, selectionArgs);
5: }


Using FlickrPhotoProvider in the App

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.

LISTING 14.8 Using a Content Provider in 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.

LISTING 14.9 Inserting and Updating Using a Content Provider


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:  }


Requesting a File from a Content Provider

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.

How to Return a File from a Content Provider

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.

LISTING 14.10 Implementing File Support in FlickrPhotoProvider


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. ParcelFileDescriptors 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.

Retrieving an Image from a File or Remotely

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.

LISTING 14.11 Retrieving an Image in FlickrPhotoProvider


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.

LISTING 14.12 Creating a Bitmap Using the Content Provider


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.

Using a ContentObserver When Content 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.

LISTING 14.13 ContentObserver Defined in ImageViewFragment


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.

Summary

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&A

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.

Workshop

Quiz

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?

Answers

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

Exercises

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.

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

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