9781449390501
Android_Content_Providers.html

Chapter 12. Content Providers

If you're new to the Android mobile operating system, Learning Android is the perfect way to master the fundamentals. This gentle introduction shows you how to use Android's basic building blocks to develop user interfaces, store data, and more. Buy the print book or ebook.

Content Providers are Android building blocks that are capable of exposing data across the boundaries between application sandboxes. As you recall, each application in Android runs in its own process with its own permissions. This means that an application cannot see another app’s data. But sometimes you may want to share data across applications. This is where Content Providers become very useful.

Take your contacts, for example. You may have a large database of contacts on your device. You can view them via the Contacts app as well as via the Dialer app. On some devices, such as HTC Android models, there may be even multiple versions of Contacts and Dialer apps available. It would not make a lot of sense to have similar data live in multiple databases.

Content Providers provide a way to centralize content in one place and have many different applications access it as needed. In case of the Contacts on your phone, there is actually a ContactProvider application that contains a Content Provider. Other applications access the data via this interface. The interface itself is fairly simple: it is the same insert(), update(), delete(), and query() methods we saw in Chapter 9, Database.

Android uses Content Providers quite a bit internally. We already mentioned contacts. Settings is another example, and so are all your bookmarks. All the media in the system is also registered with MediaStore, a content provider that dispenses images, music, and videos in your device.

Creating Content Provider

To create a content provider:

  1. Create a new Java class that subclasses the system’s ContentProvider class.
  2. Declare your CONTENT_URI.
  3. Implement all the unimplemented methods, such as insert(), update(), delete(), query(), getID(), and getType().
  4. Declare your Content Provider in the AndroidManifest.xml file.

We are going to start by creating a brand new Java class in the same package as any other classes. Its name will be StatusProvider. This class, like any of Android’s main building blocks, will subclass an Android framework class, in this case ContentProvider.

So in Eclipse, select your package, click on File→New→Java Class, and enter StatusProvider. Then update the class to subclass ContentProvider and organize imports (Ctrl-Shift-O) to import the appropriate Java packages. The result should look like this:

package com.marakana.yamba7;

import android.content.ContentProvider;

public class StatusProvider extends ContentProvider {

}

Of course this code is now broken because we need to provide implementation for many of its methods. The easiest way to do that is to click on the class name and choose "Add unimplemented methods" from the list of quick fixes. Eclipse will then create stubs, or templates, of the missing methods.

Defining the URI

Objects within a single app can refer to each other simply by variable names, because they share an address space. But objects in different apps don’t recognize the different address spaces, so they need some other mechanism just to find each other. Android uses a A Uniform Resource Identifier, a string that identifies a specific resource, to locate a Content Provider. A URI has three or four parts, shown in Parts of a URI.

Parts of a URI. 

content://com.marakana.yamba.statusprovider/status/47
   A              B                           C    D

  • Part A, content://, is always set to this value. This is written in stone.
  • Part B, com.marakana.yamba.provider, is the so-called authority. It is typically the name of the class, all in lower case. This authority must match the authority that we specify for this provider when we declare it in the manifest file later on.
  • Part C, status, indicates the type of data that this particular provider is providing. It could contain any number of segments separated with a slash, including none at all.
  • Part D, 47, is an optional ID of the specific item that we are referring to. If not set, the URI will represent the entire set. Number 47 is an arbitrary number picked for example.

Sometimes you need to refer to the Content Provider in its entirety, and sometimes to one of the items of data it contains. If you refer to it in its entirety, you leave off Part D; otherwise you include that part to identify one item within the Content Provider. Actually, since we have only one table, we do not need the C part of the URI.

One way to define the constants for this particular case is like this:

public static final Uri CONTENT_URI = Uri
    .parse("content://com.marakana.yamba7.statusprovider");
public static final String SINGLE_RECORD_MIME_TYPE =
    "vnd.android.cursor.item/vnd.marakana.yamba.status";
public static final String MULTIPLE_RECORDS_MIME_TYPE =
    "vnd.android.cursor.dir/vnd.marakana.yamba.mstatus";

In the section called “Getting the Data Type” we’ll explore the reason for two MIME types. We are also going to define the status data object in a class-global variable so that we can refer to it:

StatusData statusData;

