Android开发- 数据库和Content Provider

SQLiteOpenHelper

SQLiteOpenHelper是一个抽象类,用来实现创建、打开和升级数据库的最佳实践模式。

  private static class HoardDBOpenHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "myDatabase.db";
    private static final String DATABASE_TABLE = "GoldHoards";
    private static final int DATABASE_VERSION = 1;

    // SQL Statement to create a new database.
    private static final String DATABASE_CREATE = "create table " +
      DATABASE_TABLE + " (" + KEY_ID +
      " integer primary key autoincrement, " +
      KEY_GOLD_HOARD_NAME_COLUMN + " text not null, " +
      KEY_GOLD_HOARDED_COLUMN + " float, " +
      KEY_GOLD_HOARD_ACCESSIBLE_COLUMN + " integer);";

    public HoardDBOpenHelper(Context context, String name,
                      CursorFactory factory, int version) {
      super(context, name, factory, version);
    }

    // Called when no database exists in disk and the helper class needs
    // to create a new one.
    @Override
    public void onCreate(SQLiteDatabase db) {
      db.execSQL(DATABASE_CREATE);
    }

    // Called when there is a database version mismatch meaning that
    // the version of the database on disk needs to be upgraded to
    // the current version.
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion,
                          int newVersion) {
      // Log the version upgrade.
      Log.w("TaskDBAdapter", "Upgrading from version " +
        oldVersion + " to " +
        newVersion + ", which will destroy all old data");

      // Upgrade the existing database to conform to the new
      // version. Multiple previous versions can be handled by
      // comparing oldVersion and newVersion values.

      // The simplest case is to drop the old table and create a new one.
      db.execSQL("DROP TABLE IF IT EXISTS " + DATABASE_TABLE);
      // Create a new one.
      onCreate(db);
    }
  }

要使用SQLiteOpenHelper访问数据库,需要调用getWritableDatabase或者getReadableDatabase来分别打开和获得底层数据库的一个可写或只读的实例。

当数据库成功打开后,SQLiteOpenHelper将缓存它,以便每次查询数据库或执行数据库事物时能够(并且应该)使用这些方法,而不是在你的应用程序中缓存打开的数据库。

如果磁盘空间不够或者没有足够的权限,对getWritableDatabase的调用可能失败,因此如有必要,在需要查询数据库时应该使用getReadableDatabase方法作为后备。在大多数情况下,它将提供与getWritableDatabase相同的、已缓存的可写数据库实例,除非该数据库实例还不存在,或者存在相同的权限或磁盘空间问题,那时它将返回一个只读的数据库实例副本。

不使用SQLiteOpenHelper的情况下打开和创建数据库

使用应用程序Context对象的openOrCreateDatabase方法来创建数据库本身。

Context contex=this.getApplicationContext();

SQLiteDatabase db=contex.openOrCreateDatabase(DATABASE_NAME,Context.MODE_PRIVATE,null);

查询数据库

每个数据查询都会作为一个Cursor返回。

  private Cursor getAccessibleHoard() {
    /**
     * Listing 8-3: Querying a database
     */
    // Specify the result column projection. Return the minimum set
    // of columns required to satisfy your requirements.
    String[] result_columns = new String[] {
      KEY_ID, KEY_GOLD_HOARD_ACCESSIBLE_COLUMN, KEY_GOLD_HOARDED_COLUMN }; 

    // Specify the where clause that will limit our results.
    String where = KEY_GOLD_HOARD_ACCESSIBLE_COLUMN + "=" + 1;

    // Replace these with valid SQL statements as necessary.
    String whereArgs[] = null;
    String groupBy = null;
    String having = null;
    String order = null;

    SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
    Cursor cursor = db.query(HoardDBOpenHelper.DATABASE_TABLE,
                             result_columns, where,
                             whereArgs, groupBy, having, order);
    //
    return cursor;
  }

出于效率考虑,只有当确信不再需要数据库实例时-通常是使用它的Activity或者Service被终止时-才关闭它们。

从Cursor中取值

Cursor API: https://developer.android.com/reference/android/database/Cursor.html

以下示例展示了如何遍历一个结果,以提取一个列中的浮点值并计算其平均值。

  public float getAverageAccessibleHoardValue() {
    Cursor cursor = getAccessibleHoard();

    /**
     * Listing 8-4: Extracting values from a Cursor
     */
    float totalHoard = 0f;
    float averageHoard = 0f;

    // Find the index to the column(s) being used.
    int GOLD_HOARDED_COLUMN_INDEX =
      cursor.getColumnIndexOrThrow(KEY_GOLD_HOARDED_COLUMN);

    // Iterate over the cursors rows.
    // The Cursor is initialized at before first, so we can
    // check only if there is a "next" row available. If the
    // result Cursor is empty this will return false.
    while (cursor.moveToNext()) {
      float hoard = cursor.getFloat(GOLD_HOARDED_COLUMN_INDEX);
      totalHoard += hoard;
    }

    // Calculate an average -- checking for divide by zero errors.
    float cursorCount = cursor.getCount();
    averageHoard = cursorCount > 0 ?
                     (totalHoard / cursorCount) : Float.NaN;

    // Close the Cursor when you‘ve finished with it.
    cursor.close();

    return averageHoard;
  }

添加、更新和删除行

要添加一个新行,可以构造一个ContentValues对象,并使用它的put方法来添加代表每一列的名称机器相关值的键/值对。

  public void addNewHoard(String hoardName, float hoardValue, boolean hoardAccessible) {
    /**
     * Listing 8-5: Inserting new rows into a database
     */
    // Create a new row of values to insert.
    ContentValues newValues = new ContentValues();

    // Assign values for each row.
    newValues.put(KEY_GOLD_HOARD_NAME_COLUMN, hoardName);
    newValues.put(KEY_GOLD_HOARDED_COLUMN, hoardValue);
    newValues.put(KEY_GOLD_HOARD_ACCESSIBLE_COLUMN, hoardAccessible);
    // [ ... Repeat for each column / value pair ... ]

    // Insert the row into your table
    SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
    /*
    第二个参数称为null列侵入(null column hack)
    如果想在SQLite中添加一个空行,在传入一个空的ContentValues时,还必须传入一个值可以显示设置为null的列的名称。
    向一个SQLite数据库插入一个新行时,必须显示地指定至少一个列及对于的值,这个值可以是null。
    如果将null column hack参数设定为null,那么在插入一个空的ContentValues对象时,SQLite将抛出一个异常。
     */
    db.insert(HoardDBOpenHelper.DATABASE_TABLE, null, newValues);
  }

更新行

  public void updateHoardValue(int hoardId, float newHoardValue) {
    /**
     * Listing 8-6: Updating a database row
     */
    // Create the updated row Content Values.
    ContentValues updatedValues = new ContentValues();

    // Assign values for each row.
    updatedValues.put(KEY_GOLD_HOARDED_COLUMN, newHoardValue);
    // [ ... Repeat for each column to update ... ]

    // Specify a where clause the defines which rows should be
    // updated. Specify where arguments as necessary.
    String where = KEY_ID + "=" + hoardId;
    String whereArgs[] = null;

    // Update the row with the specified index with the new values.
    SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
    db.update(HoardDBOpenHelper.DATABASE_TABLE, updatedValues,
              where, whereArgs);
  }

删除行

  public void deleteEmptyHoards() {
    /**
     * Listing 8-7: Deleting a database row
     */
    // Specify a where clause that determines which row(s) to delete.
    // Specify where arguments as necessary.
    String where = KEY_GOLD_HOARDED_COLUMN + "=" + 0;
    String whereArgs[] = null;

    // Delete the rows that match the where clause.
    SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
    db.delete(HoardDBOpenHelper.DATABASE_TABLE, where, whereArgs);
  }


