三、分享就是关怀

|   | “数据真的为我们所做的一切提供了动力。” |   | |   | -–杰夫·韦纳,领英 |

在最后一章,我们开始编写自己的联系人管理器。我们遇到了以数据库为中心的应用的各种构件;我们介绍了数据库处理程序和构建查询,以便从数据库中获取有意义的数据。我们还探索了如何在用户界面和数据库之间建立连接,并以可消费的方式呈现给最终用户。

在本章中,我们将学习如何通过内容提供商访问其他应用的数据。我们还将学习如何构建我们自己的内容提供商,以便与其他应用共享我们的数据。我们将调查安卓的提供商,如 contactprovider 。总结一下,我们将构建一个测试应用来使用我们新构建的内容提供者。

在本章中,我们将涵盖以下主题:

  • 什么是内容提供商?
  • 创建内容提供商
  • 实现核心方法
  • 使用内容提供商

什么是内容提供商?

一个内容提供商是安卓应用的第四个组成部分。它用于管理对结构化数据集的访问。内容提供者封装数据,并提供抽象和定义数据安全性的机制。但是,内容提供者主要用于使用提供者的客户端对象访问提供者的其他应用。提供者和提供者客户端一起为数据提供一致的标准接口,该接口还处理进程间通信和安全数据访问。

内容提供商允许一个应用与其他应用共享数据。根据设计,应用创建的安卓 SQLite 数据库是应用私有的;如果从安全的角度考虑是极好的,但是当你想在不同的应用之间共享数据时就麻烦了。这是内容提供商出手相救的地方;通过构建内容提供商,您可以轻松共享数据。需要注意的是,虽然我们的讨论将集中在数据库上,但内容提供商并不局限于此。它还可以用来提供通常进入文件的文件数据,如照片、音频或视频:

What is a content provider?

在上图中,请注意交换数据时应用 A 和 B 之间的交互是如何发生的。这里,我们有一个应用 A ,它的活动需要访问应用 B 的数据库。正如我们已经看到的,应用 B 的数据库存储在内存中,不能被应用 A 直接访问。这就是内容提供商进入画面的地方;它允许我们共享数据和修改对其他应用的访问。内容提供者实现在数据库中查询、插入、更新和删除数据的方法。应用 A 现在请求内容提供商代表它执行一些期望的操作。我们将探索硬币的两面,但我们将首先使用内容提供商从手机的联系人数据库中获取联系人,然后我们将构建我们自己的内容提供商,供他人从我们的数据库中挑选数据。

使用现有内容提供商

安卓列出了很多我们可以使用的标准内容提供商。有BrowserCalendarContractCallLogContactsContactsContractMediaStoreuserDictionary等等。

在我们当前的联系人管理器应用中,我们将添加一项新功能。在AddNewContactActivity类的用户界面中,我们将添加一个小按钮,在系统现有的ContentProviderContentResolver提供商的帮助下,从手机的联系人列表中获取联系人。为此,我们将使用ContactsContract提供商。

什么是内容解析器?

应用上下文中的ContentResolver对象用于作为客户端与提供者通信。ContentResolver对象与提供者对象通信,提供者对象是实现ContentProvider的类的实例。提供程序对象从客户端接收数据请求,执行请求的操作,并返回结果。

ContentResolver是我们的应用中的单个全局实例,提供对其他应用的内容提供商的访问;我们不需要担心处理进程间通信。ContentResolver方法提供持久存储的基本 CRUD(创建、检索、更新和删除)功能;它有调用提供程序对象中同名方法的方法,但不知道实现。随着本章的深入,我们将更详细地介绍ContentResolver

What is a content resolver?

在前面的截图中,注意右侧的新图标,可以直接从电话联系人中添加联系人;我们修改了现有的 XML 来添加图标。相应的类AddNewContactActivity也会被修改:

public void pickContact() {
   try {
       Intent cIntent = new Intent(Intent.ACTION_PICK,
            ContactsContract.Contacts.CONTENT_URI);
      startActivityForResult(cIntent, PICK_CONTACT);
    } catch (Exception e) {
      e.printStackTrace();
      Log.i(TAG, "Exception while picking contact");
    }
   }