The reason we’ll be using the status data object all over our app is that all our database connectivity is centralized in that class. So now the StatusProvider class has a reference to an object of class StatusData.

Inserting Data

To insert a record into a database via the Content Provider interface, we need to override the insert() method. The caller provides the URI of this Content Provider (without an ID) and the values to be inserted. A successful call to insert the new record returns the ID for that record. We end by returning a new URI concatenating the provider’s URI with the ID we just got back.

@Override
public Uri insert(Uri uri, ContentValues values) {
  SQLiteDatabase db = statusData.dbHelper.getWritableDatabase();  // 1
  try {
    long id = db.insertOrThrow(StatusData.TABLE, null, values);  // 2
    if (id == -1) {
      throw new RuntimeException(String.format(
          "%s: Failed to insert [%s] to [%s] for unknown reasons.", TAG,
          values, uri));  // 3
    } else {
      return ContentUris.withAppendedId(uri, id); // 4
    }
  } finally {
    db.close(); // 5
  }
}

1

We need to open the database for writing.

2

We attempt to insert the values into the database and, upon a successful insert, get back the ID of the new record from the database.

3

If anything fails during the insert, the database will return -1. We can than throw a runtime exception because this is an error that should never have happened.

4

If the insert was successful, we use the ContentUris.withAppendedId() helper method to craft a new URI containing the ID of the new record appended to the standard provider’s URI.

5

We need to close the database no matter what, so a finally block is a good place to do that.

Updating Data

To update the data via the Content Provider API, we need:

The URI of the provider
This may or may not contain an ID. If it does, the ID indicates the specific record that needs to be updated, and we can ignore the selection. If the ID is not specified, it means that we are updating many records and we need the selection to indicate which are to be changed.
The values to be updated
The format of this parameter is a set of name/value pairs that represent column names and new values.
Any selection and arguments that go with it
These together make up a WHERE clause in SQL, selecting the records that will change. The selection and its arguments are omitted when there is an ID, because that is enough to select the record that is being updated.

The code that handles both types of update—by ID and by selection—can be as follows.

@Override
public int update(Uri uri, ContentValues values, String selection,
    String[] selectionArgs) {
  long id = this.getId(uri); // 1
  SQLiteDatabase db = statusData.dbHelper.getWritableDatabase(); // 2
  try {
    if (id < 0) {
      return db.update(StatusData.TABLE, values, selection, selectionArgs); // 3
    } else {
      return db.update(StatusData.TABLE, values, StatusData.C_ID + "=" + id, null); // 4
    }
  } finally {
    db.close(); // 5
  }
}

1

We use the local helper method getId() to extract the ID from the URI that we get. If no ID is present, this method returns -1. getId() will be defined later in this chapter.

2

We need to open the database for writing the updates.

3

If there’s no ID, that means we’re simply updating all the database records that match the selection and selectionArgs constraints.

4

If ID is present, we are using that ID as the only part of the WHERE clause to limit the single record that we’re updating.

5

Don’t forget to close the database no matter what.

Deleting Data

Deleting the data is similar to updating the data. The URI may or may not contain the ID of the particular record to delete.

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
  long id = this.getId(uri); // 1
  SQLiteDatabase db = statusData.dbHelper.getWritableDatabase(); // 2
  try {
    if (id < 0) {
      return db.delete(StatusData.TABLE, selection, selectionArgs); // 3
    } else {
      return db.delete(StatusData.TABLE, StatusData.C_ID + "=" + id, null); // 4
    }

  } finally {
    db.close(); // 5
  }
}

1

The getId() helper method extracts the ID from the URI that we get. If no ID is present, this method returns -1.

2

We need to open the database for writing the updates.

3

If there’s no ID, we simply delete all the database records that match the selection and selectionArgs constraints.

4

If ID is present, we use that ID as the only part of the WHERE clause to limit the operation to the single record the user wants to delete.

5

Don’t forget to close the database.

Querying Data

To query the data via a Content Provider, we override the query() method. This method has a long list of paramaters, but usually we just forward most of them to the database call with the same name.

@Override
public Cursor query(Uri uri, String[] projection, String selection,
    String[] selectionArgs, String sortOrder) {
  long id = this.getId(uri); // 1
  SQLiteDatabase db = statusData.dbHelper.getReadableDatabase(); // 2
  if (id < 0) {
    return db.query(StatusData.TABLE, projection, selection, selectionArgs, null,
        null, sortOrder); // 3
  } else {
    return db.query(StatusData.TABLE, projection, StatusData.C_ID + "=" + id, null, null, null,
        null); // 4
  }
}