Content Provider

  1. ContentProvider为存储和获取数据提供了统一的接口。ContentProvide对数据进行封装,不用关心数据存储的细节。使用表的形式来组织数据。
  2. 使用ContentProvider可以在不同的应用程序之间共享数据。
  3. Android为常见的一些数据提供了默认的ContentProvider(包括音频、视频、图片和通讯录等)。

Uri

为系统的每一个资源给其一个名字,比方说通话记录。

  1. 每一个ContentProvider都拥有一个公共的URI,这个URI用于表示这个ContentProvider所提供的数据。
  2. Android所提供的ContentProvider都存放在android.provider包中。 将其分为A,B,C,D 4个部分:


A:标准前缀,用来说明一个Content Provider控制这些数据,无法改变的;"content://"
B:URI 的标识,用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。它定义了是哪个Content Provider提供这些数据。对于第三方应用程序,为了保证URI标识的唯一性,它必须是一个完整的、小写的类名。这个标识在 元素的 authorities属性中说明:一般是定义该ContentProvider的包.类的名称
C:路径(path),通俗的讲就是你要操作的数据库中表的名字,或者你也可以自己定义,记得在使用的时候保持一致就可以了;"content://com.bing.provider.myprovider/tablename"
D:如果URI中包含表示需要获取的记录的ID;则就返回该id对应的数据,如果没有ID,就表示返回全部; "content://com.bing.provider.myprovider/tablename/#" #表示数据id。

PS:

路径(path)可以用来表示我们要操作的数据,路径的构建应根据业务而定,如下:
1、要操作person表中id为10的记录,可以构建这样的路径:/person/10
2、要操作person表中id为10的记录的name字段, person/10/name
3、要操作person表中的所有记录,可以构建这样的路径:/person
4、要操作xxx表中的记录,可以构建这样的路径:/xxx
5、当然要操作的数据不一定来自数据库,也可以是文件、xml或网络等其他存储方式,如下:
要操作xml文件中person节点下的name节点,可以构建这样的路径:/person/name
6、如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法,如下:Uri uri = Uri.parse("content://com.bing.provider.personprovider/person")

创建Content Provider

Content Provider提供了一个接口用来发布数据,通过Content Resolver来使用该数据。它们允许将使用数据的应用程序组建和底层的数据源分离,并提供了一种通用机制来允许一个应用程序共享它们的数据或者使用其他应用程序提供的数据。

要想创建一个新的Content Provider,可以扩展ContentProvider抽象类

public class MyContentProvider extends ContentProvider

同样需要重写onCreate处理程序来初始化底层的数据源,以及重写query、update、delete、insert和getType方法来实现Content Resolver用来和数据进行交互的接口。

注册Content Provider

同Activity和Service一样,在Content Resolver能够找到Content Provider之前,Content Provider必须在应用程序清单(AndroidManifest.xml)文件中进行注册

<provider android:name=".MyContentProvider"
      android:authorities="com.paad.skeletondatabaseprovider"/>

发布Content Provider的URI地址

每一个Content Provider都应该使用一个公有的静态CONTENT_URI属性来公开它的授权,使其更容易被找到。

UriMatcher API:https://developer.android.com/reference/android/content/UriMatcher.html

  /**
   * Listing 8-8: Defining a UriMatcher to determine if a request is for all elements or a single row
   */
  //Create the constants used to differentiate between the different URI
  //requests.
  private static final int ALLROWS = 1;
  private static final int SINGLE_ROW = 2;

  private static final UriMatcher uriMatcher;

  //Populate the UriMatcher object, where a URI ending in
  //‘elements‘ will correspond to a request for all items,
  //and ‘elements/[rowID]‘ represents a single row.
  static {
   uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
   uriMatcher.addURI("com.paad.skeletondatabaseprovider",
                     "elements", ALLROWS);
   uriMatcher.addURI("com.paad.skeletondatabaseprovider",
                     "elements/#", SINGLE_ROW);
  }

定义了UriMatcher以后,可以很容易的使用SQLiteQueryBuilder类对一个查询应用额外的选择条件。

    // Use an SQLite Query Builder to simplify constructing the
    // database query.
    SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();

    // If this is a row query, limit the result set to the passed in row.
    switch (uriMatcher.match(uri)) {
      case SINGLE_ROW :
        String rowID = uri.getPathSegments().get(1);
        queryBuilder.appendWhere(KEY_ID + "=" + rowID);
      default: break;
    }

创建Content Provide的数据库

在ContentProvider派生类中重写onCreate方法,来初始化打算通过Content Provider访问的数据源。

private static class MySQLiteOpenHelper extends SQLiteOpenHelper {
    public MySQLiteOpenHelper(Context context, String name,
                      CursorFactory factory, int version) {
      super(context, name, factory, version);
    }
}

public class MyContentProvider extends ContentProvider {
  private MySQLiteOpenHelper myOpenHelper;

  @Override
  public boolean onCreate() {
    // Construct the underlying database.
    // Defer opening the database until you need to perform
    // a query or transaction.
    myOpenHelper = new MySQLiteOpenHelper(getContext(),
        MySQLiteOpenHelper.DATABASE_NAME, null,
        MySQLiteOpenHelper.DATABASE_VERSION);

    return true;
  }
}

当应用程序启动时,每个Content Provider的onCreate处理程序会在应用程序的主线程中被调用。

考虑到效率因素,在应用程序运行的时候,最好打开Content Provider。如果系统需要额外的资源,应用程序将会关闭,与之相关联的数据库也会关闭。所以,在任何时候都没有必要手动关闭数据库

实现Content Provider查询

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

    // Open thedatabase.
    SQLiteDatabase db;
    try {
      db = myOpenHelper.getWritableDatabase();
    } catch (SQLiteException ex) {
      db = myOpenHelper.getReadableDatabase();
    }

    // Replace these with valid SQL statements if necessary.
    String groupBy = null;
    String having = null;

    // Use an SQLite Query Builder to simplify constructing the
    // database query.
    SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();

    // If this is a row query, limit the result set to the passed in row.
    switch (uriMatcher.match(uri)) {
      case SINGLE_ROW :
        String rowID = uri.getPathSegments().get(1);
        queryBuilder.appendWhere(KEY_ID + "=" + rowID);
      default: break;
    }

    // Specify the table on which to perform the query. This can
    // be a specific table or a join as required.
    queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TABLE);

    // Execute the query.
    Cursor cursor = queryBuilder.query(db, projection, selection,
        selectionArgs, groupBy, having, sortOrder);

    // Return the result Cursor.
    return cursor;
  }
}

实现查询后,还必须制定一个MIME类型来标识返回的数据。通过重写getType方法来返回唯一地描述了该数据类型的字符串。

返回的类型应该包含两种形式,一种表示单一的项,另一种表示所有的项。

单一项:vnd.android.cursor.item/vnd.<companyname>.<contenttype>

所有项:vnd.android.cursor.dir/vnd.<companyname>.<contenttype>

  @Override
  public String getType(Uri uri) {
    // Return a string that identifies the MIME type
    // for a Content Provider URI
    switch (uriMatcher.match(uri)) {
      case ALLROWS:
        return "vnd.android.cursor.dir/vnd.paad.elemental";
      case SINGLE_ROW:
        return "vnd.android.cursor.item/vnd.paad.elemental";
      default:
        throw new IllegalArgumentException("Unsupported URI: " +
                                             uri);
    }
  }

Content Provider事务

当执行修改数据集的事务时,最好调用Content Resolver的notifyChange方法。它将通知所有已注册的Content Observer底层的表(或者一个特殊的行)已经被删除、添加或者更新。这些Content Observer是使用Cursor.registerContentObserver方法为一个给定的Cursor注册的。