我们增加了一个新的方法pickContact()来准备一个意向以便挑选联系人。Intent.ACTION_PICK允许我们从数据源中选择一个项目;此外,我们只需要知道提供者的统一资源标识符 ( URI ),在我们的例子中是ContactsContract.Contacts.CONTENT_URI。此功能也由消息、图库和联系人提供。如果你从第二章连接点中查看代码,你会发现我们使用了相同的代码从图库中拾取图像。将弹出联系人屏幕,允许我们浏览或搜索需要迁移到新应用的联系人。注意onActivityResult,也就是我们的下一站我们将修改这个方法来处理我们对应的请求来处理联系人。让我们看看我们必须添加的代码,以便从安卓的联系人提供商那里挑选联系人:

{
.
.
.

else if (requestCode == PICK_CONTACT) {
      if (resultCode == Activity.RESULT_OK)

       {
          Uri contactData = data.getData();
          Cursor c = getContentResolver().query(contactData, null, null, null, null);
         if (c.moveToFirst()) {
             String id = c
                   .getString(c
                         .getColumnIndexOrThrow(ContactsContract.Contacts._ID));

             String hasPhone = c
                   .getString(c
                         .getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER));

            if (hasPhone.equalsIgnoreCase("1")) {
                Cursor phones = getContentResolver()
                      .query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                           null,
                           ContactsContract.CommonDataKinds.Phone.CONTACT_ID
                                  + " = " + id, null, null);
               phones.moveToFirst();
               contactPhone.setText(phones.getString(phones
                      .getColumnIndex("data1")));

               contactName
                      .setText(phones.getString(phones
                            .getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)));

 }
..

类型

为了给你的应用增加一点天赋,从安卓开发者网站http://goo.gl/4Msuct下载整套模版、资源、动作栏图标包、色板和 Roboto 字体系列。如果没有遵循安卓指导方针的一致的用户界面,设计一个功能性应用是不完整的。

我们首先检查请求代码是否与我们的匹配。然后,我们交叉检查resultcode。我们通过调用Context对象上的getcontentresolver来获取ContentResolver对象;这是android.content.Context班的一个方法。当我们在一个继承自Context的活动中时,我们不需要明确地对其进行调用。服务也是如此。我们现在将验证我们选择的联系人是否有电话号码。在核实必要的详细信息后,我们拉取我们需要的数据,如联系人姓名和电话号码,并在相关字段中设置。

创建内容提供商

内容提供商以两种方式提供对数据的访问:一种是以数据库的形式出现的结构化数据,就像我们目前正在研究的例子一样,或者是以文件数据的形式出现,也就是说,以图片、音频、视频等形式出现的数据存储在应用的私有空间中。在我们开始研究如何创建内容提供商之前,我们还应该回顾一下我们是否需要一个。如果我们想向其他应用提供数据,允许用户将数据从我们的应用复制到另一个应用,或者在我们的应用中使用搜索框架,那么答案是肯定的。

就像其他安卓组件(ActivityServiceBroadcastReceiver)一样,内容提供商是通过扩展ContentProvider类创建的。由于ContentProvider是一个抽象类,我们必须实现六个抽象方法。这些方法如下:

|

方法

|

使用

| | --- | --- | | void onCreate() | 初始化提供程序 | | String getType(Uri) | 返回内容提供者中数据的 MIME 类型 | | int delete(Uri uri, String selection, String[] selectionArgs) | 从内容提供商删除数据 | | Uri insert(Uri uri, ContentValues values) | 将新数据插入内容提供商 | | Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) | 向调用者返回数据 | | int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) | 更新内容提供商中的现有数据 |

这些方法将在后面随着章节的进展和构建我们的应用而被更详细地讨论。

了解 URIs 内容

ContentProvider的每个数据访问方法都有一个内容 URI,作为允许其确定要访问的表、行或文件的参数。它通常遵循以下结构:

content://authority/Path/Id