1

The getId() helper method extracts the ID from the URI that we get.

2

We need to open the database, in this case just for reading.

3

If there’s no ID, we simply forward what we got for the Content Provider to the equivalent database call. Note that the database call has two additional parameters that correspond to SQL GROUPING and HAVING components. Because Content Providers do not support this feature, we simply pass in null.

4

If an ID is present, we use that ID as the WHERE clause to limit what record to return.

Note

We do not close the database here because closing the database will destroy the cursor and we still need it on the receiving end to go over the data returned by the query. One way to handle the cursor is to have the receiver manage it. Activities have a simple startManagingCursor() method for this purpose.

Getting the Data Type

A ContentProvider must return the MIME type of the data it is returning. The MIME type is either single item, or all the records for the given URI. Earlier in this chapter we defined the single-record MIME type is as vnd.android.cursor.item/vnd.marakana.yamba.status and the directory of all statuses as vnd.android.cursor.dir/vnd.marakana.yamba.status. The call we must define, to let others retrieve the MIME type, is called getType().

The first part of the MIME type is either vnd.android.cursor.item or vnd.android.cursor.dir, depending on whether the type represents a specific item or all items we are referring to. The second part, vnd.marakana.yamba.status or vnd.marakana.yamba.mstatus for our app, is a combination of constant vnd followed by your company or app name and the actual content type.

As you may recall, the URI could end with a number. If it does, that number is the ID of the specific record. If it doesn’t, the URI refers to the entire collection.

The following source shows the implementation of getType() as well as the getId() helper method that we’ve already used several times.

@Override
public String getType(Uri uri) {
  return this.getId(uri) < 0 ? MULTIPLE_RECORDS_MIME_TYPE
      : SINGLE_RECORD_MIME_TYPE;  // 1
}

private long getId(Uri uri) {
  String lastPathSegment = uri.getLastPathSegment();  // 2
  if (lastPathSegment != null) {
    try {
      return Long.parseLong(lastPathSegment); // 3
    } catch (NumberFormatException e) { // 4
      // at least we tried
    }
  }
  return -1;   // 5
}

1

getType() uses the helper method getId() to determine whether the URI has an ID part. If it does not—as indicated by a negative return value—we return vnd.android.cursor.dir/vnd.marakana.yamba.mstatus for the MIME type. Otherwise, we’re referring to a single record and the MIME type is vnd.android.cursor.item/vnd.marakana.yamba.status. Of course, we previously defined these values as class constants.

2

To extract the ID in our implementation of getId(), we take the last part of the URI.

3

If that last part is not null, we try to parse it as long and return it.

4

It could be that the last part is not a number at all, in which case the parse will fail.

5

We return -1 to indicate that the given URI doesn’t contain a valid ID.

Updating the Android Manifest File

As with any major building block, we want to define our Content Provider in the Android Manifest XML file. Notice that in this case the android:authorities property specifies the URI authority authorized to access this content provider. Typically, this authority would be your Content Provider class—which we use here—or your package.

<application>
  ...
  <provider android:name=".StatusProvider"
    android:authorities="com.marakana.yamba7.statusprovider" />
  ...
</application>

At this point our content provider is complete and we are ready to use it in other building blocks of Yamba. But since our application already centralizes all data access in a StatusData object that is readily accessible via YambaApplication, we don’t really have a good use for this content provider within the same application. Besides, content providers mostly make sense when we want to expose the data to another application.

Using Content Providers Through Widgets

As mentioned before, Content Providers mostly make sense when you want to expose the data to other applications. It is a good practice to always think of your application as part of a larger Android ecosystem and, as such, a potential provider of useful data to other applications.

To demonstrate how Content Providers can be useful, we’ll create a Home screen widget. We’re not using the term widget here as in a synonym for Android’s View class, but as a useful embedded service for the Home screen to offer.

Android typically ships with a few Home screen widgets. You can access them by going to your home screen, long-pressing on it to pull up an Add to Home Screen dialog, and choosing Widgets. Widgets that come with Android include Alarm Clock, Picture Frame, Power Controls, Music, and Search. Our goal is to create our own Yamba Widget that the user will be able to add to the Home screen.