public class MyContentProvider extends ContentProvider {
  @Override
  public int delete(Uri uri, String selection, String[] selectionArgs) {
    // Open a read / write database to support the transaction.
    SQLiteDatabase db = myOpenHelper.getWritableDatabase();

    // If this is a row URI, limit the deletion to the specified row.
    switch (uriMatcher.match(uri)) {
      case SINGLE_ROW :
        String rowID = uri.getPathSegments().get(1);
        selection = KEY_ID + "=" + rowID
            + (!TextUtils.isEmpty(selection) ?
              " AND (" + selection + ‘)‘ : "");
      default: break;
    }

    // To return the number of deleted items you must specify a where
    // clause. To delete all rows and return a value pass in "1".
    if (selection == null)
      selection = "1";

    // Perform the deletion.
    int deleteCount = db.delete(MySQLiteOpenHelper.DATABASE_TABLE,
      selection, selectionArgs);

    // Notify any observers of the change in the data set.
    getContext().getContentResolver().notifyChange(uri, null);

    // Return the number of deleted items.
    return deleteCount;
  }

  @Override
  public Uri insert(Uri uri, ContentValues values) {
    // Open a read / write database to support the transaction.
    SQLiteDatabase db = myOpenHelper.getWritableDatabase();

    // To add empty rows to your database by passing in an empty
    // Content Values object you must use the null column hack
    // parameter to specify the name of the column that can be
    // set to null.
    String nullColumnHack = null;

    // Insert the values into the table
    long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE,
        nullColumnHack, values);

    // Construct and return the URI of the newly inserted row.
    if (id > -1) {
      // Construct and return the URI of the newly inserted row.
      Uri insertedId = ContentUris.withAppendedId(CONTENT_URI, id);

      // Notify any observers of the change in the data set.
      getContext().getContentResolver().notifyChange(insertedId, null);

      return insertedId;
    }
    else
      return null;
  }

  @Override
  public int update(Uri uri, ContentValues values, String selection,
    String[] selectionArgs) {

    // Open a read / write database to support the transaction.
    SQLiteDatabase db = myOpenHelper.getWritableDatabase();

    // If this is a row URI, limit the deletion to the specified row.
    switch (uriMatcher.match(uri)) {
      case SINGLE_ROW :
        String rowID = uri.getPathSegments().get(1);
        selection = KEY_ID + "=" + rowID
            + (!TextUtils.isEmpty(selection) ?
              " AND (" + selection + ‘)‘ : "");
      default: break;
    }

    // Perform the update.
    int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TABLE,
      values, selection, selectionArgs);

    // Notify any observers of the change in the data set.
    getContext().getContentResolver().notifyChange(uri, null);

    return updateCount;
  }
}

在Content Provider中存储文件

应该在数据表中用一个完全限定的URI来标识存储在文件系统中某一位置的文件,而不是在Content Provider中存储大文件。

要想在表中支持文件,必须包含一个名为_data的列,它包含这条记录所表示的文件的路径。该列不应该被客户端应用程序所使用。当Content Resolver请求这条记录所关联的文件时,可以重写openFile方法来提供一个ParcelFileDescription(代表了该文件)。

对于Content Provider来说,它通常包含两个表:一个仅用于存储外部文件;另一个包含一个面向用户的列,该列包含指向文件表中行的URI引用。

public class MyHoardContentProvider extends ContentProvider {
  @Override
  public ParcelFileDescriptor openFile(Uri uri, String mode)
    throws FileNotFoundException {

    // Find the row ID and use it as a filename.
    String rowID = uri.getPathSegments().get(1);

    // Create a file object in the application‘s external
    // files directory.
    String picsDir = Environment.DIRECTORY_PICTURES;
    File file =
      new File(getContext().getExternalFilesDir(picsDir), rowID);

    // If the file doesn‘t exist, create it now.
    if (!file.exists()) {
      try {
        file.createNewFile();
      } catch (IOException e) {
        Log.d(TAG, "File creation failed: " + e.getMessage());
      }
    }

    // Translate the mode parameter to the corresponding Parcel File
    // Descriptor open mode.
    int fileMode = 0;
    if (mode.contains("w"))
      fileMode |= ParcelFileDescriptor.MODE_WRITE_ONLY;
    if (mode.contains("r"))
      fileMode |= ParcelFileDescriptor.MODE_READ_ONLY;
    if (mode.contains("+"))
      fileMode |= ParcelFileDescriptor.MODE_APPEND;     

    // Return a Parcel File Descriptor that represents the file.
    return ParcelFileDescriptor.open(file, fileMode);
  }
}

使用Content Provider

当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用ContentResolver 类来完成。

Content Resolver

ContentResolver 类提供了与ContentProvider类相同签名的四个方法:
public Uri insert(Uri uri, ContentValues values):该方法用于往ContentProvider添加数据。
public int delete(Uri uri, String selection, String[] selectionArgs):该方法用于从ContentProvider删除数据。
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs):该方法用于更新ContentProvider中的数据。
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder):该方法用于从ContentProvider中获取数据。

这些方法的第一个参数为Uri,代表要操作的ContentProvider和对其中的什么数据进行操作,
其实和contentprovider里面的方法是一样的.他们所对应的数据,最终是会被传到我们在之前程序里面定义的那个contentprovider类的方法,
假设给定的是:Uri.parse("content://com.bing.providers.personprovider/person/10"),那么将会对主机名为com.bing.providers.personprovider的ContentProvider进行操作,操作的数据为person表中id为10的记录。

每一个应用程序都有一个ContentResolver实例,可以使用getContentResolver方法来对其进行访问。

ContentResolver cr = getContentResolver();

当使用Content Provider公开数据时,Content Resolver是用来在这些Content Provider上进行查询和执行事务的对应类。

Content Provider提供了底层数据的抽象,而Content Resolver则提供了查询和处理Content Provider的抽象。

查询Content Provider

    // Get the Content Resolver.
    ContentResolver cr = getContentResolver();

    // Specify the result column projection. Return the minimum set
    // of columns required to satisfy your requirements.
    String[] result_columns = new String[] {
      MyHoardContentProvider.KEY_ID,
      MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN,
      MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN }; 

    // Specify the where clause that will limit your results.
    String where = MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN
                   + "=" + 1;

    // Replace these with valid SQL statements as necessary.
    String whereArgs[] = null;
    String order = null;

    // Return the specified rows.
    Cursor resultCursor = cr.query(MyHoardContentProvider.CONTENT_URI,
      result_columns, where, whereArgs, order);

可以使用ContentUris类中的withAppendedId静态方法来把一个行ID附加到内容URI后面来在Content Provider中查询特定行

  private Cursor getRow(long rowId) {
    /**
     * Listing 8-16: Querying a Content Provider for a particular row
     */
    // Get the Content Resolver.
    ContentResolver cr = getContentResolver();

    // Specify the result column projection. Return the minimum set
    // of columns required to satisfy your requirements.
    String[] result_columns = new String[] {
      MyHoardContentProvider.KEY_ID,
      MyHoardContentProvider.KEY_GOLD_HOARD_NAME_COLUMN,
      MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN }; 

    // Append a row ID to the URI to address a specific row.
    Uri rowAddress =
      ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI,
      rowId); 

    // These are null as we are requesting a single row.
    String where = null;
    String whereArgs[] = null;
    String order = null;

    // Return the specified rows.
    Cursor resultCursor = cr.query(rowAddress,
      result_columns, where, whereArgs, order);

    return resultCursor;
  }

如果ContentProvider的访问者需要知道ContentProvider中的数据发生变化,可以在ContentProvider发生数据变化时调用getContentResolver().notifyChange(uri, null)来通知注册在此URI上的访问者。