我们来分析一下 URI 组件的分解。内容提供商的方案永远是content。冒号和双斜线(://)作为权威部分的分隔符。然后,我们有authority部分。根据规定,每个内容提供商的权限必须是唯一的。安卓文档推荐使用的命名约定是内容提供者子类的完全限定类名。通常,它被构建为一个包名加上我们发布的每个内容提供商的限定符。

其余部分是可选的,也称为路径,用于隔离我们的内容提供商可以提供的不同类型的数据。一个很好的例子是MediaStore提供商,它需要区分音频、视频和图像文件。

另一个可选部分是id,指向特定记录;根据id是否存在,URI 分别变为基于 ID 或基于目录。另一种理解方式是,基于标识的 URI 使我们能够在行级别单独与数据交互,而基于目录的 URI 使我们能够与数据库的多行交互。

比如考虑content://com.personalcontactmanager.provider/contacts;我们很快就会遇到这种情况,因为我们将进入定义如何访问我们当前正在构建的内容提供商的章节。

另一方面,应用的包名应该总是唯一的;这是因为 Play Store 上的所有应用都是通过它们的包名来标识的。Play Store 上应用的所有更新都需要具有相同的包名,并使用最初使用的相同密钥库进行签名。例如,下面是一个 Gmail 应用的 Play Store 链接;请注意,在 URL 的末尾,我们会找到应用的包名:

play.google.com/store/apps/details?id=com.google.android.gm

申报我们的合同类

声明合同是构建我们的内容提供商的一个非常重要的部分。顾名思义,这个类将充当我们的内容提供商和将要访问我们的内容提供商的应用之间的合同。它是一个public final类,包含 URIs、列名和其他元数据的常量定义。它也可以包含 Javadoc,但最大的优点是使用它的开发人员不必担心表、列和常数的名称,从而导致不太容易出错的代码。

契约类为我们提供了必要的抽象;我们可以根据需要更改底层操作,也可以更改影响其他相关应用的相应数据操作。需要注意的重要一点是,我们在未来变更合同时需要谨慎;如果我们不小心,我们可能会破坏使用我们的合约类的其他应用。

我们的合同类如下所示:

public final class PersonalContactContract {

   /**
    * The authority of the PersonalContactProvider
    */
   public static final String AUTHORITY = "com.personalcontactmanager.provider";

   public static final String BASE_PATH = "contacts";

   /**
    * The Uri for the top-level PersonalContactProvider
    * authority
    */
   public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY 
         + "/" + BASE_PATH);

   /**
    * The mime type of a directory of items.
    */
   public static final String CONTENT_TYPE =                  
ContentResolver.CURSOR_DIR_BASE_TYPE + 
                  "/vnd.com.personalcontactmanager.provider.table";
   /**
    * The mime type of a single item.
    */
   public static final String CONTENT_ITEM_TYPE = 
ContentResolver.CURSOR_ITEM_BASE_TYPE + 
                 "/vnd.com.personalcontactmanager.provider.table_item";

   /**
    * A projection of all columns 
    * in the items table.
    */
   public static final String[] PROJECTION_ALL = { "_id", 
      "contact_name", "contact_number", 
      "contact_email", "photo_id" };

   /**
    * The default sort order for 
    * queries containing NAME fields.
    */
   //public static final String SORT_ORDER_DEFAULT = NAME + " ASC";

   public static final class Columns {
      public static String TABLE_ROW_ID = "_id";
      public static String TABLE_ROW_NAME  = "contact_name";
      public static String TABLE_ROW_PHONENUM = "contact_number";
      public static String TABLE_ROW_EMAIL = "contact_email";
      public static String TABLE_ROW_PHOTOID = "photo_id";
   }
}

AUTHORITY是的象征性名称,在众多注册为安卓系统一部分的提供商中标识提供商。BASE_PATH是表的路径。CONTENT_URI是提供者封装的表的 URI。CONTENT_TYPE是安卓平台内容 URI 的基本 MIME 类型,包含零个或多个项目的光标。CONTENT_ITEM_TYPE是安卓平台内容 URIs 的基本 MIME 类型,包含单个项目的光标。PROJECTION_ALLColumns包含表格的列标识。

如果没有这些信息,其他开发人员将无法访问您的提供商,即使它是开放的。

提供程序中可以有许多表,每个表都应该有唯一的路径;该路径不是真正的物理路径,而是标识符。