The Yamba Widget will be simple, displaying just the latest status update. To create it, we’ll create a new YambaWidget Class that subclasses AppWidgetProviderInfo. We’ll also have to register the widget with the manifest file.

Implementing the YambaWidget class

YambaWidget is the main class for our widget. It is a subclass of AppWidgetProvider, a special system class that makes widgets. This class itself is a subclass of BroadcastReceiver, so our Yamba Widget is automatically a broadcast receiver. Basically, whenever our widget is updated, deleted, enabled, or disabled, we’ll get a broadcast intent with that information. So this class inherits the onUpdate(), onDeleted(), onEnabled(), onDisabled(), and onReceive() callbacks. We can override any of these, but typically we care mostly about the updates and general broadcasts we receive.

Now that we understand the overall design of the widget framework, here’s how we implement it.

Example 12.1. YambaWidget.java

package com.marakana.yamba7;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.text.format.DateUtils;
import android.util.Log;
import android.widget.RemoteViews;

public class YambaWidget extends AppWidgetProvider { // 1
  private static final String TAG = YambaWidget.class.getSimpleName();

  @Override
  public void onUpdate(Context context, AppWidgetManager appWidgetManager,
      int[] appWidgetIds) { // 2
    Cursor c = context.getContentResolver().query(StatusProvider.CONTENT_URI,
        null, null, null, null); // 3
    try {
      if (c.moveToFirst()) { // 4
        CharSequence user = c.getString(c.getColumnIndex(StatusData.C_USER)); // 5
        CharSequence createdAt = DateUtils.getRelativeTimeSpanString(context, c
            .getLong(c.getColumnIndex(StatusData.C_CREATED_AT)));
        CharSequence message = c.getString(c.getColumnIndex(StatusData.C_TEXT));

        // Loop through all instances of this widget
        for (int appWidgetId : appWidgetIds) { // 6
          Log.d(TAG, "Updating widget " + appWidgetId);
          RemoteViews views = new RemoteViews(context.getPackageName(),
              R.layout.yamba_widget); // 7
          views.setTextViewText(R.id.textUser, user); // 8
          views.setTextViewText(R.id.textCreatedAt, createdAt);
          views.setTextViewText(R.id.textText, message);
          views.setOnClickPendingIntent(R.id.yamba_icon, PendingIntent
              .getActivity(context, 0, new Intent(context,
                  TimelineActivity.class), 0));
          appWidgetManager.updateAppWidget(appWidgetId, views); // 9
        }
      } else {
        Log.d(TAG, "No data to update");
      }
    } finally {
      c.close(); // 10
    }
    Log.d(TAG, "onUpdated");
  }

  @Override
  public void onReceive(Context context, Intent intent) { // 11
    super.onReceive(context, intent);
    if (intent.getAction().equals(UpdaterService.NEW_STATUS_INTENT)) { // 12
      Log.d(TAG, "onReceived detected new status update");
      AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); // 13
      this.onUpdate(context, appWidgetManager, appWidgetManager
          .getAppWidgetIds(new ComponentName(context, YambaWidget.class))); // 14
    }
  }
}

1

As mentioned before, our widget is a subclass of AppWidgetProvider, which itself is a BroadcastReceiver.

2

This method is called whenever our widget is to be updated, so it’s where we’ll implement the main functionality of the widget. When we register the widget with the system in the manifest file later, we’ll specify the update frequency we’d like. In our case, this method will be called about every 30 minutes.

3

We finally get to use our Content Provider. The whole purpose of this widget in this chapter is to illustrate how to use the StatusProvider that we created earlier. As you have already seen when we implemented the content provider, its API is quite similar to the SQLite database API. The main difference is that instead of passing a table name to a database object, we’re passing a Content URI to the Content Resolver. We still get back the very same Cursor object as we did with databases in Chapter 9, Database.

4

In this particular example, we care only about the very latest status update from the online service. So we position the cursor to the first element. If one exists, it’s our latest status update.

5

In the next few of lines of code, we extract data from the cursor object and store it in local variables.

6

Since the user could have multiple Yamba Widgets installed, we need to loop through them and update them all. We don’t particularly care what the appWidgetId is because we’re doing identical work to update every instance of Yamba Widget. The appWidgetId becomes an opaque handle we use to access each widget in turn.