public class PersonContentProvider extends ContentProvider {
   public Uri insert(Uri uri, ContentValues values) {
      db.insert("person", "personid", values);
      getContext().getContentResolver().notifyChange(uri, null);
   }
}

如果ContentProvider的访问者需要得到数据变化通知,必须使用ContentObserver对数据(数据采用uri描述)进行监听,当监听到数据变化通知时,系统就会调用ContentObserver的onChange()方法

getContentResolver().registerContentObserver(Uri.parse("content://com.ljq.providers.personprovider/person"),
       true, new PersonObserver(new Handler()));
public class PersonObserver extends ContentObserver{
   public PersonObserver(Handler handler) {
      super(handler);
   }
   public void onChange(boolean selfChange) {
      //此处可以进行相应的业务处理
   }
}

使用Cursor Loader异步查询内容

Loader API:https://developer.android.com/reference/android/content/Loader.html

通过LoaderManager可以在每个Activity和Fragment中使用Loader。这些Loader被设计用来异步加载数据和监控底层数据源的变化

CursorLoader API:https://developer.android.com/reference/android/content/CursorLoader.html

CursorLoader允许你针对Content Provider执行异步查询并返回一个结果Cursor,而且对底层提供程序的任何更新都会发出通知。

使用CursorLoader

CursorLoader能够处理在Activity或者Fragment中使用Cursor所需的所有管理任务,所以实际上不建议再使用Activity的managedQuery和startManagingCursor方法。这包含管理Cursor的生命周期以确保在Activity终止时关闭Cursor。

CursorLoader同样会监控底层查询的改变,所以不再需要实现自己的ContentObserver。

实现CursorLoaderCallback

要使用CursorLoader,可以创建一个新的LoaderManager.LoaderCallbacks实现。

public class MyContentProvider extends ContentProvider {
    public static final Uri CONTENT_URI = Uri.parse("content://com.paad.skeletondatabaseprovider/elements");
}

public class DatabaseSkeletonActivity extends Activity {
  LoaderManager.LoaderCallbacks<Cursor> loaderCallback = new LoaderManager.LoaderCallbacks<Cursor>() {
    /**
     * Listing 8-18: Implementing Loader Callbacks
     */
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
      // Construct the new query in the form of a Cursor Loader. Use the id
      // parameter to contruct and return different loaders.
      String[] projection = null;
      String where = null;
      String[] whereArgs = null;
      String sortOrder = null;

      // Query URI
      Uri queryUri = MyContentProvider.CONTENT_URI;

      // Create the new Cursor loader.
      return new CursorLoader(DatabaseSkeletonActivity.this, queryUri,
        projection, where, whereArgs, sortOrder);
    }

    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
      // Replace the result Cursor displayed by the Cursor Adapter with
      // the new result set.
      adapter.swapCursor(cursor);

      // This handler is not synchonrized with the UI thread, so you
      // will need to synchronize it before modiyfing any UI elements
      // directly.
    }

    public void onLoaderReset(Loader<Cursor> loader) {
      // Remove the existing result Cursor from the List Adapter.
      adapter.swapCursor(null);

      // This handler is not synchonrized with the UI thread, so you
      // will need to synchronize it before modiyfing any UI elements
      // directly.
    }
  };
}

初始化和重新启动CursorLoader

每一个Activity和Fragment都提供了一个getLoaderManager方法,可以调用该方法来访问LoaderManager。

调用LoaderManager的initLoader方法,来初始化一个新的Loader,并传入LoaderCallback实现的引用、一个可选参数Bundle以及一个Loader标识符。

通常会在宿主Activity的onCreate(对于Fragment来说是onActivityCreated)方法中完成。

public class DatabaseSkeletonSearchActivity extends ListActivity
  implements LoaderManager.LoaderCallbacks<Cursor> {

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

      // Initiate the Cursor Loader
    getLoaderManager().initLoader(0, null, this);
  }

  public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    String query = "0";

    // Extract the search query from the arguments.
    if (args != null)
      query = args.getString(QUERY_EXTRA_KEY);

    // Construct the new query in the form of a Cursor Loader.
    String[] projection = {
        MyContentProvider.KEY_ID,
        MyContentProvider.KEY_COLUMN_1_NAME
    };
    String where = MyContentProvider.KEY_COLUMN_1_NAME
                   + " LIKE \"%" + query + "%\"";
    String[] whereArgs = null;
    String sortOrder = MyContentProvider.KEY_COLUMN_1_NAME +
                       " COLLATE LOCALIZED ASC";

    // Create the new Cursor loader.
    return new CursorLoader(this, MyContentProvider.CONTENT_URI,
      projection, where, whereArgs, sortOrder);
  }

  public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    // Replace the result Cursor displayed by the Cursor Adapter with
    // the new result set.
    adapter.swapCursor(cursor);
  }

  public void onLoaderReset(Loader<Cursor> loader) {
    // Remove the existing result Cursor from the List Adapter.
    adapter.swapCursor(null);
  }
}

创建Loader,重复调用initLoader方法将只会返回现有的Loader。如果想要抛弃旧的Loader并重新创建它,可以使用restartLoader方法。

    // Restart the Cursor Loader to execute the new query.
    getLoaderManager().restartLoader(0, args, this);

添加、删除和更新内容

Insert

    ContentValues newValues = new ContentValues();

    // Assign values for each row.
    newValues.put(MyHoardContentProvider.KEY_GOLD_HOARD_NAME_COLUMN,
                  hoardName);
    newValues.put(MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN,
                  hoardValue);
    newValues.put(MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN,
                  hoardAccessible);
    // [ ... Repeat for each column / value pair ... ]

    // Get the Content Resolver
    ContentResolver cr = getContentResolver();

    // Insert the row into your table
    Uri myRowUri = cr.insert(MyHoardContentProvider.CONTENT_URI,
                             newValues);

Update

    // Create the updated row content, assigning values for each row.
    ContentValues updatedValues = new ContentValues();
    updatedValues.put(MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN,
                      newHoardValue);
    // [ ... Repeat for each column to update ... ]

    // Create a URI addressing a specific row.
    Uri rowURI =
      ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI,
      hoardId);

    // Specify a specific row so no selection clause is required.
    String where = null;
    String whereArgs[] = null;

    // Get the Content Resolver.
    ContentResolver cr = getContentResolver();

    // Update the specified row.
    int updatedRowCount =
      cr.update(rowURI, updatedValues, where, whereArgs);

Delete

    // Specify a where clause that determines which row(s) to delete.
    // Specify where arguments as necessary.
    String where = MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN +
                   "=" + 0;
    String whereArgs[] = null;

    // Get the Content Resolver.
    ContentResolver cr = getContentResolver();

    // Delete the matching rows
    int deletedRowCount =
      cr.delete(MyHoardContentProvider.CONTENT_URI, where, whereArgs);


将搜索功能添加到应用程序中

使Content Provider可搜索

在启用搜索对话框或者在程序中使用搜索视图组件之前,需要定义哪些是可以搜索的内容。首先需要在项目的res/xml文件夹中建立一个新的可搜索元数据XML资源。必须制定android:lable属性,并且最好也包含一个android:hint属性,以帮助用户了解他们可以搜索的内容。

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
  android:label="@string/app_name"
  android:hint="@string/search_hint">
</searchable>

为应用程序创建一个搜索Activity

定义可以搜索的Content Provider之后,现在必须创建一个Activity来显示搜索结果。

通常情况下用户不希望把重复的查询添加到“返回栈”中,所以最好将搜索Activity设置为SingleTop模式,这样就会保证同一实例会被重复使用。