创建 UriMatcher 定义

UriMatcher是一个实用程序类,它有助于在内容提供商中匹配 URIs。addURI()方法采用提供商应该识别的内容 URI 模式。我们添加一个匹配的 URI,当这个 URI 匹配时返回的代码是:

addURI(String authority, String path, int code)

我们将authority、一个path模式和一个整数值传递给UriMatcheraddURI()方法;它返回int值,当我们试图匹配模式时,我们将其定义为常量。

我们的UriMatcher看起来如下:

private static final int CONTACTS_TABLE = 1;
private static final int CONTACTS_TABLE_ITEM = 2;

private static final UriMatcher mmURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
   static {
      mmURIMatcher.addURI(PersonalContactContract.AUTHORITY, 
            PersonalContactContract.BASE_PATH, CONTACTS_TABLE);
      mmURIMatcher.addURI(PersonalContactContract.AUTHORITY, 
            PersonalContactContract.BASE_PATH+  "/#",  
                       CONTACTS_TABLE_ITEM);
   }

注意它也支持使用通配符;我们在前面的代码片段中使用了 hashtag ( #),我们也可以使用*等通配符。在我们的例子中,通过标签," content://com.personalcontactmanager.provider/contacts/2"这个表达式匹配,但是使用* "content://com.personalcontactmanager.provider/contacts它不匹配。

实施核心方法

为了构建我们的内容提供者,下一步将是准备我们的核心数据库访问和数据修改方法,更好地称为 CRUD 方法。这就是我们希望如何根据收到的插入、查询或删除调用与数据交互的核心逻辑。我们还将实现安卓架构的生命周期方法,如onCreate()

通过 onCreate()方法初始化提供程序

我们在onCreate()中创建了一个数据库管理器类的对象。oncreate()中应该有最少的操作,因为它运行在主用户界面线程上,可能会给一些用户造成延迟。在oncreate()避免长时间运行任务是很好的做法,因为这增加了提供商的启动时间。甚至建议推迟数据库创建和数据加载,直到我们的提供者实际收到数据请求,也就是说,将持久的操作转移到 CRUD 方法:

@Override
Public Boolean onCreate() {
   dbm = new DatabaseManager(getContext());
   return false;
}   

通过 query()方法查询记录

query()方法将在结果集上返回一个光标。URI 传递给我们的UriMatcher,看它是否匹配我们之前定义的任何模式。在我们的 switch case 语句中,如果是表项相关的案例,我们检查selection语句是否为空;在这种情况下,我们将选择语句构建到lastpathsegment,否则我们将选择附加到lastpathsegment语句。我们使用一个DatabaseManager对象在数据库上运行查询,结果得到一个游标。期望query()方法抛出IllegalArgumentException告知未知的 URI;如果我们在查询过程中遇到内部错误,抛出 a nullPointerException也是一个很好的做法:

@Override
public Cursor query(Uri uri, String[] projection, String selection,
      String[] selectionArgs, String sortOrder) {

   int uriType = mmURIMatcher.match(uri);
   switch(uriType) {

   case CONTACTS_TABLE:
      break;
   case CONTACTS_TABLE_ITEM:
      if (TextUtils.isEmpty(selection)) {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID 
                  + "=" + uri.getLastPathSegment();
      } else {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID 
                  + "=" + uri.getLastPathSegment() + 
               " and " + selection;
      }
      break;
   default:
      throw new IllegalArgumentException("Unknown URI: " + uri);
   }

   Cursor cr = dbm.getRowAsCursor(projection, selection, 
               selectionArgs, sortOrder);

   return cr;
}

请记住,安卓系统必须能够跨越进程边界传递异常。安卓可以对以下异常执行此操作,这些异常可能有助于处理查询错误:

  • IllegalArgumentException:如果您的提供商收到无效内容 URI,您可以选择抛出此问题
  • NullPointerException:这个是当对象为空并且我们试图访问它的字段或方法时抛出的

通过 insert()方法添加记录

顾名思义,insert()方法用于在我们的数据库中插入一个值。它返回插入行的 URI,在检查 URI 时,我们需要记住插入可以发生在表级别,因此方法中的操作在与表匹配的 URI 处理。匹配后,我们使用标准的DatabaseManager对象将新值插入数据库。新行的内容 URI 是通过将新行的_ID值附加到表的内容 URI 来构建的:

@Override
public Uri insert(Uri uri, ContentValues values) {

   int uriType = mmURIMatcher.match(uri);
   long id;

   switch(uriType) {
   case CONTACTS_TABLE:
      id = dbm.addRow(values);
      break;
   default:
      throw new IllegalArgumentException("Unknown URI: " + uri);
   }

   Uri ur = ContentUris.withAppendedId(uri, id);
   return ur;
}

通过 update()方法更新记录

update()方法使用ContentValues参数中的值更新相应表中的现有行。首先,我们识别 URI,无论它是基于目录还是基于 ID,然后我们像在query()方法中一样构建我们的选择语句。现在,我们将执行前面在第 2 章中构建此应用时定义的DatabaseManager的标准updateRow()方法,该方法返回受影响的行数。

update()方法返回更新的行数。根据 selection 子句,可以更新一行或多行:

@Override
public int update(Uri uri, ContentValues values, String selection,
      String[] selectionArgs) {
   int uriType = mmURIMatcher.match(uri);

   switch(uriType) {
   case CONTACTS_TABLE:
      break;
   case CONTACTS_TABLE_ITEM:
      if (TextUtils.isEmpty(selection)) {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID
 + "=" + uri.getLastPathSegment();
      } else {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID 
+ "=" + uri.getLastPathSegment() 
+ " and " + selection;
      }
      break;
   default:
      throw new IllegalArgumentException("Unknown URI: " + uri);
   }

   int count = dbm.updateRow(values, selection, selectionArgs);

   return count;
}

通过 delete()方法删除记录

delete()法和update()法很像,使用的过程也是相似;这里,调用是删除一行,而不是更新它。delete()方法返回删除的行数。根据 selection 子句,可以删除一行或多行:

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {

   int uriType = mmURIMatcher.match(uri);

   switch(uriType) {
   case CONTACTS_TABLE:
      break;
   case CONTACTS_TABLE_ITEM:
      if (TextUtils.isEmpty(selection)) {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID
 + "=" + uri.getLastPathSegment();
      } else {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID 
 + "=" + uri.getLastPathSegment() 
 + " and " + selection;
      }
      break;
   default:
      throw new IllegalArgumentException("Unknown URI: " + uri);
   }

   int count = dbm.deleteRow(selection, selectionArgs);

   return count;
}

通过 getType()方法获取数据的返回类型

这个简单方法的签名采用 URI 并返回一个字符串值;每个内容提供商必须返回其支持的 URIs 的内容类型。一个非常有趣的事实是,应用访问这些信息不需要任何权限;如果我们的内容提供者需要权限,或者没有被导出,所有的应用仍然可以调用这个方法,不管它们的访问权限如何来检索 MIME 类型。

所有这些 MIME 类型都应该在协定类中声明:

@Override
public String getType(Uri uri) {

   int uriType = mmURIMatcher.match(uri);
   switch(uriType) {
   case CONTACTS_TABLE:
      return PersonalContactContract.CONTENT_TYPE;
   case CONTACTS_TABLE_ITEM:
      return PersonalContactContract.CONTENT_ITEM_TYPE;
   default:
      throw new IllegalArgumentException("Unknown URI: " + uri);   
   }

}

向清单添加提供者

另一个重要步骤是将我们的内容提供商添加到清单中,就像我们对其他安卓组件所做的那样。我们可以在这里注册多个提供商。这里重要的一点,除了android:authorities,就是android:exported;它定义了内容提供商是否可供其他应用使用。在true的情况下,该提供商可用于其他应用;如果是false,则该提供商不可用于其他应用。如果应用与提供程序具有相同的用户标识(UID),它们将有权访问它:

<provider
   android:name="com.personalcontactmanager.provider.PersonalContactProvider"
   android:authorities="com.personalcontactmanager.provider"
   android:exported="true"
   android:grantUriPermissions="true" >
   </provider>

另一个重要的概念是 权限。我们可以通过添加读写权限来增加额外的安全性,其他应用必须在它们的清单 XML 文件中添加这些权限,然后自动通知用户它们将使用特定应用的内容提供程序来读取、写入或两者兼而有之。我们可以通过以下方式添加权限:

android:readPermission="com.personalcontactmanager.provider.READ"

使用内容提供商

我们构建内容提供者的主要原因是允许其他应用访问我们数据库中复杂的数据存储并执行 CRUD 操作。我们现在将再构建一个应用,以便测试我们新构建的内容提供商。测试应用非常简单,只包含一个活动类和一个布局文件。它有标准按钮来执行操作。没什么稀奇的,只是我们用来测试刚刚实现的功能的工具。我们现在将深入研究TestMainActivity类并研究它的实现:

public class TestMainActivity extends Activity {

public final String AUTHORITY = "com.personalcontactmanager.provider";
public final String BASE_PATH = "contacts";
private TextViewqueryT, insertT;

public class Columns {
   public final static String TABLE_ROW_ID = "_id";
   public final static String TABLE_ROW_NAME = "contact_name";
   public final static String TABLE_ROW_PHONENUM =

"contact_number";
   public final static String TABLE_ROW_EMAIL = "contact_email";
   public final static String TABLE_ROW_PHOTOID = "photo_id";
   }

要访问一个内容提供者,我们需要诸如AUTHORITYBASE_PATH等细节以及数据库表的列名;为此,我们需要进入公共课堂Columns。我们有更多的表,我们会看到更多这样的类。一般来说,所有这些必要的信息都将取自内容提供商发布的合同类别。一些内容提供者还要求在清单中实现读或写权限:

<uses-permissionandroid:name="AUTHORITY.permission.WRITE_TASKS"/>

在某些情况下,我们需要访问的内容提供商可以要求我们在清单中添加权限。当用户安装应用时,他们将在其权限列表中看到添加的权限:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_test_main);
   queryT = (TextView) findViewById(R.id.textQuery);
   insertT = (TextView) findViewById(R.id.textInsert);
   }