7

The actual view representing our widget is in another process. To be precise, our widget is running inside the Home application, which acts as its host and is the process we are updating. Hence the RemoteViews constructor. The Remote Views framework is a special shared memory system designed specifically for widgets.

8

Once we have the reference to the Java memory space of our widget views in another process, we can update those views. In this case, we’re setting the status data in the row that represents our widget.

9

Once we update the remote views, the AppWidgetManager call to updateAppWidget() actually posts a message to have the system update our widget. This will happen asynchronously, but shortly after onUpdate() completes.

10

Whether or not the StatusProvider found a new status, we release the data that we might have gotten from the Content Provider. This is just a good practice.

11

The call to onReceive() is not necessary in a typical widget. But since a widget is a Broadcast Receiver, and since our Updater Service does send a broadcast when we get a new status update, this method is a good opportunity to invoke onUpdate() and get the latest status data updated on the widget.

12

We check whether the intent was the one for the new status broadcast.

13

If it was, we get the instance of AppWidgetManager for this context.

14

We then invoke onUpdate().

At this point, we have coded the Yamba Widget, and as a receiver, it will get notified periodically or when there are new updates, and it will loop through all instances of this widget on the Home screen and update them.

Next, we need to set up the layout for our widget.

Creating the XML Layout

The layout for the widget is fairly straightforward. Note that we’re reusing our existing row.xml file that displays status data properly in the Timeline Activity. Here, we just include it along with a little title and an icon to make it look good on the home screen.

Example 12.2. res/layout/yamba_widget.xml

<?xml version="1.0" encoding="utf-8"?>
  <!-- 1 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_height="wrap_content" android:layout_width="fill_parent"
  android:background="@color/edit_text_background"
  android:layout_margin="5dp" android:padding="5dp">
  <!-- 2 -->
  <ImageView android:layout_width="wrap_content" android:src="@drawable/icon"
    android:layout_height="fill_parent" android:id="@+id/yamba_icon"
    android:clickable="true" />
  <!-- 3 -->
  <include layout="@layout/row" />
</LinearLayout>

1

We’re using Linear Layout to hold our widget together. It runs horizontally with the icon on the left and the status data on the right.

2

This is the icon, our standard Yamba icon.

3

Notice the use of <include> element. This is how we include our existing row.xml into this layout so we don’t have to duplicate the code.

This layout is simple enough, but does the job for our particular needs. Next, we need to define some basic information about this widget and its behavior.

Creating the AppWidgetProviderInfo File

This XML file is responsible for describing the widget. It typically specifies what layout this widget uses, how frequently it should be updated by the system, and what its size is.

Example 12.3. res/xml/yamba_widget_info.xml

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  android:initialLayout="@layout/yamba_widget" android:minWidth="294dp"
  android:minHeight="72dp" android:label="@string/msgLastTimelineUpdate"
  android:updatePeriodMillis="1800000" />

In this case we are specifying that we’d like to have to have our widget updated every 30 minutes or so (1800000 milliseconds). Here, we also specify the layout to use, the title of this widget, and its size.

Updating the Manifest File

Finally, we need to update the manifest file and register the widget.

  ...
  <application .../>
    ...
    <receiver android:name=".YambaWidget" android:label="@string/msgLastTimelineUpdate">
      <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
      </intent-filter>
      <intent-filter>
        <action android:name="com.marakana.yamba.NEW_STATUS" />
      </intent-filter>
      <meta-data android:name="android.appwidget.provider"
        android:resource="@xml/yamba_widget_info" />
    </receiver>
    ...
  </application>
  ...

Notice that the widget is a receiver, as we mentioned before. So, just like other broadcast receivers, we declare it within a <receiver> tag inside an <application> element. It is important to register this receiver to receive ACTION_APPWIDGET_UPDATE updates. We do that via the <intent-filter>. The <meta-data> specifies the meta information for this widget in the yamba_widget_info XML file described in the previous section.

That’s it. We now have the widget and are ready to test it.

Test That It Works

To test this widget, install your latest application on the device. Next, go to the Home screen, long-press it, and click on the Widgets choice. You should be able to navigate to the Yamba Widget at this point. After adding it to the Home screen, the widget should display the latest status update.