为了表明一个Activity可以用来提供搜索结果,需要在Activity中包含一个为android.intent.action.SEARCH操作和DEFAULT类别注册的Intent Filter。

    <activity android:name=".DatabaseSkeletonSearchActivity"
              android:label="Element Search"
              android:launchMode="singleTop">
      <intent-filter>
        <action android:name="android.intent.action.SEARCH" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
      <meta-data
        android:name="android.app.searchable"
        android:resource="@xml/searchable"
      />
    </activity>

要为一个给定的Activity启动搜索对话框,需要指定使用哪个搜索结果Activity来处理搜索请求。可以通过清单文件中向它的Activity节点中添加meta-data标记来实现这一点。把name属性设置为android.app.default_searchable,而且用value属性来指定搜索Activity。

<meta-data
      android:name="android.app.default_searchable"
      android:value=".DatabaseSkeletonSearchActivity"
    />

搜索会从搜索结果Activity内启动,并由该Activity自动处理,所以不需要特别地注解他。

用户启动搜索后,会启动Activity,并且在启动它的Intent中可以使用它们的搜索查询,这些查询可以通过SearchManager.QUERY额外方法来访问。从搜索结果Activity中启动的搜索会导致收到新的Intent,可以在onNewIntent处理程序中获取这些Intent并提取新的查询

public class DatabaseSkeletonSearchActivity extends ListActivity
  implements LoaderManager.LoaderCallbacks<Cursor> {

  private static String QUERY_EXTRA_KEY = "QUERY_EXTRA_KEY";

  private SimpleCursorAdapter adapter;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Get the launch Intent
    parseIntent(getIntent());
  }

  @Override
  protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    parseIntent(getIntent());
  }

  private void parseIntent(Intent intent) {
    // If the Activity was started to service a Search request,
    // extract the search query.
    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
      String searchQuery = intent.getStringExtra(SearchManager.QUERY);
      // Perform the search
      performSearch(searchQuery);
    }
  }
}

将搜索Activity设置为应用程序的默认搜索Provider

通常最好是在整个应用程序中使用相同的搜索结果形式。设置一个Activity为默认的搜索结果提供程序,为应用程序内的所有Activity提供搜索功能。实现这个设置需要在application清单节点上添加一个<meta-data>标记。将name属性设置为android.app.default_searchable并使用value属性制定搜索Activity。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.paad.DatabaseSkeleton"
  android:versionCode="1"
  android:versionName="1.0" >

  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name" >
    <activity
      android:label="@string/app_name"
      android:name=".DatabaseSkeletonActivity" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>

    <activity android:name=".DatabaseSkeletonSearchActivity"
              android:label="Element Search"
              android:launchMode="singleTop">
      <intent-filter>
        <action android:name="android.intent.action.SEARCH" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
      <meta-data
        android:name="android.app.searchable"
        android:resource="@xml/searchable"
      />
    </activity>

    <meta-data
      android:name="android.app.default_searchable"
      android:value=".DatabaseSkeletonSearchActivity"
    />
  </application>
</manifest>

执行搜索并显示结果(以搜索一个ContentProvider为例)

public class DatabaseSkeletonSearchActivity extends ListActivity
  implements LoaderManager.LoaderCallbacks<Cursor> {

  private static String QUERY_EXTRA_KEY = "QUERY_EXTRA_KEY";

  private SimpleCursorAdapter adapter;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Create a new adapter and bind it to the List View
    adapter = new SimpleCursorAdapter(this,
            android.R.layout.simple_list_item_1, null,
            new String[] { MyContentProvider.KEY_COLUMN_1_NAME },
            new int[] { android.R.id.text1 }, 0);
    setListAdapter(adapter);

    // Get the launch Intent
    parseIntent(getIntent());
  }

  @Override
  protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    parseIntent(getIntent());
  }

  private void parseIntent(Intent intent) {
    // If the Activity was started to service a Search request,
    // extract the search query.
    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
      String searchQuery = intent.getStringExtra(SearchManager.QUERY);
      // Perform the search
      performSearch(searchQuery);
    }
  }

  // Execute the search.
  private void performSearch(String query) {
    // Pass the search query as an argument to the Cursor Loader
    Bundle args = new Bundle();
    args.putString(QUERY_EXTRA_KEY, query);

    // Restart the Cursor Loader to execute the new query.
    getLoaderManager().restartLoader(0, args, this);
  }

  public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    String query = "0";

    // Extract the search query from the arguments.
    if (args != null)
      query = args.getString(QUERY_EXTRA_KEY);

    // Construct the new query in the form of a Cursor Loader.
    String[] projection = {
        MyContentProvider.KEY_ID,
        MyContentProvider.KEY_COLUMN_1_NAME
    };
    String where = MyContentProvider.KEY_COLUMN_1_NAME
                   + " LIKE \"%" + query + "%\"";
    String[] whereArgs = null;
    String sortOrder = MyContentProvider.KEY_COLUMN_1_NAME +
                       " COLLATE LOCALIZED ASC";

    // Create the new Cursor loader.
    return new CursorLoader(this, MyContentProvider.CONTENT_URI,
      projection, where, whereArgs, sortOrder);
  }

  public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    // Replace the result Cursor displayed by the Cursor Adapter with
    // the new result set.
    adapter.swapCursor(cursor);
  }

  public void onLoaderReset(Loader<Cursor> loader) {
    // Remove the existing result Cursor from the List Adapter.
    adapter.swapCursor(null);
  }
}

大多数情况下,除了简单显示搜索结果外,还需要提供一些功能。如果使用ListActivity或ListFragment,可以重写onListItemClick处理程序来响应用户选择搜索结果。例如显示用户选择的结果的详细信息。

  @Override
  protected void onListItemClick(ListView listView, View view, int position, long id) {
    super.onListItemClick(listView, view, position, id);

    // Create a URI to the selected item.
    Uri selectedUri =
      ContentUris.withAppendedId(MyContentProvider.CONTENT_URI, id);

    // Create an Intent to view the selected item.
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setData(selectedUri);

    // Start an Activity to view the selected item.
    startActivity(intent);
  }

搜索视图组件

将搜索视图链接到搜索Activity,首先使用SearchManager的getSearchableInfo方法提取SearchableInfo的一个引用。接着使用搜索视图的setSearchableInfo方法把这个对象绑定到你的搜索视图上。

     // Use the Search Manager to find the SearchableInfo related
     // to this Activity.
     SearchManager searchManager =
       (SearchManager)getSystemService(Context.SEARCH_SERVICE);
     SearchableInfo searchableInfo =
       searchManager.getSearchableInfo(getComponentName());

     // Bind the Activity‘s SearchableInfo to the Search View
     SearchView searchView = (SearchView)findViewById(R.id.searchView);
     searchView.setSearchableInfo(searchableInfo);

当搜索视图和搜索Activity连接之后,搜索视图就会想搜索栏一样工作,提供搜索建议并在输入查询后显示搜索Activity。

默认情况下搜索视图会显示为一个图标,当单机该图标时,它就会展开为一个搜索编辑框。可以使用搜索视图的setIconifiedByDefalut方法禁用这个默认设置,让它一直以编辑框的形式显示。

searchView.setIconifiedByDefault(false);

默认情况下,当用户按下Enter键时,搜索视图查询就会被启动。可以使用setSubmitButtonEnable方法来显示一个提交搜索的按钮。

searchView.setSubmitButtonEnable(true);

由ContentProvider支持搜索建议

当用户输入他们的查询时,搜索建议在搜索栏/搜索视图组件下以一个简单的列表显示可能的搜索结果,这就允许用户绕开搜索结果Activity,而直接调到搜索结果。

尽管搜索Activity可以构建它的查询,并且可以按照任何方式显示结果Cursor数据,但是如果想要提供搜索建议,那么需要创建一个ContentProvider来接受搜索查询并用预期的投影返回建议。