要试用其他应用的内容提供商,请参考http://goo.gl/NEX2hN

它列出了如何使用 Any.do 的内容提供程序——一个非常著名的任务应用。

我们将设置我们的布局,并初始化我们在onCreate()活动中需要的视图。要进行查询,我们首先需要准备与该表匹配的 URI 对象。

内容解析器现在开始发挥作用;它充当了我们准备的 URI 内容的解析器。在这种情况下,我们的getContentResolver.query()方法将获取所有的列和行。我们现在将光标移动到第一个位置,以便读取结果。出于测试目的,它被读取为字符串:

public void query(View v) {
  Uri contentUri = Uri.parse("content://" + AUTHORITY 
               + "/" + BASE_PATH);

  Cursor cr = getContentResolver().query(contentUri, null, 
            null, null, null);     

  if (cr != null) {
      if (cr.getCount() > 0) {
         cr.moveToFirst();
         String name = cr.getString(cr.getColumnIndexOrThrow( 
Columns.TABLE_ROW_NAME));
         queryT.setText(name);
      }
  }

  ....
  ....
}

现在,我们构建一个 URI 来读取一个特定的行,而不是一个完整的表。我们已经提到,要使 URI 基于 ID,我们需要将 ID 部分添加到我们现有的contenturi中。现在,我们构建我们的投影字符串数组,作为参数在我们的query()方法中传递:

public void query(View v) {

 ...
 ...

  Uri rowUri = contentUri = ContentUris.withAppendedId
            (contentUri, getFirstRowId());

  String[] projection = new String[] {
      Columns.TABLE_ROW_NAME, Columns.TABLE_ROW_PHONENUM,
      Columns.TABLE_ROW_EMAIL, Columns.TABLE_ROW_PHOTOID };

  cr = getContentResolver().query(contentUri, projection,
      null, null, null);

  if (cr != null) {
      if (cr.getCount() > 0) {
         cr.moveToFirst();
         String name = cr.getString(cr.getColumnIndexOrThrow(
                  Columns.TABLE_ROW_NAME));

         queryT.setText(name);

      }
  }

}   

getFirstRowId()方法获取表中第一行的 ID。这样做是因为第一行的 ID 不会一直是1。删除行时,它会发生变化。如果删除行标识为1的表格中的第一项,则行标识为1的第二项成为第一项:

private int getFirstRowId() {

  int id = 1;
  Uri contentUri = Uri.parse("content://" + AUTHORITY + "/"
               + "contacts");
  Cursor cr = getContentResolver().query(contentUri, null,
            null, null, null);
  if (cr != null) {
      if (cr.getCount() > 0) {
         cr.moveToFirst();
         id = cr.getInt(cr.getColumnIndexOrThrow(
            Columns.TABLE_ROW_ID));
      }
  }
return id;

}