If your Updater Service is running, you should be able to see the latest updates show up on the Home screen. This means your widget is running properly.

Summary

At this point, the Yamba app is complete—congratulations! You are ready to fine-tune it, customize it, and publish it to the market.

Figure 12.1, “Yamba Completion” illustrates what we have done so far as part of the design outlined in Figure 5.4, “Yamba Design Diagram”.

Figure 12.1. Yamba Completion

Yamba Completion

Site last updated on: April 8, 2011 at 12:51:47 PM PDT
Cover for Learning Android

View 2 comments

  1. Marilyn Escue – Posted Dec. 4, 2010

    In the last sentence above, it seems that you really should be saying that we do not want to have separate databases for each application.

  2. Frank Maker – Posted Jan. 16, 2011

    HTC ones sounds too informal, I'd say HTC models.

Add a comment

View 1 comment

  1. Clemens Hladek – Posted Jan. 31, 2011

    You might look through your book and - after one or two mentions - replace "like any of Android’s main building blocks" with "as usual" or someting...

Add a comment

View 3 comments

  1. Marilyn Escue – Posted Dec. 4, 2010

    In the bullet point for B below, it seems that so-cold is supposed to be "so-called".

  2. Marilyn Escue – Posted Dec. 4, 2010

    In the bullet point for D below, how was the 47 chosen. I understand needing an id, but where did this particular number come from?

  3. Frank Maker – Posted Jan. 16, 2011

    I'd say "different apps can shared information because they are in don't share address spaces, ..."

Add a comment

View 2 comments

  1. alex-san – Posted March 2, 2011

    Isn't it very costly to open (i.e. call getWriteableDatabase) and close the database for each insert, update, etc. operation? In the samples that come with the SDK the database is only opened. In the android developer group we can find the following information: We do not explicitly need to close the database as providers remain around as long as the hosting process does and closing the database is part of the kernel cleanup when this hosting process gets killed. (see https://groups.google.com/group/android-developers/browse_frm/thread/3700d1a47517b745/dc1e88efcf9573a9?hl=en&lnk=gst&q=close+sqlite+provider#dc1e88efcf9573a9)

  2. Isaac Liu – Posted Sept. 6, 2011

    Isn't the purpose of statusData to hide the database calls? In this case why can't we can use statusData.insertOrIgnore(values). Just need to change the return type of the function to return the id.

Add a comment

View 2 comments

  1. Marilyn Escue – Posted Dec. 4, 2010

    In the note above this section, "property" should be "properly".

  2. Frank Maker – Posted Jan. 3, 2011

    Is there a list of standard MIME types?

Add a comment

View 1 comment

  1. Zachary Rowitsch – Posted Jan. 13, 2012

    What is the significance of vnd? And - what is meant by "the constant vnd"?

    Edited on January 13, 2012, 8:15 p.m. PST

Add a comment

View 1 comment

  1. Marilyn Escue – Posted Dec. 4, 2010

    Seems like "Url" should be "Uri".

Add a comment

View 1 comment

  1. Marilyn Escue – Posted Dec. 4, 2010

    Please let me know when these sections are updated so I can review them.

Add a comment

View 2 comments

  1. Frank Maker – Posted Jan. 16, 2011

    s/hchange/change

  2. Isaac Liu – Posted Sept. 6, 2011

    getId source code isn't shown till later, might be worth it to mention it so users won't get compile errors at this point.

    Also, to be consistent with the purpose of statusData, we could add a function in statusData that does the database updates, and call it from here.

    Edited on September 6, 2011, 9:52 a.m. PDT

Add a comment

View 1 comment

  1. Clemens Hladek – Posted Jan. 31, 2011

    PLEASE show a screenshot of this widget HERE!

Add a comment

View 2 comments

  1. Baochuan Lu – Posted Oct. 27, 2011

    Line #3 in Example 12.1 reads as follows in the printed version of this text: Cursor c = context.getContentResolver().query(StatusProvider.CONTENT_URI, null, null, null, StatusData.C_CREATED_AT+" DESC");

  2. Zachary Rowitsch – Posted Jan. 13, 2012

    Seems like 14 comments to a single file might be a bit much - why not do this like we did the ContentProvider in the above section?

Add a comment

View 1 comment

  1. Frank Maker – Posted Jan. 16, 2011

    It's not a security hole, it's just shared memory.

Add a comment