为了支持搜索建议,需要配置ContentProvider,使其能够识别作为搜索查询的特殊URI路径。

  private static final int ALLROWS = 1;
  private static final int SINGLE_ROW = 2;
  private static final int SEARCH = 3;

  private static final UriMatcher uriMatcher;
  static {
   uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
   uriMatcher.addURI("com.paad.skeletondatabaseprovider",
                     "elements", ALLROWS);
   uriMatcher.addURI("com.paad.skeletondatabaseprovider",
                     "elements/#", SINGLE_ROW);

   uriMatcher.addURI("com.paad.skeletondatabaseprovider",
     SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH);
   uriMatcher.addURI("com.paad.skeletondatabaseprovider",
     SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH);
   uriMatcher.addURI("com.paad.skeletondatabaseprovider",
     SearchManager.SUGGEST_URI_PATH_SHORTCUT, SEARCH);
   uriMatcher.addURI("com.paad.skeletondatabaseprovider",
     SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SEARCH);
  }

在ContentProvider中使用UriMatcher来为搜索查询返回搜索建议的MIME类型。

  @Override
  public String getType(Uri uri) {
    // Return a string that identifies the MIME type
    // for a Content Provider URI
    switch (uriMatcher.match(uri)) {
      case ALLROWS:
        return "vnd.android.cursor.dir/vnd.paad.elemental";
      case SINGLE_ROW:
        return "vnd.android.cursor.item/vnd.paad.elemental";
      case SEARCH :
        return SearchManager.SUGGEST_MIME_TYPE;      default:
        throw new IllegalArgumentException("Unsupported URI: " + uri);
    }
  }

SearchManager通过在ContentProvider中启动一个查询并把查询值作为URI路径中的最后一部分传入来请求搜索建议。为了提供搜索建议,必须用一组预定义列来返回一个Cursor。

目前有两个必需的列,SUGGEST_COLUMN_TEXT_1用于显示搜索结果的文本,而_id用于指示唯一的行ID。可以提供两个包含文本的列,而一个图标可以显示在文本结果的左边或右边。

包含一个SUGGEST_COLUMN_INTENT_DATA_ID列也是有用的。在此列中返回的值可以附加到指定的URI路径和用来填充一个Intent,如果搜索建议被用户选中,则Intent就会激活。

由于速度对于实时搜索结果来说是至关重要的,因此在许多情况下,最好专门创建一个单独的表来存储和提供搜索结果。

  public static final String KEY_SEARCH_COLUMN = KEY_COLUMN_1_NAME;

  private static final HashMap<String, String> SEARCH_SUGGEST_PROJECTION_MAP;
  static {
    SEARCH_SUGGEST_PROJECTION_MAP = new HashMap<String, String>();
    SEARCH_SUGGEST_PROJECTION_MAP.put(
      "_id", KEY_ID + " AS " + "_id");
    SEARCH_SUGGEST_PROJECTION_MAP.put(
      SearchManager.SUGGEST_COLUMN_TEXT_1,
      KEY_SEARCH_COLUMN + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
    SEARCH_SUGGEST_PROJECTION_MAP.put(
      SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, KEY_ID +
      " AS " + "_id");
  }

要执行这个查询,该查询提供搜索建议,则该查询的实现中使用UriMatcher,并且使用应用程序清单中定义的投影

  @Override
  public Cursor query(Uri uri, String[] projection, String selection,
      String[] selectionArgs, String sortOrder) {
    // Open a read-only database.
    SQLiteDatabase db = myOpenHelper.getWritableDatabase();

    // Replace these with valid SQL statements if necessary.
    String groupBy = null;
    String having = null;

    SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
    queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TABLE);

    // If this is a row query, limit the result set to the passed in row.
    switch (uriMatcher.match(uri)) {
      case SINGLE_ROW :
        String rowID = uri.getPathSegments().get(1);
        queryBuilder.appendWhere(KEY_ID + "=" + rowID);
        break;
      case SEARCH :
        String query = uri.getPathSegments().get(1);
        queryBuilder.appendWhere(KEY_SEARCH_COLUMN +
          " LIKE \"%" + query + "%\"");
        queryBuilder.setProjectionMap(SEARCH_SUGGEST_PROJECTION_MAP);
        break;
      default: break;
    }

    Cursor cursor = queryBuilder.query(db, projection, selection,
        selectionArgs, groupBy, having, sortOrder);

    return cursor;
  }

最后一步是更新可搜索资源,为用来给搜索栏和/或搜索视图提供搜索建议的ContentProvider制定授权。这可能是用来执行查询的同一个ContentProvider或者是一个完全不同的提供程序。

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
  android:label="@string/app_name"
  android:searchSuggestAuthority=
    "com.paad.skeletonsearchabledatabaseprovider"
  android:searchSuggestIntentAction="android.intent.action.VIEW"
  android:searchSuggestIntentData=
    "content://com.paad.skeletonsearchabledatabaseprovider/elements">
</searchable>

searchSuggestIntentAction用来指定搜索建议被但即使要执行的操作。

searchSuggestIntentData用来指定在操作Intent的数据值中使用的基础URI。如果在搜素建议的结果的Cursor中包含一个Intent数据ID列,那么它会被附加到这个基础URI上。



本地Android Content Provider

  • MediaStore 对设备上的多媒体信息,包括音频、视频和图像,提供了集中的、托管的访问。
  • Browser 读取或修改浏览器和浏览器搜索历史记录。
  • ContactsContract 检索、修改或者存储联系人的详细信息以及相关的社交流更新。
  • Calendar 创建新事件,删除或更新现有的日历项。这包括修改参与者列表和设置提醒。
  • CallLog 查看或者更新通话记录,既包括来电和去电,也包括未接电话和通话的细节,如呼叫者ID和通话持续时间。

使用MediaStore ContentProvider

任何时候向文件系统添加新的多媒体文件时,还应该使用内容扫描器把它添加到MediaStore中。这样,该文件就可以被其他应用程序使用。大多数情况下,没有必要直接修改MediaStore ContentProvider的内容。

以下示例显示了用来为存储在外部卷上的每个音频找出歌曲名称和专辑名称的简单代码片段。

  private String[] getSongListing() {
    // Get a Cursor over every piece of audio on the external volume,
    // extracting the song title and album name.
    String[] projection = new String[] {
      MediaStore.Audio.AudioColumns.ALBUM,
      MediaStore.Audio.AudioColumns.TITLE
    };

    Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;

    Cursor cursor =
      getContentResolver().query(contentUri, projection,
                                 null, null, null); 

    // Get the index of the columns we need.
    int albumIdx =
      cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM);
    int titleIdx =
      cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE);

    // Create an array to store the result set.
    String[] result = new String[cursor.getCount()];

    // Iterate over the Cursor, extracting each album name and song title.
    while (cursor.moveToNext()) {
      // Extract the song title.
      String title = cursor.getString(titleIdx);
      // Extract the album name.
      String album = cursor.getString(albumIdx);

      result[cursor.getPosition()] = title + " (" + album + ")";
    } 

    // Close the Cursor.
    cursor.close();

    //
    return result;
  }

使用Contacts Contract Content Provider

Android向所有被赋予READ_CONTACTS权限的应用程序提供联系人信息数据库的完全访问权限。

Contacts Contract Content Provider并没有提供一个具有良好定义的来想你人详情列的表,而是使用一个三层数据模型来存储数据,将数据与联系人关联起来,并把同一个人的数据聚集起来,这是通过下面的ContactsContract子类实现的

  • A row in the ContactsContract.Data table can store any kind of personal data, such as a phone number or email addresses. The set of data kinds that can be stored in this table is open-ended. There is a predefined set of common kinds, but any application can add its own data kinds.
  • A row in the ContactsContract.RawContacts table represents a set of data describing a person and associated with a single account (for example, one of the user‘s Gmail accounts).
  • A row in the ContactsContract.Contacts table represents an aggregate of one or more RawContacts presumably describing the same person. When data in or associated with the RawContacts table is changed, the affected aggregate contacts are updated as necessary