让我们仔细看看的query()方法:

public final Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

在应用编程接口级别 1 中,query()方法根据我们提供的参数将光标返回到结果集上。以下是前面代码的参数:

  • uri:这个就是我们这里的contentURI,对于要检索的内容使用content://方案。它可以基于标识或基于目录。
  • projection:这个是一个要返回的列的列表,因为我们已经使用列名准备好了。通过null将返回所有列。
  • selection:将格式化为一个 SQL WHERE子句,不包括WHERE本身,它作为一个过滤器来声明要返回哪些行。
  • selectionArgs :我们可能会在selection中加入?参数标记。安卓 SQL 查询构建器将按照参数标记在selection中出现的顺序,用从selectionArgs绑定为字符串的值替换?参数标记。
  • sortOrder: This tells us how to order the rows, formatted as an SQL ORDER BY clause. A null value will use the default sort order.

    根据官方文件,为了获得最佳性能,我们应该遵循以下几条准则:

    • 提供一个显式投影,以防止从存储中读取不使用的数据。
    • 使用问号参数标记,如phone=?代替选择参数中的显式值,以便仅那些值不同的查询将被识别为相同的,用于缓存目的。

执行与我们之前检查null值和空光标相同的过程,最后从光标中提取所需的值。

现在,让我们看看我们测试应用的insert方法。

我们通过构建我们的内容值对象和相关的键值对来开始,例如,在相关的Columns.TABLE_ROW_PHONENUM字段中输入一个电话号码。请注意,因为像列名这样的细节是以类的形式与我们共享的,所以我们不需要担心像实际列名这样的细节。我们只需要通过Columns类访问它。这确保了我们只需要更新相关的值。如果将来内容提供商经历了一些变化并更改了表名,其余的功能和实现将保持不变。我们用所需的列名构建投影字符串数组,就像我们前面在向内容提供者查询数据时所做的那样。

我们也建设我们的内容 URI;请注意,它匹配表,而不是单个行。insert()方法也返回一个 URI,这与query()方法不同,后者在结果集上返回一个光标:

public void insert(View v) {

  String name = getRandomName();
  String number = getRandomNumber();

  ContentValues values = new ContentValues();
  values.put(Columns.TABLE_ROW_NAME, name);
  values.put(Columns.TABLE_ROW_PHONENUM, number);
  values.put(Columns.TABLE_ROW_EMAIL, name + "@gmail.com");
  values.put(Columns.TABLE_ROW_PHOTOID, "abc");

  String[] projection = new String[] {
      Columns.TABLE_ROW_NAME, Columns.TABLE_ROW_PHONENUM,
      Columns.TABLE_ROW_EMAIL, Columns.TABLE_ROW_PHOTOID };

  Uri contentUri = Uri.parse("content://" + AUTHORITY + "/"
            + BASE_PATH);

  Uri insertedRowUri = getContentResolver().insert(
            contentUri, values);

  //checking the added row
  Cursor cr = getContentResolver().query(insertedRowUri,
         projection, null, null, null);

  if (cr != null) {
      if (cr.getCount() > 0) {
           cr.moveToFirst();
           name = cr.getString(cr.getColumnIndexOrThrow(
               Columns.TABLE_ROW_NAME));
           insertT.setText(name);
      }
  }

}

getRandomName()getRandomNumber()方法生成一个随机的名称和数字插入到表中:

private String getRandomName() {

      Random rand = new Random();
      String name = "" + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26)) ;

      return name;
}