通常会使用Data表来添加、删除或修改为已有的联系人账户存储的数据,使用RawContacts表来创建和管理账户,使用Contacts来查询数据库已提取联系人的详情。

读取联系人详情

首先需要在应用程序的清单文件中包含READ_CONTACTS的权限配置

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.paad.contentproviders"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="14" />

    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".Ch08_ContentProvidersActivity" >
        </activity>
    </application>

</manifest>

使用ContentResolver和CONTENT_URI静态常量可以查询上面描述的3种Contacts Contract Content Provider。每个类的静态属性的形式包含他们的列名。

如下示例在Contacts表中查询通讯簿中每个人的Cursor,并创建一个字符串数组来保存每个联系人的姓名和唯一ID。

  private String[] getNames() {
    // Create a projection that limits the result Cursor
    // to the required columns.
    String[] projection = {
      ContactsContract.Contacts._ID,
      ContactsContract.Contacts.DISPLAY_NAME
    };

   // Get a Cursor over the Contacts Provider.
    Cursor cursor =
      getContentResolver().query(ContactsContract.Contacts.CONTENT_URI,
                                 projection, null, null, null);

    // Get the index of the columns.
    int nameIdx =
      cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME);
    int idIdx =
      cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID);

    // Initialize the result set.
    String[] result = new String[cursor.getCount()];

    // Iterate over the result Cursor.
    while(cursor.moveToNext()) {
      // Extract the name.
      String name = cursor.getString(nameIdx);
      // Extract the unique ID.
      String id = cursor.getString(idIdx);

      result[cursor.getPosition()] = name + " (" + id + ")";
    } 

    // Close the Cursor.
    cursor.close();

    //
    return result;
  }

ContactsContract.DataContentProvider用来存储所有的联系人详情,如地址、电话号码和电子邮件地址。大多数情况下,很可能需要根据一个完整的或者部分的联系人姓名来查询联系人详情。

为了简化这种操作,Android提供了ContracsContract.Contacts.CONTENT_FILTER_URI查询URI。可以将完整的或者部分的名称作为URI的额外路径片段附加到查询上。要提取相关的联系人详情,需要从返回的Cursor中找到_ID值,并使用它创建一个对Data表的查询。

Data表中的某一行的每一列的内容取决于为该行制定的MIME类型。因此,任何对Data表的查询必须按MIME类型对行进行过滤,以便提取有意义的数据。

以下示例显示了如何使用CommonDataKinds子类中的联系人详情列名,从Data表中为特定的联系人提取显示名称和手机号码。

  private String[] getNameAndNumber() {
    ContentResolver cr = getContentResolver();
    String[] result = null;

    // Find a contact using a partial name match
    String searchName = "andy";
    Uri lookupUri =
      Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI,
                           searchName);

    // Create a projection of the required column names.
    String[] projection = new String[] {
      ContactsContract.Contacts._ID
    };

    // Get a Cursor that will return the ID(s) of the matched name.
    Cursor idCursor = cr.query(lookupUri,
      projection, null, null, null);

    // Extract the first matching ID if it exists.
    String id = null;
    if (idCursor.moveToFirst()) {
      int idIdx =
        idCursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID);
      id = idCursor.getString(idIdx);
    }

    // Close that Cursor.
    idCursor.close();

    // Create a new Cursor searching for the data associated with the returned Contact ID.
    if (id != null) {
      // Return all the PHONE data for the contact.
      String where = ContactsContract.Data.CONTACT_ID +
        " = " + id + " AND " +
        ContactsContract.Data.MIMETYPE + " = ‘" +
        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE +
        "‘";

      projection = new String[] {
        ContactsContract.Data.DISPLAY_NAME,
        ContactsContract.CommonDataKinds.Phone.NUMBER
      };

      Cursor dataCursor =
        getContentResolver().query(ContactsContract.Data.CONTENT_URI,
          projection, where, null, null);

      // Get the indexes of the required columns.
      int nameIdx =
        dataCursor.getColumnIndexOrThrow(ContactsContract.Data.DISPLAY_NAME);
      int phoneIdx =
        dataCursor.getColumnIndexOrThrow(
          ContactsContract.CommonDataKinds.Phone.NUMBER);

      result = new String[dataCursor.getCount()];

      while(dataCursor.moveToNext()) {
        // Extract the name.
        String name = dataCursor.getString(nameIdx);
        // Extract the phone number.
        String number = dataCursor.getString(phoneIdx);

        result[dataCursor.getPosition()] = name + " (" + number + ")";
      }

      dataCursor.close();
    }

    return result;
  }

Contacts子类还提供了电话号码查找URI,用来帮助找到与特定电话号码相关联的联系人。这个查询经过了高度优化,可以为传入的呼叫者ID通知快速返回结果。

可以使用ContractsContract.PhoneLookup.CONTENT_FILTER_URI,并将要查找的电话号码作为一个额外的路径片段附加到它的后面。

  private String performCallerId() {
    String incomingNumber = "(650)253-0000";
    String result = "Not Found";

    Uri lookupUri =
      Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
                           incomingNumber);

    String[] projection = new String[] {
      ContactsContract.Contacts.DISPLAY_NAME
    };

    Cursor cursor = getContentResolver().query(lookupUri,
      projection, null, null, null);

    if (cursor.moveToFirst()) {
      int nameIdx =
        cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME);

      result = cursor.getString(nameIdx);
    }

    cursor.close();

    return result;
  }

使用Intent创建和选择联系人

Contacts Contract Content Provider包含一个基于Intent的机制,允许使用现有的联系人应用程序查看、插入或选择一个联系人。

为了显示一个联系人列表供用户选择,可以结合使用Intent.ACTION_PICK和ContactsContract.Contacts.CONTENT_URI。

  private static int PICK_CONTACT = 0;

  private void pickContact() {
    Intent intent = new Intent(Intent.ACTION_PICK,
                               ContactsContract.Contacts.CONTENT_URI);
    startActivityForResult(intent, PICK_CONTACT);
  }

当用户选择一个联系人时,它将作为返回Intent的data属性内的URI返回

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if ((requestCode == PICK_CONTACT) && (resultCode == RESULT_OK)) {
      resultTextView.setText(data.getData().toString());
    }
  }

创建联系人

  private void insertContactWithIntent() {
    Intent intent =
      new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT,
                 ContactsContract.Contacts.CONTENT_URI);
    intent.setData(Uri.parse("tel:(650)253-0000"));

    intent.putExtra(ContactsContract.Intents.Insert.COMPANY, "Google");
    intent.putExtra(ContactsContract.Intents.Insert.POSTAL,
      "1600 Amphitheatre Parkway, Mountain View, California");

    startActivity(intent);

  }

修改联系人详情,需要有WRITE_CONTRACTS权限。

使用Calendar Content Provider

查询日历

权限:android.permission.READ_CALENDAR

使用ContentResolver,通过它们的CONTENT_URI常量来查询任何Calendar Provider表。

  • Calendars Calendar应用程序可以显示多个日历,这些日历关联多个账户。该表存储每个可显示的日历,以及日历的详情,如日历的显示名称/时区和颜色。
  • Events Event表为每个调度的日历时间包含一项,内容包括名称、描述、地点和开始/结束时间。
  • Instances 每个时间有一个或多个实例(在事件重复发生的情况下)。Instances表有Events表的内容所声称的项来填充,它还包含一个生成它的事件的引用。
  • Attendees Attendees表中的每一项表示一个给定事件的单个参与者。每个参与者可以包含姓名、电子邮件地址和出席状态,以及他们是否是可选的或需要的来宾。
  • Reminders Reminders表描述了事件提醒,每一行代表一个特定事件的提醒。

以下示例展示了查询Events表中的每一个事件,并创建一个字符串数组来保存每一个时间的名称和唯一ID。

  private String[] queryCalendar() {

    //Create a projection that limits the result Cursor
    //to the required columns.
    String[] projection = {
       CalendarContract.Events._ID,
       CalendarContract.Events.TITLE
    };

    //Get a Cursor over the Events Provider.
    Cursor cursor =
     getContentResolver().query(CalendarContract.Events.CONTENT_URI,
                                projection, null, null, null);

    //Get the index of the columns.
    int nameIdx =
    cursor.getColumnIndexOrThrow(CalendarContract.Events.TITLE);
    int idIdx = cursor. getColumnIndexOrThrow(CalendarContract.Events._ID);

    //Initialize the result set.
    String[] result = new String[cursor.getCount()];

    //Iterate over the result Cursor.
    while(cursor.moveToNext()) {
      // Extract the name.
      String name = cursor.getString(nameIdx);
      // Extract the unique ID.
      String id = cursor.getString(idIdx);

      result[cursor.getPosition()] = name + " (" + id + ")";
    } 

    //Close the Cursor.
    cursor.close();

    //
    return result;
  }

使用Intent创建和编辑日历项

创建新的日历项

使用Intent.ACTION_INSERT操作,并指定CalendarContract.Events.CONTENT_URI作为URI,可以不需要任何特殊权限就在现有的日历上添加新事件。

  private void insertNewEventIntoCalendar() {

    // Create a new insertion Intent.
    Intent intent = new Intent(Intent.ACTION_INSERT, CalendarContract.Events.CONTENT_URI);

    // Add the calendar event details
    intent.putExtra(CalendarContract.Events.TITLE, "Launch!");
    intent.putExtra(CalendarContract.Events.DESCRIPTION,
                    "Professional Android 4 " +
                    "Application Development release!");
    intent.putExtra(CalendarContract.Events.EVENT_LOCATION, "Wrox.com");

    Calendar startTime = Calendar.getInstance();
    startTime.set(2012, 2, 13, 0, 30);
    intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startTime.getTimeInMillis());

    intent.putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, true);    

     // Use the Calendar app to add the new event.
    startActivity(intent);
  }

编辑日历事件

Intent机制只对编辑事件的开始和结束时间提供支持。

  private void editCalendarEvent() {

    // Create a URI addressing a specific event by its row ID.
    // Use it to  create a new edit Intent.
    long rowID = 760;
    Uri uri = ContentUris.withAppendedId(
      CalendarContract.Events.CONTENT_URI, rowID);

    Intent intent = new Intent(Intent.ACTION_EDIT, uri);

    // Modify the calendar event details
    Calendar startTime = Calendar.getInstance();
    startTime.set(2012, 2, 13, 0, 30);
    intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startTime.getTimeInMillis());

    intent.putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, true);    

    // Use the Calendar app to edit the event.
    startActivity(intent);
  }

显示日历和日历事件

使用Intent.ACTION_VIEW来显示制定日期和时间的日历事件。

  private void displayCalendarEvent() {

    // Create a URI that specifies a particular time to view.
    Calendar startTime = Calendar.getInstance();
    startTime.set(2012, 2, 13, 0, 30);

    Uri uri = Uri.parse("content://com.android.calendar/time/" +
      String.valueOf(startTime.getTimeInMillis()));
    Intent intent = new Intent(Intent.ACTION_VIEW, uri);

    // Use the Calendar app to view the time.
    startActivity(intent);
  }
时间: 2024-10-26 07:42:48

Android开发- 数据库和Content Provider的相关文章

Android开发-API指南-Content Provider

Content Providers 英文原文:http://developer.android.com/guide/topics/providers/content-providers.html 采集日期:2015-01-07 文章目录 Content Provider 基础 创建 Content Provider Calendar Provider Contact Provider 相关示例 Contact Manager 应用 “游标(联系人)” “游标(电话)” Sync Adapter

Android开发-API指南-Content Provider基础

Content Provider Basics 英文原文:http://developer.android.com/guide/topics/providers/content-provider-basics.html 采集日期:2015-01-07 在本文中 概述 访问 Provider Content URI 从 Content Provider 读取数据 申请读取权限 建立查询请求 显示查询结果 从查询结果中获取数据 Content Provider 相关权限 插入.修改.删除数据 插入数

Android应用程序组件Content Provider应用实例

文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6950440 文简要介绍了Android应用程序组件Content Provider在应用程序间共享数据的原理,但是没有进一步研究它的实现.本文将实现两个应用程序,其中一个以Content Provider的形式来提供数据访问入口,另一个通过这个Content Provider来访问这些数据.本文的例子不仅可以为下文分析Content Provi

Android应用程序组件Content Provider简要介绍和学习计划

文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6946067 在Android系统中,Content Provider作为应用程序四大组件之一,它起到在应用程序之间共享数据的作用,同时,它还是标准的数据访问接口.前面的一系列文章已经分析过 Android应用程序的其它三大组件(Activity.Service和Broadcast Receiver)了,本文将简要介绍Content Provid

数据库和Content Provider

SQLite提供了强大的SQL数据库的库文件,从而使应用程序拥有一个具备完全控制权的健壮的持久化层. Content Provider实现在应用程序内和应用程序之间存储.共享和使用结构化数据.通过将数据存储层和应用层分离,Content Provider为各种数据源提供了一个通用的接口(用来发布数据). Android通过结合使用SQLite数据库和Content Provider,提供了结构化数据的持久化功能.每个应用程序都可以创建自己的数据库,并对真个数据库拥有完全的控制权.创建了底层数据存

Android应用程序组件Content Provider的共享数据更新通知机制分析

在Android系统中,应用程序组件Content Provider为不同的应用程序实现数据共享提供了基础设施,它主要通过Binder进程间通信机制和匿名共享内存机制来实现的.关于数据共享的另一个 话题便是数据更新通知机制了,即如果一个应用程序对共享数据做了修改,它应该如何通知其它正在使用这些共享数据的应用程序呢?本文将分析Content Provider的共享数据更新通知机制,为读者解答这个问题. Android应用程序组件Content Provider中的数据更新通知机制和Android系

Android开发-API指南- Calendar Provider

Calendar Provider 英文原文:http://developer.android.com/guide/topics/providers/calendar-provider.html 采集日期:2015-05-10 在本文中 基础知识 用户权限 日程表 查询日程 修改日程 插入日程 events 表 添加 events 数据 更新 events 数据 删除 events 数据 attendees 表 添加 attendees 数据 reminders 表 添加 reminders 数

Android应用程序组件Content Provider的启动过程源代码分析

文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6963418 通过前面的学习,我们知道在Android系统中,Content Provider可以为不同的应用程序访问相同的数据提供统一的入口.Content Provider一般是运行在独立的进程中的,每一个Content Provider在系统中只有一个实例存在,其它应用程序首先要找到这个实例,然后才能访问它的数据.那么,系统中的Conten

Android应用程序组件Content Provider在应用程序之间共享数据的原理分析

文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6967204 在Android系统中,不同的应用程序是不能直接读写对方的数据文件的,如果它们想共享数据的话,只能通过 Content Provider组件来实现.那么,Content Provider组件又是如何突破应用程序边界权限控制来实现在不同的应用程序之间共享数据的呢?在前面的文章中,我们已经简要介绍过它是通过 Binder进程间通信机制以