public String getRandomNumber() {
  Random rand = new Random();
  String number = rand.nextInt(98989)*rand.nextInt(59595)+"";

  return number;
}

让我们仔细看看insert()法:

public final Uri insert (Uri url, ContentValues values)

以下是前一行代码的参数:

  • url:要插入数据的表格的网址
  • values:以ContentValues对象形式新插入的行的值,关键是字段的列名

请注意插入后,我们正在使用insert()方法返回的 URI 再次运行query()方法。我们运行这个来查看我们想要插入的值是否已经被插入;该查询将根据附加了标识的行的投影返回列。

到目前为止,我们已经涵盖了query()insert()方法;现在,我们将介绍update()法。

通过准备ContentValues对象,我们在insert()方法中取得了进展。同样,我们将准备一个对象,我们将在ContentResolverupdate()方法中使用该对象来更新现有行。在这种情况下,我们将按照 ID 构建我们的 URI,因为这个操作是基于 ID 的。按照rowUri对象的指示更新行,它将返回更新的行数,与 URI 相同;在这种情况下,rowUri只指向一行。另一种方法是结合使用contentUri(指向表格)和selection / selectionArgs。在这种情况下,根据selection条款,更新的行可能不止一行:

public void update(View v) {

  String name = getRandomName();
  String number = getRandomNumber();

  ContentValues values = new ContentValues();
  values.put(Columns.TABLE_ROW_NAME, name);
  values.put(Columns.TABLE_ROW_PHONENUM, number);
  values.put(Columns.TABLE_ROW_EMAIL, name + "@gmail.com");
  values.put(Columns.TABLE_ROW_PHOTOID, " ");

  Uri contentUri = Uri.parse("content://" + AUTHORITY
                    + "/" + BASE_PATH);
  Uri rowUri = ContentUris.withAppendedId(
                    contentUri, getFirstRowId());
  int count = getContentResolver().update(rowUri, values, null, null);

}

让我们仔细看看update()法:

public final int update (Uri uri, ContentValues values, String where, String[] selectionArgs)

以下是前一行代码的参数:

  • uri:这个是我们希望修改的内容 URI
  • values:这个和我们之前用别的方法用的值差不多;传递null值将删除现有字段值
  • where:一个 SQL WHERE子句,在更新行之前作为行的过滤器

我们可以再次运行query()方法,看看是否反映了变化;这个活动是留给你的练习。

最后一个方法是delete(),我们需要它来完成我们的 CRUD 方法库。delete()方法的开始方式与其他方法相似;首先,在目录级别准备我们的内容 URI,然后为 ID 级别构建它,也就是说,在单独的行级别。之后,我们把它传给ContentResolverdelete()法。与返回整数值的query()insert()方法不同,delete()方法删除基于标识的内容 URI 对象rowUri所指向的行,并返回删除的行数。在我们的情况下,这将是1,因为我们的 URI 只指向一行。另一种方法是结合使用指向表格的contentUriselection / selectionArgs。在这种情况下,根据selection条款,删除的行可能超过 1 行:

public void delete(View v) {

      Uri contentUri = Uri.parse("content://" + AUTHORITY
                              + "/" + BASE_PATH);
      Uri rowUri = contentUri = ContentUris.withAppendedId(
                              contentUri, getFirstRowId());
      int count = getContentResolver().delete(rowUri, null,
               null);
}

用户界面和输出如下所示:

Using a content provider

如果你想深入了解安卓内容提供商实际上是如何管理各种表之间的各种读写调用的(提示:它使用CountDownLatch),你可以查看道格拉斯·c·施密特博士在 Coursera 的视频了解更多信息。视频可以在https://class.coursera.org/posa-002/lecture/49找到。

总结

在本章中,我们介绍了内容提供商的基础知识。我们学习了如何访问系统提供的内容提供商,甚至是我们自己版本的内容提供商。我们从创建一个基本的联系人管理器,到通过实现ContentProvider将它发展成为安卓生态系统的一个完全成熟的公民,以便在其他应用之间共享数据。

在下一章中,我们将介绍LoadersCursorAdapters、俏皮的黑客和技巧,以及一些开源库,让我们在使用 SQLite 数据库时的生活变得更加轻松。