我的Android 4 学习系列之数据库和Content Provider

目录
  • 创建数据库和使用SQLite
  • 使用Content Provider、Cusor和Content Value来存储、共享和使用应用程序数据
  • 使用Cursor Loader异步查询Content Provider
  • 在应用程序中添加搜索功能
  • 使用原生的Media Store、Contact和Calander Content Provider
创建数据库和使用SQLite

Android 通过结合使用SQLite数据库和ContentProvider,提供了结构化数据的持久化功能。

SQLite数据库存储在设备(或模拟器)上的/data/data/<package_name>/databases文件夹中。所有数据库都是私有的,只能被创建她的应用程序访问。

ContentProvider提供了一种基于使用content://模式的简单URI寻址模型来发布和使用数据的接口。它们允许将应用层从底层数据层中分离,通过抽象底层数据源使用应用程序不必依赖于某个数据源。可以通过查询ContentProvider来获取结果,更新或者删除Content Provider 中已有的记录,以及在其中添加新的记录。任何具有合适权限的应用程序都可以添加、删除或者更新其他任意应用程序的数据,包括本地Android Contnet Provider中的数据。

SQLite是一种流行的关系数据库管理系统(Relational Database Management System,RDBMS)。SQLite具有一下特征:(1)开源 (2)符合标准(3)轻量级(4)单一层(single-tier)。

SQLite被实现为一种简洁的C语言库,并且是Android软件栈中的一部分。

轻量且强大是SQLite与其他很多传统的数据库引擎的不同之处,它在列中使用了一种松散类型的方法,即并不要求一列中所有值都是同一种类型;这样分配和提取值时候就不需要进行严格的类型检查了。

1. Content Value 和 Cursor

Content Value 用来向数据库表中插入新的一行。每一个ContentValues对象都将一个表的行表示为列名值的映射。

数据库查询作为Cursor对象返回。Cursor是底层数据中结果集的指针,它没有提取和返回结果的副本。Cursor为控制在数据库中查询的结果集中的位置(行)提供了一种易于管理的方式。moveToFirst,moveToNext,moveToPrevious,getCount,getColumnIndexOrThrow,getColumnName,getColumnNames,moveToPosition,getPosition。

SQLiteOpenHelper:

SQLiteOpenHelper是一个抽象类,用来实现创建、打开、升级数据库的最佳模式。通过实现SQLiteOpenHelper,可以隐藏那些用于决定一个数据库打开之前是否需要创建或者升级的逻辑。

  1: private static class HoardDBOpenHelper extends SQLiteOpenHelper {
  2:   private static final String DATABASE_NAME = "myDatabase.db";
  3:   private static final String DATABASE_TABLE = "GoldHoards";
  4:   private static final int DATABASE_VERSION = 1;
  5:   
  6:   // 创建新数据库的SQL语句
  7:   private static final String DATABASE_CREATE = "create table" + DATABASE_TABLE 
  8:     + "(" +  KEY_ID + " integer primary key autoincrement, "
  9:     + KEY_GOLD_HOARD_NAME_COLUMN + " text not null,"
 10:     + KEY_GOLD_HOADED_COLUMN + " float, "
 11:     + KEY_GOLD_HOAD_ACCESSIBLE_COLUMN + " integer);";
 12:     
 13:   public HoardDBOpenHelper(Context context, String name, CursorFactory factory, int version){
 14:     super(context, name, factory, version);
 15:   }
 16:   
 17:   // 当磁盘不存在数据库,辅助类需要创建一个新数据库时调用
 18:   @Override
 19:   public void onCreate(SQLiteDatabase db){
 20:     db.execSQL(DATABASE_CREATE);
 21:   }
 22:   
 23:   // 当数据库版本不一致,磁盘上的数据库版本需要升级到当前版本时调用
 24:   @Override
 25:   public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
 26:     // 记录版本升级
 27:     Log.w("TaskDBAdapter", 
 28:       "Upgrading from version " + oldVersion + "to " + newVersion + ", which will destroy all old data");
 29:       
 30:     // 当数据库升级到现在的版本。通过比较oldVersion 和 newVersion 的值,
 31:     // 可以处理多个旧版本的值
 32:     
 33:     // 最简单的情况是删除旧表,创建新表
 34:     db.execSQL("DROP TABLE IF EXISTS " + DATABASE_NAME);
 35:     
 36:     // 创建新表
 37:     onCreate(db);
 38:   }
 39: }

 

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

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

创建数据库后,必须处理原来SQLiteOpenHelper和onCreate和onUpgrade处理程序中处理的创建和升级逻辑--通常,这要使用数据库的execSQL方法来根据需要创建和删除表。

Android数据库设计注意事项

当专门为设计Android设计数据库时,需要考虑一下两点:

  • 文件(位图或者音频文件)通常不存储在数据库的表中的。
  • 虽然这不是一个严格的要求,但也仍然应该包含一个自增的主键字段,作为每一行的唯一字段索引字段。如果计划使用Content Provider来共享表,就必须具有唯一的ID字段。

查询数据库

  1: String[] result_columns = new String[]{
  2:   KEY_ID, KEY_GOLD_HOARD_ACCESSIBLE_COLUMN, KEY_GOLD_HOARDED_COLUMN };
  3:   
  4: // 指定用于限制结果的where子句
  5: String where = KEY_GOLD_HOARD_ACCESSIBLE_COLUMN + "=" + 1;
  6: 
  7: // 根据需要把以下语句替换为有效的SQL语句
  8: String whereArgs[] = null;
  9: String groupBy = null;
 10: String having = null;
 11: String order = null;
 12: 
 13: SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
 14: Cursor cursor = db.query(HoardDBOpenHelper.DATABASE_TABLE,
 15:           result_columns,
 16:           where,
 17:           whereArgs,
 18:           groupBy,
 19:           having,
 20:           order);

每次需要对数据库执行一个查询或者事务时,请求数据库实例是一个很好的的做法。出于效率考虑,只有当确信不再需要数据库实例时---通常是使用他的Activity或者Service时---才关闭它。

从Cursor中提取值

  1: int columIndex = cursor.getColumnIndex(KEY_COLUMN_1_NAME);
  2: if(columnIndex > -1){
  3:   String columnValue = cursor.getString(columnIndex);
  4:   // 对列值执行一些操作
  5: }
  6: else{
  7:   // 对列不存在时执行的操作
  8: }

数据库实现应该发布一些提供了列名称的静态常量。这些静态常量通常在数据库的合同类或者ContentProvider内公开。

遍历一个结果的Cursor,以提取一个列中的浮点值并计算其平均值:

  1: float totalHoard = 0f;
  2: float averageHoard = 0f;
  3: 
  4: //指出所用列的索引
  5: int GOLD_HOARDED_COLUMN_INDEX = cursor.getColumnIndexOrThrow(KEY_GOLD_HOARDED_COLUMN);
  6: 
  7: //遍历游标行
  8: //Cursor类被初始化为第一个元素的前一个位置
  9: //所以我们只能检查是否有”下一行“
 10: //如果Cursor为空,则返回false
 11: while(cursor.moveToNext()){
 12:   float hoard = cursor.getFloat(GOLD_HOARDED_COLUMN_INDEX);
 13:   totalHoard += hoard;
 14: }
 15: 
 16: //计算平均值,并检查被零除错误
 17: float cursorCount = cursor.getCount();
 18: averageHoard = cursorCount > 0 ? (totalHoard / cursorCount) : Float.NaN;
 19: 
 20: //完成操作后关闭Cursor
 21: cursor.close();

因为SQLite数据库的列是松散类型的,所以可以根据需要把个别值强制转换为有效类型。例如,作为浮点数存储的值可以作为字符串来读取。

添加、更新和删除行

在任何时候,只要对底层数据库值进行了修改,就应该通过运行一个新的查询来更新Cursor。

1. insert

  1: //创建要插入的一行新值
  2: ContentValues newValues = new ContentValues();
  3: 
  4: //为每一行赋值
  5: newValues.put(KEY_GOLD_HOARD_NAME_COLUMN, hoardName);
  6: newValues.put(KEY_GOLD_HOARDED_COLUMN, hoardValue);
  7: newValues.put(KEY_GOLD_HOARD_ACCESSIBLE_COLUMN, hoardAccessible);
  8: //[... Repeat for earch column / value pair ...]
  9: 
 10: //把行插入到表中
 11: SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
 12: db.insert(HoardDBOpenHelper.DATABASE_TABLE, null, newValues);

在上面,insert方法的第二个参数称为null列侵入(null column hack)。如果想在SQLite数据库表插入一个新行时,必须显式地指定至少一列及对应的值,这个值可以是null。

2. 更新行

  1: //创建更新行的ContentValues
  2: ContentValues updatedValues = new ContentValues();
  3: 
  4: //为每一行赋值
  5: updatedValues.put(KEY_GOLD_HOARD_NAME_COLUMN, newHoardName);
  6: //[... Repeat for earch column / value pair ...]
  7: 
  8: //指定一个where子句来定义那些应该被更新
  9: //必要的时候指定where子句的参数
 10: String where = KEY_ID + "=" + hoardId;
 11: String[] whereArgs = null;
 12: 
 13: //使用新值更新具有指定索引的行
 14: SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
 15: db.update(HoardDBOpenHelper.DATABASE_TABLE,
 16:       updatedValues,
 17:       where,
 18:       whereArgs);

3.删除行

  1: String segment = uri.getPathSegments().get(1);
  2:       count = db.delete(EarthquakeDatabaseHelper.EARTHQUAKE_TABLE, KEY_ID
  3:           + "="
  4:           + segment
  5:           + (!TextUtils.isEmpty(selection) ? " AND" + " ("
  6:               + selection + ")" : ""), selectionArgs);
使用Content Provider、Cusor和Content Value来存储、共享和使用应用程序数据

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

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

public class MyContentProvider extends ContentProvider

1. 在Manifest中注册ContentProvider

使用authorites标记来设定Content Provider的基本URI。Content Provider的授权如同一个地址,Content Resolver使用它找到想要交互的数据库:

<provider android:name=”.MyContentProvider” Uri_CONTENT_URI = Uri.parse(“content://com.paad.skeletondatabaseprovider/elements”);

2. 发布ContentProvider的URI地址

public static final Uri_CONTENT_URI = Uri.parse(“content://com.paad.skeletondatabaseprovider/elements);

UriMatcher:分析URI的形式 ---明确的确定这个URI是请求所有数据还是请求单行数据

  1: //创建两个常量来区分不同的URI请求
  2: private static final int ALLROWS = 1;
  3: private static final int SINGLE_ROW = 2; 
  4: 
  5: private static final UriMatcher uriMatcher; 
  6: 
  7: //填充UriMatcher对象,其中以‘element’结尾的URI对应请求所有数据
  8: //以‘elments/[rowID]’结尾的URI代表请求单行数据
  9: Static{
 10:     uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 11:     uriMatcher.addURI("com.paad.skeletondatabaseprovider", "elements", ALLROWS);
 12:     uriMatcher.addURI("com.paad.skeletondatabaseprovider", "elements/#", SINGLE_ROW);
 13: }
 14: 

区分了全表和单行查询后,就可以很容易的使用SQLiteQueryBuilder类对一个查询应用额外的选择条件:

  1: SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
  2: 
  3: //如果是行查询,用传入的行限制结果集
  4: switch(uriMatcher.match(uri)){
  5:   case SINGLE_ROW:
  6:     String rowID = uri.getPathSegments().get(1);
  7:     queryBuilder.appendWhere(KEY_ID + "=" + rowID);
  8:     break;
  9:   default: break;
 10: }

3.创建Content Provider 的数据库

  1: private MySQLiteOpenHelper myOpenHelper;
  2: 
  3: @OVerride
  4: public boolean onCreate(){
  5:   //构造底层的数据库
  6:   //延迟打开数据库,直到需要执行一个查询或者事务时再打开
  7:   myOpenHelper = new MySQLiteOpenHelper(getContext(),
  8:             MySQLiteOpenHelper.DATABASE_NAME,
  9:             null,
 10:             MySQLiteOpenHelper.DATABASE_VERSION);
 11:             
 12:   return true;
 13: }

4. 实现ContentProvider查询

在ContentProvider中使用底层的SQLite数据库实现查询的框架代码

 

  1: private MySQLiteOpenHelper myOpenHelper;
  2: 
  3: @OVerride
  4: public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder){
  5:   //打开数据库
  6:   SQLiteDatabase db;
  7:   try{
  8:     db = myOpenHelper.getWritableDatabase();
  9:   }
 10:   catch(SQLiteException ex){
 11:     db = myOpenHelper.getReadableDatabase();
 12:   }
 13:   
 14:   //必要的话,使用有效的SQL语句替换这些语句
 15:   String groupBy = null;
 16:   String having = null;
 17:   
 18:   //使用SQLiteQueryBuilder来简化构造数据库查询的过程
 19:   SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
 20:   
 21:   //如果是行查询,用传入行限定结果集
 22:   switch(uriMatcher.match(uri)){
 23:     case SINGLE_ROW:
 24:       String rowID = uri.getPatchSegments().get(1);
 25:       queryBuilder.appedWhere(KEY_ID + "=" + rowID);
 26:       break;
 27:     default:break;
 28:   }
 29:   
 30:   //指定要执行查询的表,根据需要,这可以是一个特定的表或者一个连接
 31:   queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TABLE);
 32:   
 33:   //执行查询
 34:   Cursor cursor = queryBuilder.query(db, projection, selction, selectionArgs, groupBy, having, sortOrder);
 35:   
 36:   //返回结果Cursor
 37:   return cursor;
 38: }

实现查询后,还必须指定一个MIME类型来标识返回的数据。通过重写getType方法来返回唯一的描述了该数据类型的字符串。返回的类型应该包含两种形式:

  • 单一项:vnd.android.cursor.item/vnd.<companyname>.<contenttype>
  • 所有项:vnd.android.cursor.dir/vnd.<companyname>.<contenttype>
  1: @Override
  2: public String getType(Uri uri){
  3:   //为一个Content Provider URI返回一个字符串,它标识了MIME类型
  4:   switch(uriMatcher.match(uri)){
  5:     case ALLROWS:
  6:       return "vnd.android.cursor.dir/vnd.paad.elemental";
  7:     case SINGLE_ROW:
  8:       return "vnd.android.cursor.item/vnd.paad.elemental";
  9:     default:
 10:       throw new IllegalArgumentException("Unsupported URI: " + uri);
 11:   }
 12: }

5. Content Provider 事务

要想在Content Provider上使用delete、insert和update事务,需要实现相应的delete、insert和update方法。

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

  1: @Override
  2: public int delete(Uri uri, String selection, String[] selectionArgs){
  3:   //打开一个可读/可写的数据库来支持事务
  4:   SQLiteDatabase db = myOpenHelper.getWritableDatabase();
  5:   
  6:   //如果是行URI,限定删除的行为指定的行
  7:   switch(uriMatcher.match(uri)){
  8:     case SIGLE_ROW:
  9:       String rowID = uri.getPathSegments().get(1);
 10:       selection = KEY_ID + "=" + rowID + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
 11:     default: break;
 12:   }
 13: 
 14:   //要想返回删除的项的数量,必须指定一条where子句。要删除所有行并返回一个值,则传入“1”
 15:   if(selection == null) selection = "1";
 16:   
 17:   //执行删除
 18:   int deleteCount = db.delete(MySQLiteOpenHelper.DATABASE_TABLE, selection, selectionArgs);
 19:   
 20:   //通知所有的观察者,数据集已经改变
 21:   getContext().getContentResolver().notifyChange(uri, null);
 22:   
 23:   //返回删除的项的数量
 24:   return deleteCount;
 25: }
 26: 
 27: @Override
 28: public Uri insert(Uri uri, ContentValues values){
 29:   //打开一个可读/可写的数据库来支持事务
 30:   SQLiteDatabase db = myOpenHelper.getWritableDatabase();
 31:   
 32:   //要想通过传入一个空Content Value对象来向数据库添加一个空行
 33:   //必须使用nullColumnHack参数来指定可以设置为null的列名
 34:   String nullColumnHack = null;
 35:   
 36:   //向表中插入值
 37:   long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE, nullColumnHack, values);
 38:   
 39:   //构造并返回新插入行的URI
 40:   if(id > -1){
 41:     //构造并返回新插入行的URI
 42:     Uri insertedId = ContentUris.withAppeddedId(CONTENT_URI, id);
 43:     
 44:     //通知所有的观察者,数据集已经改变
 45:     getContext().getContentResolver().notifyChange(insertedId);
 46:     
 47:     return insertedId;
 48:   }
 49:   else
 50:     return null;  
 51: }
 52: 
 53: @Override
 54: public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs){
 55:   //打开一个可读/可写的数据库来支持事务
 56:   SQLiteDatabase db = myOpenHelper.getWritableDatabase();
 57:   
 58:   //如果是行URI,限定删除的行为指定的行
 59:   switch(uriMatcher.match(uri)){
 60:     case SINGLE_ROW:
 61:       String rowID = uri.getPathSegments().get(1);
 62:       selection = KEY_ID + "=" + rowID +(!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
 63:     default:break;
 64:   }
 65:   
 66:   //执行更新
 67:   int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TABLE, values, selection, selectionArgs);
 68:   
 69:   //通知所有的观察者,数据集已经改变
 70:   getContext().getContentResolver().notifyChange(uri, null);
 71:   
 72:   return updateCount;
 73: }

6. 在Content Provider中存储文件

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

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

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

  1: @Override
  2: public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException{
  3:   //找到行ID并把它作为一个文件名使用
  4:   String rowID = uri.getPathSegments().get(1);
  5:   
  6:   //在应用程序的外部文件目录中创建一个文件对象
  7:   String picsDir = Environment.DIRECTORY_PICTURES;
  8:   File file = new File(getContext().getExternalFilesDir(picsDir), rowID);
  9:   
 10:   //如果文件不存在,则直接创建它
 11:   if(!file.exists()){
 12:     try{
 13:       file.createNewFile();
 14:     }
 15:     catch(IOExecption e){
 16:       Log.d(TAG, "File creation failed: " + e.getMessage());
 17:     }
 18:   }
 19:   
 20:   //将mode参数转换为对应的ParcelFileDescriptor打开模式
 21:   int fileMode = 0;
 22:   if(mode.contains("w")) fileMode |= ParcelFileDescriptor.MODE_WRITE_ONLY;
 23:   if(mode.contains("r")) fileMode |= ParcelFileDescriptor.MODE_READ_ONLY;
 24:   if(mode.contains("+")) fileMode |= ParcelFileDescriptor.MODE_APPEND;
 25:   
 26:   //返回一个代表了文件的ParcelFileDescriptor
 27:   return ParcelFileDescriptor.open(file, fileMode);
 28: }

使用Content Provider

1. Content Resolver 与 Content Provider每个应用程序都有一个ContentResolver实例,可以使用getContentResolver方法来对其进行访问

ContentResolver cr = getContentResolver();

当使用Content Provider公开数据时,Content Resolver是用来在这些Content Provider上进行查询和执行事务的对应类。ContentProvider提供了底层数据的抽象,而Content Resolver则提供了查询或处理的ContentProvider的抽象。

2. 查询Content Provider

  1: //获得Content Resolver
  2: ContentResolver cr = getContentResolver();
  3: 
  4: //指定结果列投影。返回满足要求所需的最小列集
  5: String[] result_columns = new String[]{
  6:   MyHoardContentProvider.KEY_ID,
  7:   MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN,
  8:   MyHoardContentProvider.KEY_GOLD_HOARD_COLUMN };
  9: 
 10: //指定用于限制结果的where子句
 11: String where = MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN + "=" + 1;
 12: 
 13: //根据需要,用有效的SQL语句替换以下语句
 14: String whereArgs[] = null;
 15: String order = null;
 16: 
 17: //返回指定的行
 18: Cursor resultCursor = cr.query(MyHoardContentProvider.CONTENT_URI, result_columns, where, whereArgs, order);

在Content Provider中查询特定行

  1: //获得Content Resolver
  2: ContentResolver cr = getContentResolver();
  3: 
  4: //指定结果列投影,返回满足要求所需的最小列集
  5: String[] result_columns = new String[]{
  6:   MyHoardContentProvider.KEY_ID,
  7:   MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN,
  8:   MyHoardContentProvider.KEY_GOLD_HOARD_COLUMN };
  9: 
 10: //将一个行ID附加到URI以定位特定的行
 11: Uri rowAddress = ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI, rowId);
 12: 
 13: //由于我们在请求单独的一行,因此下列变量的取值都为null
 14: String where = null;
 15: String whereArgs[] = null;
 16: String order = null;
 17: 
 18: //返回指定的行
 19: Cursor resultCursor = cr.query(rowAddress, result_columns, where, whereArgs, order);

从Content Provider结果Cursor中取值

  1: float largestHoard = 0f;
  2: String hoardName = "No Hoards";
  3: 
  4: //找出所用列的索引
  5: int GOLD_HOARDED_COLUMN_INDEX = resultCursor.getColumnIndexOrThrow(MyHoardContentProvider.KEY_GOLD_HOARDED_CLUMN);
  6: int HOARD_NAME_COLUMN_INDEX = reslutCursor.getColumnIndexOrThrow(MyHoardContentProvider.KEY_GOLD_HOARD_NAME_COLUMN);
  7: 
  8: //迭代游标行
  9: //Cursor类被初始化为第一个元素的前一个位置,所以我们只能检查是否有“下一”行
 10: //如果结果Cursor为空,则返回false
 11: while(resultCursor.moveToNext()){
 12:   float hoard = resultCursor.getFloat(GOLD_HOARDED_COLUMN_INDEX);
 13:   if(hoard > largestHoard){
 14:     largestHoard = hoard;
 15:     hoardName = resultCursor.getString(HORAD_NAME_COLUMN_INDEX);
 16:   }
 17: }
 18: 
 19: //关闭Cursor
 20: resultCursor.close();

 

使用Cursor Loader异步查询Content Provider

数据库操作可能非常耗时的,所以对于任何数据库和Content Provider查询来说,最好不要在应用程序的主线程中执行,这一点特别重要哦。

Loader简介

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

CursorLoader允许针对ContentProvider执行异步查询并返回一个结果Cursor,而且对底层提供程序的任何更新都会发出通知。最好经常使用Cursor Loader来管理Activity和Fragment中的Cursor。

使用Cursor Loader

实现Cursor Loader Callback

要使用Cursor Loader,可创建一个新的LoaderManager.LoaderCallbacks实现。Loader Callback是使用泛型实现的。

LoaderManager.LoaderCallbacks<Cursor> loaderCallback = new LoaderManager.LoaderCallbacks<Cursor>(){

Loader Callback 由3个处理程序组成:

  • onCreatLoader 当Loader被初始化后调用它,返回Cursor Loader 对象。
  • onLoadFinished 当Loader Manager 已经完成了异步查询后执行,并把结果Cursor作为参数传入。
  • onLoaderReset 当Loader Manager重置Cursor Loader的时候,会调用onLoaderReset处理程序。应该在此处理程序中释放查询返回的数据引用,并且重置相应的UI。Load Manager会自动关闭Cursor。

onLoaderFinished和onLoaderReset与UI线程不是同步的。如果直接修改UI元素,首先需要使用Handler或者类似的机制与UI线程同步。

  1: public Loader<Cursor> onCreateLoader(int id, Bundle args){
  2:   //按照Cursor Loader的形式构造一个新的查询,使用id参数来构造和返回
  3:   //不同Loader
  4:   String[] projection = null;
  5:   String[] where = null;
  6:   String[] whereArgs = null;
  7:   String sortOder = null;
  8:   
  9:   //查询URI
 10:   Uri queryUri = MyContentProvider.CONTENT_URI;
 11:   
 12:   //创建新的Cursor Loader
 13:   return new CursorLoader(DatabaseSkeletonActivity.this, 
 14:               queryUri,
 15:               where,
 16:               whereArgs,
 17:               sortOrder);
 18: }
 19: 
 20: public void onLoadFinished(Loader<Cursor> loader, Cursor cursor){
 21:   //用一个新的结果集代替Cursor Adapter所显示的结果Cursor
 22:   adapter.swapCursor(cursor);
 23:   
 24:   //这个事件处理不会和UI线程同步,因此在直接修改任意UI元素之前需要同步它
 25: }
 26: 
 27: public void onLoaderReset(Loader<Cursor> loader){
 28:   //在List Adapter中将现有的结果Cursor移除
 29:   adapter.swapCursor(null);
 30:   
 31:   //这个处理程序不会和UI线程同步,因此在直接修改任意UI元素之前需要同步它
 32: }

初始化和重新启动Cursor Loader

LoaderManager loaderManager = getLoaderManager();

Bundle args = null;

loaderManager.initLoader(LOADER_ID, args, myLoaderCallbacks);

创建loader,重复调用initLoader方法将只会返回现有的Loader。若重新创建它:

loaderManager.restartLoader(LOADER_ID, args, myLoaderCallbacks);

添加、删除和更新内容

Uri myRowUri = cr.insert(MyHoardContentProvider.CONTENT_URI, newValues);

int deletedRowCount = cr.delete(MyHoardContentProvider.CONTENT_URI, where, whereArgs);

int updatedRowCount = cr.update(rowURI, updatedValues, where, whereArgs);

访问Content Provider中存储的文件

ContentProvider 使用完全限定的URI来表示大文件,而不是使用原始文件blob. 但是,在使用ContentResolver时,这一点被抽象化。

要访问存储在Content Provider中的文件,或者把一个新文件插入到Content Provider中,可以分别使用Content Resolver的openOutputStream和openInputStream方法,并传入包含所需文件的Content Provider行的URI。Content Provider将解释你的请求,并返回所请求文件的一个输入流或者输出流。

  1: public void addNewHoardWithImag(String hoardName, float hoardValue, boolean hoardAccessible, Bitmap bitmap){
  2:   //创建要插入的新行
  3:   ContentValues newValues = new ContentValues();
  4:   
  5:   //为每一行赋值
  6:   newValues.put(MyHoardContentProvider.KEY_GOLD_HOARD_NAME_COLUMN, hoardName);
  7:   newValues.put(MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN, hoardName);
  8:   newValues.put(MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN, hoardAccessible);
  9:   
 10:   //获得Content Provider
 11:   ContentResolver cr = getContentResolver();
 12:   
 13:   //在表中插入行
 14:   Uri myRowUri = cr.insert(MyHoardContentProvider.CONTENT_URI, newValues);
 15:   
 16:   try{
 17:     //使用新行的URI打开一个输出流
 18:     OutputStream outStream = cr.openOutputStream(myRowUri);
 19:     //压缩位图并将其保存到提供程序中
 20:     bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
 21:   }
 22:   catch(FileNotFoundException e){
 23:     Log.d(TAG, "No file Found for this record.");
 24:   }
 25: }
 26: 
 27: public Bitmap getHoardImage(long rowId){
 28:   Uri myRowUri = ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI, rowId);
 29:   
 30:   try{
 31:     //使用新行的URI打开一个输入流
 32:     InputStream inStream = getContentResolver().openInputStream(myRowUri);
 33:     
 34:     //创建位图的一个副本
 35:     Bitmap bitmap = BitmapFactory.decodeStream(inStream);
 36:     return bitmap;
 37:   }
 38:   catch(FileNotFoundException e){
 39:     Log.d(TAG, "No file found for this record.");
 40:   }
 41:   
 42:   return null;
 43: }

又到了实践检验真理阶段了,为To-Do List 创建数据库支持,继续前面的例子:

(1)首先创建一个新的ToDoContentProvider类。它将扩展了ContentProvider类并使用SQLiteOPenHelper用来托管数据库和管理数据库交互。它将包含一些stub方法。

  1: package com.paad.todolist;
  2: 
  3: import android.content.ContentProvider;
  4: import android.content.ContentUris;
  5: import android.content.ContentValues;
  6: import android.content.Context;
  7: import android.content.UriMatcher;
  8: import android.database.Cursor;
  9: import android.database.sqlite.SQLiteDatabase;
 10: import android.database.sqlite.SQLiteDatabase.CursorFactory;
 11: import android.database.sqlite.SQLiteOpenHelper;
 12: import android.database.sqlite.SQLiteQueryBuilder;
 13: import android.net.Uri;
 14: import android.text.TextUtils;
 15: import android.util.Log;
 16: 
 17: public class ToDoContentProvider extends ContentProvider {
 18: 
 19:   @Override
 20:   public boolean onCreate() {
 21:     // 构造底层的数据库
 22:     // 延迟打开数据库,知道需要执行一个查询或者事务时再打开
 23:     myOpenHelper = new MySQLiteOpenHelper(getContext(),
 24:         MySQLiteOpenHelper.DATABASE_NAME, null,
 25:         MySQLiteOpenHelper.DATABASE_VERSION);
 26: 
 27:     return true;
 28:   }
 29: 
 30:   @Override
 31:   public Cursor query(Uri uri, String[] projection, String selection,
 32:       String[] selectionArgs, String sortOrder) {
 33: 
 34:       return false;
 35:   }
 36: 
 37:   @Override
 38:   public String getType(Uri uri) {
 39:                return "";
 40:   }
 41: 
 42:   @Override
 43:   public Uri insert(Uri uri, ContentValues values) {
 44:     return null;
 45:   }
 46: 
 47:   @Override
 48:   public int delete(Uri uri, String selection, String[] selectionArgs) {
 49: 
 50:     return 0;
 51:   }
 52: 
 53:   @Override
 54:   public int update(Uri uri, ContentValues values, String selection,
 55:       String[] selectionArgs) {
 56:     
 57:     return 0;
 58:   }
 59: 
 60:   private class MySQLiteOpenHelper extends SQLiteOpenHelper {
 61: 
 62: 
 63:     public MySQLiteOpenHelper(Context context, String name,
 64:         CursorFactory factory, int version) {
 65:       super(context, name, factory, version);
 66:     }
 67: 
 68:     // 当磁盘中没有数据库时调用,辅助类需要创建一个数据库
 69:     @Override
 70:     public void onCreate(SQLiteDatabase db) {
 71: 
 72:     }
 73: 
 74:     // 当数据库版本不匹配时调用,也就是说,磁盘上的数据库版本需要升级到当前版本
 75:     @Override
 76:     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
 77:       
 78:     }
 79:   }
 80: }
 81: 

(2) 为ToDoContentProvider发布一个URI,其它应用程序组件通过ContentResolver使用这个URI来访问ToDoContentProvider。

  1: public static final Uri CONTENT_URI = Uri
  2:       .parse("content://com.paad.todoprovider/todoitems");

(3)创建定义了列名的公有静态变量。这些变量会在SQLiteOpenHelper类中使用,用来创建数据库和其他应用程序组件提取你的查询结果值。

  1:   public static final String KEY_ID = "_id";
  2:   public static final String KEY_TASK = "task";
  3:   public static final String KEY_CREATION_DATE = "creation_date";

(4)在MySQLiteOpenHelper类中,创建变量来存储数据库的名称和版本以及待办事项表的名称。

  1:     public static final String DATABASE_NAME = "todoDatabase.db";
  2:     public static final int DATABASE_VERSION = 1;
  3:     public static final String DATABASE_TABLE = "todoItemTable";

(5) 在MySQLiteOpenHelper类中,重写onCreate和onUpgrade方法,它们处理使用第(3)步中创建的列名变量和标准的升级逻辑来创建数据库。

  1: // 创建新数据库的SQL语句
  2:     private static final String DATABASE_CREATE = "create table "
  3:         + DATABASE_TABLE + " (" + KEY_ID
  4:         + " integer primary key autoincrement, " + KEY_TASK
  5:         + " text not null, " + KEY_CREATION_DATE + " long);";
  6: 
  7:     // 当磁盘中没有数据库时调用,辅助类需要创建一个数据库
  8:     @Override
  9:     public void onCreate(SQLiteDatabase db) {
 10:       db.execSQL(DATABASE_CREATE);
 11:     }
 12: 
 13:     // 当数据库版本不匹配时调用,也就是说,磁盘上的数据库版本需要升级到当前版本
 14:     @Override
 15:     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
 16:       // 记录版本升级
 17:       Log.w("TaskDBAdapter", "Upgrading from version " + oldVersion
 18:           + " to " + newVersion + ", which will destroy all old data");
 19: 
 20:       // 将现有数据库升级到新的版本,通过比较oldVersion和新的newVersion的值可以处理多个旧版本
 21: 
 22:       // 最简单的方式就是删除旧表,创建新表
 23:       db.execSQL("DROP TABLE IF IT EXISTS " + DATABASE_TABLE);
 24: 
 25:       // 创建新表
 26:       onCreate(db);
 27:     }

(6)回到ToDoContentProvider类中,添加一个私有变量来保存一个MySQLiteOpenHelper类的引用,并在onCreate方法中创建它。

  1: private MySQLiteOpenHelper myOpenHelper;
  2: 
  3: @Override
  4: public boolean onCreate() {
  5: // 构造底层的数据库
  6: // 延迟打开数据库,知道需要执行一个查询或者事务时再打开
  7:   myOpenHelper = new MySQLiteOpenHelper(getContext(),
  8:   MySQLiteOpenHelper.DATABASE_NAME, null,
  9:   MySQLiteOpenHelper.DATABASE_VERSION);
 10: 
 11:   return true;
 12: }
 13: 

(7)仍然在ToDoContentProvider中,创建一个新的UriMatcher,使得Content Provider能够区分是全表查询还是针对特定的行查询。在getType方法中根据查询的类型返回正确的MIME类型。

  1:   private static final UriMatcher uriMatcher;
  2: 
  3:   // 填充UriMatcher对象,以‘element’结尾的URI对应请求全部数据
  4:   // 以‘elements/[rowID]’结尾的URI代表请求单行数据
  5:   static {
  6:     uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
  7:     uriMatcher.addURI("com.paad.todoprovider", "todoitems", ALLROWS);
  8:     uriMatcher.addURI("com.paad.todoprovider", "todoitems/#", SINGLE_ROW);
  9:   }
 10: 
 11: @Override
 12:   public String getType(Uri uri) {
 13:     // 为一个Content Provider URI返回一个字符串,它标识了MIME类型
 14:     switch (uriMatcher.match(uri)) {
 15:     case ALLROWS:
 16:       return "vnd.android.cursor.dir/vnd.paad.todos";
 17:     case SINGLE_ROW:
 18:       return "vnd.android.cursor.item/vnd.paad.todos";
 19:     default:
 20:       throw new IllegalArgumentException("Unsupported URI: " + uri);
 21:     }
 22:   }

(8)实现query stub方法。在基于传入的参数构造一个查询之前,首先请求一个数据库的实例。

  1: @Override
  2:   public Cursor query(Uri uri, String[] projection, String selection,
  3:       String[] selectionArgs, String sortOrder) {
  4:     // 打开一个只读的数据库
  5:     SQLiteDatabase db = myOpenHelper.getReadableDatabase();
  6: 
  7:     // 必要的话,使用SQL语句替换这些语句
  8:     String groupBy = null;
  9:     String having = null;
 10: 
 11:     SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
 12:     queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TABLE);
 13: 
 14:     // 如果是行查询,用传入行限制结果集
 15:     switch (uriMatcher.match(uri)) {
 16:     case SINGLE_ROW:
 17:       String rowID = uri.getPathSegments().get(1);
 18:       queryBuilder.appendWhere(KEY_ID + "=" + rowID);
 19:     default:
 20:       break;
 21:     }
 22: 
 23:     Cursor cursor = queryBuilder.query(db, projection, selection,
 24:         selectionArgs, groupBy, having, sortOrder);
 25: 
 26:     return cursor;
 27:   }

(9)使用相同的方式实现delete、insert,update。

  1: @Override
  2:   public Uri insert(Uri uri, ContentValues values) {
  3:     // 打开一个可写的数据库支持事务
  4:     SQLiteDatabase db = myOpenHelper.getWritableDatabase();
  5: 
  6:     // 要想通过传入一个空Content Value对象的方式向数据库中添加一个空行,
  7:     // 必须使用nullColumnHack参数指定可以设置为null的列名
  8:     String nullColumnHack = null;
  9: 
 10:     // 向表中插入值
 11:     long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE, nullColumnHack,
 12:         values);
 13: 
 14:     // 构造并返回新插入行的URI
 15:     if (id > -1) {
 16:       Uri insertedId = ContentUris.withAppendedId(CONTENT_URI, id);
 17: 
 18:       // 通知所有的观察者,数据集已经改变
 19:       getContext().getContentResolver().notifyChange(insertedId, null);
 20: 
 21:       return insertedId;
 22:     } else
 23:       return null;
 24:   }
 25: 
 26:   @Override
 27:   public int delete(Uri uri, String selection, String[] selectionArgs) {
 28:     // 打开一个可写的数据库支持事务
 29:     SQLiteDatabase db = myOpenHelper.getWritableDatabase();
 30: 
 31:     // 如果是行URI,限制删除的行为指定为行
 32:     switch (uriMatcher.match(uri)) {
 33:     case SINGLE_ROW:
 34:       String rowID = uri.getPathSegments().get(1);
 35:       selection = KEY_ID
 36:           + "="
 37:           + rowID
 38:           + (!TextUtils.isEmpty(selection) ? " AND (" + selection
 39:               + ")" : "");
 40:     default:
 41:       break;
 42:     }
 43: 
 44:     // 返回想要删除的项的数量,必须指定一条where语句。要删除所有行并返回一个值,则传入1""
 45:     if (selection == null)
 46:       selection = "1";
 47: 
 48:     // 执行删除
 49:     int deleteCount = db.delete(MySQLiteOpenHelper.DATABASE_NAME,
 50:         selection, selectionArgs);
 51: 
 52:     return deleteCount;
 53:   }
 54: 
 55:   @Override
 56:   public int update(Uri uri, ContentValues values, String selection,
 57:       String[] selectionArgs) {
 58:     // 打开一个可写的数据库
 59:     SQLiteDatabase db = myOpenHelper.getWritableDatabase();
 60: 
 61:     // 如果是行URI,限定删除的行为指定的行
 62:     switch (uriMatcher.match(uri)) {
 63:     case SINGLE_ROW:
 64:       String rowID = uri.getPathSegments().get(1);
 65:       selection = KEY_ID
 66:           + "="
 67:           + rowID
 68:           + (!TextUtils.isEmpty(selection) ? " AND (" + selection
 69:               + ")" : "");
 70:     default:
 71:       break;
 72:     }
 73: 
 74:     //执行更新
 75:     int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TABLE, values, selection, selectionArgs);
 76:     
 77:     //通知所有观察着,数据集已经改变
 78:     getContext().getContentResolver().notifyChange(uri, null);
 79:     
 80:     return updateCount;
 81:   }

(10)将ToDoContentProvider加入到Manifest.XML中。

<provider
            android:name=".ToDoContentProvider"
            android:authorities="com.paad.todoprovider" />

(11)回到ToDoListActivity并修改它来持久化保存待办事项数组。首先,通过修改Activity来实现LoaderManager.LoaderCallbacks<Cursor>,然后添加相关的stub方法。

  1: @SuppressLint("NewApi")
  2: public class ToDoListActivity extends Activity implements
  3:     NewItemFragment.OnNewItemAddedListener,
  4:     LoaderManager.LoaderCallbacks<Cursor> {
  5:   @Override
  6:   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
  8:     return loader;
  9:   }
 10: 
 11:   @Override
 12:   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
 22:   }
 23: 
 24:   @Override
 25:   public void onLoaderReset(Loader<Cursor> loader) {
 27:   }
 28: 
 29: }

(12)通过构建并返回一个Loader来完成onCreateLoader程序,该Loader会查询ToDoListContentProvider的所有元素。

  1: @Override
  2:   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
  3:     CursorLoader loader = new CursorLoader(this, ToDoContentProvider.CONTENT_URI, null, null, null, null);
  4:     return loader;
  5:   }
  6: 

(13)当Loader的查询完成时,结果会返回到onLoaderFinished处理程序。更新Cursor以迭代结果Cursor,并据此填充待办事项列表的ArrayAdapter。

  1:   @Override
  2:   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
  3:     int keyTaskIndex = cursor.getColumnIndexOrThrow(ToDoContentProvider.KEY_TASK);
  4:     
  5:     todoItems.clear();
  6:     while(cursor.moveToNext()){
  7:       ToDoItem newItem = new ToDoItem(cursor.getString(keyTaskIndex));
  8:       todoItems.add(newItem);
  9:     }
 10:     
 11:     aa.notifyDataSetChanged();
 12:   }

在onCreate中加入:

getLoaderManager().initLoader(0, null, this);

在onResume中加入:

getLoaderManager().initLoader(0, null, this);

(14)最后一步要修改onNewItemAdded方法的行为。应该使用ContentResolver把事项添加到ContentProvider中,而不是直接向待办事项ArrayList添加新事项。

  1:   public void onNewItemAdded(String newItem) {
  2: //    ToDoItem newTodoItem = new ToDoItem(newItem);
  3: //    todoItems.add(0, newTodoItem);
  4: //    aa.notifyDataSetChanged();
  5:     
  6:     ContentResolver cr = getContentResolver();
  7:     ContentValues values = new ContentValues();
  8:     values.put(ToDoContentProvider.KEY_TASK, newItem);
  9:     
 10:     cr.insert(ToDoContentProvider.CONTENT_URI, values);
 11:     getLoaderManager().restartLoader(0, null, this);
 12:   }

到此,数据已经保存到了你创建的数据库,运行效果:

image

 

在应用程序中添加搜索功能

Android 通过3种方式来为应用程序提供搜索功能:

  • 搜索栏: 一般由硬件提供
  • 搜索视图:Android3.0(API Level 11)中引入,它是一个搜索微件,可以放在Activity的任何地方。
  • 快速搜索框:是一个主屏幕搜索微件,它可以跨越所有快速支持搜索的应用程序执行搜索。

1. 使ContentProvider可搜索

要启用搜索对话框或者程序中使用搜索视图微件之前,需要定义什么内容可以被搜索。首先要在项目RES/xml文件夹中创建一个新的可搜索元数据的XML资源。

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

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

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

  1:         <activity
  2:             android:name=".ToDoListActivity"
  3:             android:label="@string/app_name" >
  4:             <intent-filter>
  5:                 <action android:name="android.intent.action.SEARCH" />
  6:                 <category android:name="android.intent.category.DEFAULT" />
  7:                 
  8:                 <action android:name="android.intent.action.MAIN" />
  9: 
 10:                 <category android:name="android.intent.category.LAUNCHER" />
 11:             </intent-filter>
 12:             <meta-data
 13:                 android:name="android.app.searchable"
 14:                 android:resource="@xml/searchable" />            
 15:         </activity>

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

  1: <meta-data
  2:                 android:name="android.app.searchable"
  3:                 android:resource="@xml/searchable"
  4:                 android:value=".DatabaseSkeletonSearchActivity" />

用户搜索之后,会启动Activity:

  1: @Override
  2: public void onCreate(Bundle savedInstanceState){
  3:   super.onCreate(savedInstanceState);
  4:   
  5:   //获得启动的Intent
  6:   parseIntent(getIntent());
  7: }
  8: 
  9: @Override
 10: protected void onNewIntent(Intent intent){
 11:   super.onNewIntent(intent);
 12:   
 13:   //获得启动的Intent
 14:   parseIntent(getIntent());
 15: }
 16: 
 17: private void parseIntent(Intent intent){
 18:   // 如果Activity已开始为搜索请求提供服务,那么提供搜索查询
 19:   if(Intent.ACTION_SEARCH.equals(intent.getAction())){
 20:     String searchQuery = intent.getStringExtra(SearchManager.QUERY);
 21:     
 22:     //执行搜索
 23:     performSearch(searchQuery);
 24:   }
 25: }
 26: 

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

设置一个Activity为默认的搜索结果提供程序,为应用程序内的所有Activity提供搜索功能。实现这个功能需要在Application清单节点上添加一个<meta-data>标记。

<meta-data

    android:name=”android.app.default_searchable”

    android:value=”.DatabaseSkeletonSearchActivity” />

执行搜索并显示结果

当搜索Activity接收到一个新的搜索查询时,就必须执行搜索并在Activity中显示搜索结果。

4. 使用搜索视图微件

Android 3.0(API Level 11)引入了Search View 微件来替换Activity搜索栏。搜索视图的外观和功能类似于编辑文本视图,但是其通过配置可以提供搜索建议和启动应用程序中的搜索查询。

要把搜索视图连接到搜索Activity中,首先使用Search Manager的getSearchableInfo方法提取SearchableInfo的一个引用。使用搜索视图的setSearchableInfo方法把这个对象绑定到你的搜索视图上。

  1: //使用Search Manager获得与这个Activity关联的SearchableInfo
  2: SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
  3: 
  4: SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
  5: 
  6: //将Activity的SearchableInfo绑定到搜索视图上
  7: SearchView searchView = (SearchView)findViewById(R.Id.searchView);
  8: searchView.SetSearchableInfo(searchalbeInfo);

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

searchView.setIconifiedByDefault(false) ;

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

searchView.setSubmitButtonEnabled(true);

5. 由Content Provider支持搜索建议

6.在快速搜索框中显示搜索结果

快速搜索框(Quick Search Box QSB)是一个主屏幕微件,可以搜索设备上安装的每个应用程序以及进行Web搜索。

---------------------------
保存更改
---------------------------
尚未保存对此日志所做的更改。

是否要在继续操作之前保存更改?
---------------------------
Yes   No   Cancel  
---------------------------

实例学习:创建可搜索的地震Content Provider

1. 创建 Content Provider

(1)打开Earthquake项目,创建一个新的EarthquakeProvider类,它扩展了ContentProvider类。

  1: package com.paad.earthquake;
  2: 
  3: import java.util.HashMap;
  4: 
  5: import android.app.SearchManager;
  6: import android.content.ContentProvider;
  7: import android.content.ContentUris;
  8: import android.content.ContentValues;
  9: import android.content.Context;
 10: import android.content.UriMatcher;
 11: import android.database.Cursor;
 12: import android.database.SQLException;
 13: import android.database.sqlite.SQLiteDatabase;
 14: import android.database.sqlite.SQLiteDatabase.CursorFactory;
 15: import android.database.sqlite.SQLiteOpenHelper;
 16: import android.database.sqlite.SQLiteQueryBuilder;
 17: import android.net.Uri;
 18: import android.text.TextUtils;
 19: import android.util.Log;
 20: 
 21: public class EarthquakeProvider extends ContentProvider {
 22:   public EarthquakeProvider() {
 23:   }
 24: 
 25:   @Override
 26:   public int delete(Uri uri, String selection, String[] selectionArgs) {
 27: 
 28:     return count;
 29:   }
 30: 
 31:   @Override
 32:   public String getType(Uri uri) {
 33:   }
 34: 
 35:   @Override
 36:   public Uri insert(Uri uri, ContentValues values) {
 37:   }
 38: 
 39:   @Override
 40:   public boolean onCreate() {
 41:   }
 42: 
 43:   @Override
 44:   public Cursor query(Uri uri, String[] projection, String selection,
 45:       String[] selectionArgs, String sortOrder) {
 46:   }
 47: 
 48:   @Override
 49:   public int update(Uri uri, ContentValues values, String selection,
 50:       String[] selectionArgs) {
 51:   }
 52: 
 53:   // 用于打开、创建和管理数据库版本控制的辅助类
 54:   private static class EarthquakeDatabaseHelper extends SQLiteOpenHelper {
 55: 
 56:   }
 57: }
 58: 

(2)发布这个程序的URI。这个URI将用来通过ContentResolver从其他应用程序组件中访问对应的ContentProvider。

public static final Uri CONTENT_URI = Uri
            .parse("content://com.paad.earthquakeprovider/earthquakes");

(3)定义表列名

  1: // 列名
  2:   public static final String KEY_ID = "_id";
  3:   public static final String KEY_DATE = "date";
  4:   public static final String KEY_DETAILS = "details";
  5:   public static final String KEY_SUMMARY = "summary";
  6:   public static final String KEY_LOCATION_LAT = "latitude";
  7:   public static final String KEY_LOCATION_LNG = "longitude";
  8:   public static final String KEY_MAGNITUDE = "magnitude";
  9:   public static final String KEY_LINK = "link";

(4)开始数据库的创建。创建一个SQLiteOpenHelper的实现类。

  1: // 用于打开、创建和管理数据库版本控制的辅助类
  2:   private static class EarthquakeDatabaseHelper extends SQLiteOpenHelper {
  3: 
  4:     private static final String TAG = "EarthquakeProvider";
  5: 
  6:     private static final String DATABASE_NAME = "earthquakes.db";
  7:     private static final int DATABASE_VERSION = 1;
  8:     private static final String EARTHQUAKE_TABLE = "earthquakes";
  9: 
 10:     private static final String DATABASE_CREATE = "create table "
 11:         + EARTHQUAKE_TABLE + " (" + KEY_ID
 12:         + " integer primary key autoincrement, " + KEY_DATE
 13:         + " INTEGER, " + KEY_DETAILS + " TEXT, " + KEY_SUMMARY
 14:         + " TEXT, " + KEY_LOCATION_LAT + " FLOAT, " + KEY_LOCATION_LNG
 15:         + " FLOAT, " + KEY_MAGNITUDE + " FLOAT, " + KEY_LINK
 16:         + " TEXT);";
 17: 
 18:     // 底层数据库
 19:     private SQLiteDatabase earthquakeDB;
 20: 
 21:     public EarthquakeDatabaseHelper(Context context, String name,
 22:         CursorFactory factory, int version) {
 23:       super(context, name, factory, version);
 24:       // TODO Auto-generated constructor stub
 25:     }
 26: 
 27:     @Override
 28:     public void onCreate(SQLiteDatabase db) {
 29:       db.execSQL(DATABASE_CREATE);
 30:     }
 31: 
 32:     @Override
 33:     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
 34:       Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
 35:           + newVersion + ", which will destroy all old data.");
 36: 
 37:       db.execSQL("DROP TABLE IF EXISTS " + EARTHQUAKE_TABLE);
 38:       onCreate(db);
 39:     }
 40: 
 41:   }

(5)重写onCreate.创建一个第四步中创建的数据库辅助类的新实例。

  1: @Override
  2:   public boolean onCreate() {
  3:     Context context = getContext();
  4: 
  5:     dbHelper = new EarthquakeDatabaseHelper(context,
  6:         EarthquakeDatabaseHelper.DATABASE_NAME, null,
  7:         EarthquakeDatabaseHelper.DATABASE_VERSION);
  8: 
  9:     return true;
 10:   }

(6)创建一个UriMatcher。来处理不同URI的请求,对整个QUAKES数据集上的查询和事务操作,还有对匹配地震索引值(QUAKE_ID)的单一记录的支持。还要重写getType方法,以便为支持的每种URI结构返回一个MIME类型。

  1:   // 创建用来区分不同URI请求的常量
  2:   private static final int QUAKES = 1;
  3:   private static final int QUAKE_ID = 2;
  4: 
  5:   private static final UriMatcher uriMatcher;
  6: // 分配UriMatcher对象,其中‘earthquakes’结尾的URI对应对所有地震的请求
  7:   // 在‘earthquakes’后面带有‘/[rowID]’的URI标识单个地震行。
  8:   static {
  9:     uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 10:     uriMatcher.addURI("com.paad.earthquakeprovider", "earthquakes", QUAKES);
 11:     uriMatcher.addURI("com.paad.earthquakeprovider", "earthquakes/#",
 12:         QUAKE_ID);
 13:   }
 14: 
 15: @Override
 16:   public String getType(Uri uri) {
 17:     switch (uriMatcher.match(uri)) {
 18:     case QUAKES:
 19:       return "vnd.android.cursor.dir/vnd.paad.earthquake";
 20:     case QUAKE_ID:
 21:       return "vnd.android.cursor.item/vnd.paad.earthquake";
 22:     case SEARCH:
 23:       return SearchManager.SUGGEST_MIME_TYPE;
 24:     default:
 25:       throw new IllegalArgumentException("Unsupported URI:" + uri);
 26:     }
 27:   }

(7)实现查询和事务stub。

  1: @Override
  2:   public Cursor query(Uri uri, String[] projection, String selection,
  3:       String[] selectionArgs, String sortOrder) {
  4:     SQLiteDatabase db = dbHelper.getReadableDatabase();
  5: 
  6:     SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
  7: 
  8:     qb.setTables(EarthquakeDatabaseHelper.EARTHQUAKE_TABLE);
  9: 
 10:     // 如果这是一个行查询,把结果集限制为传入的行
 11:     switch (uriMatcher.match(uri)) {
 12:     case QUAKE_ID:
 13:       qb.appendWhere(KEY_ID + "=" + uri.getPathSegments().get(1));
 14:       break;
 20:     default:
 21:       break;
 22:     }
 23: 
 24:     // 如果没有指定排序顺序,就按日期、时间顺序
 25:     String orderBy;
 26:     if (TextUtils.isEmpty(sortOrder))
 27:       orderBy = KEY_DATE;
 28:     else
 29:       orderBy = sortOrder;
 30: 
 31:     // 对底层数据库应用查询
 32:     Cursor c = qb.query(db, projection, selection, selectionArgs, null,
 33:         null, orderBy);
 34: 
 35:     // 注册当游标结果集改变时将通知的上下文ContentResolver
 36:     c.setNotificationUri(getContext().getContentResolver(), uri);
 37: 
 38:     // 返回查询结果的游标
 39:     return c;
 40:   }

(8)实现插入,删除和更新。

  1: @Override
  2:   public Uri insert(Uri uri, ContentValues values) {
  3:     SQLiteDatabase db = dbHelper.getWritableDatabase();
  4: 
  5:     // 插入新行,如果db.insert的调用成功,就返回行号
  6:     long rowID = db.insert(EarthquakeDatabaseHelper.EARTHQUAKE_TABLE,
  7:         "quake", values);
  8: 
  9:     // 返回成功插入的行的URI
 10:     if (rowID > 0) {
 11:       Uri _uri = ContentUris.withAppendedId(CONTENT_URI, rowID);
 12:       getContext().getContentResolver().notifyChange(uri, null);
 13: 
 14:       return _uri;
 15:     }
 16: 
 17:     throw new SQLException("Failed to insert row into " + uri);
 18:   }
 19: 
 20:   @Override
 21:   public int delete(Uri uri, String selection, String[] selectionArgs) {
 22:     SQLiteDatabase db = dbHelper.getWritableDatabase();
 23: 
 24:     int count;
 25:     switch (uriMatcher.match(uri)) {
 26:     case QUAKES:
 27:       count = db.delete(EarthquakeDatabaseHelper.EARTHQUAKE_TABLE,
 28:           selection, selectionArgs);
 29:       break;
 30:     case QUAKE_ID:
 31:       String segment = uri.getPathSegments().get(1);
 32:       count = db.delete(EarthquakeDatabaseHelper.EARTHQUAKE_TABLE, KEY_ID
 33:           + "="
 34:           + segment
 35:           + (!TextUtils.isEmpty(selection) ? " AND" + " ("
 36:               + selection + ")" : ""), selectionArgs);
 37:     default:
 38:       throw new IllegalArgumentException("Unsupported URI:" + uri);
 39:     }
 40: 
 41:     getContext().getContentResolver().notifyChange(uri, null);
 42: 
 43:     return count;
 44:   }
 45: 
 46: 
 47:   @Override
 48:   public int update(Uri uri, ContentValues values, String selection,
 49:       String[] selectionArgs) {
 50:     SQLiteDatabase db = dbHelper.getWritableDatabase();
 51: 
 52:     int count;
 53:     switch (uriMatcher.match(uri)) {
 54:     case QUAKES:
 55:       count = db.update(EarthquakeDatabaseHelper.EARTHQUAKE_TABLE,
 56:           values, selection, selectionArgs);
 57:       break;
 58:     case QUAKE_ID:
 59:       String segment = uri.getPathSegments().get(1);
 60:       count = db.update(EarthquakeDatabaseHelper.EARTHQUAKE_TABLE,
 61:           values,
 62:           KEY_ID
 63:               + "="
 64:               + segment
 65:               + (!TextUtils.isEmpty(selection) ? " AND ("
 66:                   + selection + ")" : ""), selectionArgs);
 67:       break;
 68:     default:
 69:       throw new IllegalArgumentException("Unkown URI" + uri);
 70:     }
 71: 
 72:     getContext().getContentResolver().notifyChange(uri, null);
 73: 
 74:     return count;
 75:   }

(9)Provider 注册。

  1:         <provider
  2:             android:name=".EarthquakeProvider"
  3:             android:authorities="com.paad.earthquakeprovider"
  4:             android:enabled="true"
  5:             android:exported="true" >
  6:         </provider>

2. 使用Content Provider

(1)在EarthquakeListFragment中更新addNewQuake方法。

  1:   private void addNewQuake(Quake _quake) {
  2:     ContentResolver cr = getActivity().getContentResolver();
  3: 
  4:     // 构造一条where子句,以保证现在的提供程序中没有这个地震
  5:     String selection = EarthquakeProvider.KEY_DATE + " = "
  6:         + _quake.getDate().getTime();
  7: 
  8:     // 如果地震是新的,把它插入到提供程序中。
  9:     Cursor query = cr.query(EarthquakeProvider.CONTENT_URI, null,
 10:         selection, null, null);
 11:     if (query.getCount() == 0) {
 12:       ContentValues values = new ContentValues();
 13: 
 14:       values.put(EarthquakeProvider.KEY_DATE, _quake.getDate().getTime());
 15:       values.put(EarthquakeProvider.KEY_DETAILS, _quake.getDetails());
 16:       values.put(EarthquakeProvider.KEY_SUMMARY, _quake.toString());
 17: 
 18:       double lat = _quake.getLocation().getLatitude();
 19:       double lng = _quake.getLocation().getLongitude();
 20:       values.put(EarthquakeProvider.KEY_LOCATION_LAT, lat);
 21:       values.put(EarthquakeProvider.KEY_LOCATION_LNG, lng);
 22: 
 23:       values.put(EarthquakeProvider.KEY_LINK, _quake.getLink());
 24:       values.put(EarthquakeProvider.KEY_MAGNITUDE, _quake.getMagnitude());
 25: 
 26:       cr.insert(EarthquakeProvider.CONTENT_URI, values);
 27:     }
 28: 
 29:     query.close();
 30:   }

(2)因为现在地震信息存储到了ContentProvider中,所以应该使用SimpleCursorAdapter替换ArrayAdapter。

  1: @Override
  2: public void onActivityCreated(Bundle savedInstanceState) {
  3:     super.onActivityCreated(savedInstanceState); 
  4: 
  5:     // 创建一个新的适配器,并把它绑定到List View
  6:     adapter = new SimpleCursorAdapter(getActivity(),
  7:             android.R.layout.simple_list_item_1, null,
  8:             new String[] { EarthquakeProvider.KEY_SUMMARY },
  9:             new int[] { android.R.id.text1 }, 0); 
 10: 
 11:     setListAdapter(adapter); 
 12: 
 13:     getLoaderManager().initLoader(0, null, this); 
 14: 
 15:     Thread t = new Thread(new Runnable() { 
 16: 
 17:         @Override
 18:         public void run() {
 19:             refreshEarthquakes();
 20:         }
 21:     }); 
 22: 
 23:     t.start();
 24: }

(3)实现LoaderManager.LoaderCallbacks<Cursor>.

使用Cursor Loader来查询数据库,并向第二步中创建的游标适配器提供一个Cursor。

  1: @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  2: @SuppressLint({ "NewApi", "SimpleDateFormat" })
  3: public class EarthquakeListFragment extends ListFragment implements
  4:     LoaderManager.LoaderCallbacks<Cursor> {
  5:   @Override
  6:   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
  7:   }
  8: 
  9:   @Override
 10:   public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
 11:   }
 12: 
 13:   @Override
 14:   public void onLoaderReset(Loader<Cursor> loader) {
 15:   }
 16: 
 17: }

(4)完成onCreateLoader处理程序。使其构建并返回一个Loader,该Loader会查询EarthquakeProvider中的所有元素。

  1:   @Override
  2:   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
  3:     String[] projection = new String[] { EarthquakeProvider.KEY_ID,
  4:         EarthquakeProvider.KEY_SUMMARY };
  5: 
  6:     Earthquake earthquakeActivity = (Earthquake) getActivity();
  7:     String where = EarthquakeProvider.KEY_MAGNITUDE + " > "
  8:         + earthquakeActivity.minimumMagnitude;
  9: 
 10:     CursorLoader loader = new CursorLoader(getActivity(),
 11:         EarthquakeProvider.CONTENT_URI, projection, where, null, null);
 12: 
 13:     return loader;
 14:   }

(5)Loader查询完成之后,结果Cursor将返回给onLoadFinished处理程序,所以你需要用新的结果交换出原来的Cursor。类似的,当Loader重置时,删除对Cursor的引用。

  1:   @Override
  2:   public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
  3:     adapter.swapCursor(data);
  4:   }
  5: 
  6:   @Override
  7:   public void onLoaderReset(Loader<Cursor> loader) {
  8:     adapter.swapCursor(null);
  9:   }

(6)启动Loader在Activity的onActivityCreated。

  1:   @Override
  2:   public void onActivityCreated(Bundle savedInstanceState) {
  3:     super.onActivityCreated(savedInstanceState);
  4: 
  5:     // 创建一个新的适配器,并把它绑定到List View
  6:     adapter = new SimpleCursorAdapter(getActivity(),
  7:         android.R.layout.simple_list_item_1, null,
  8:         new String[] { EarthquakeProvider.KEY_SUMMARY },
  9:         new int[] { android.R.id.text1 }, 0);
 10: 
 11:     setListAdapter(adapter);
 12: 
 13:     getLoaderManager().initLoader(0, null, this);
 14: 
 15:     Thread t = new Thread(new Runnable() {
 16: 
 17:       @Override
 18:       public void run() {
 19:         refreshEarthquakes();
 20:       }
 21:     });
 22: 
 23:     t.start();
 24:   }
  1:   public void refreshEarthquakes() {
  2:     handler.post(new Runnable() {
  3: 
  4:       @Override
  5:       public void run() {
  6:         getLoaderManager().restartLoader(0, null, EarthquakeListFragment.this);
  7:       }
  8:     });
  9:   }

3. 搜索EarthquakeContentProvider

(1)添加字符串资源--对搜索的描述:

  1: <string name="search_description">search earthquake locations</string>

(2)res/xml下创建searchable.xml文件,它将搜索结果提供程序定义元数据,将步骤(1)中的字符串指定为描述。指定ContentProvider的授权并设置searchSuggestIntentAction和searchSuggestIntentData的属性。

  1: <?xml version="1.0" encoding="utf-8"?>
  2: <searchable xmlns:android="http://schemas.android.com/apk/res/android"
  3:     android:label="@string/app_name"
  4:     android:searchSettingsDescription="@string/search_description"
  5:     android:searchSuggestAuthority="com.paad.earthquakeprovider"
  6:     android:searchSuggestIntentAction="android.intent.action.VIEW"
  7:     android:searchSuggestIntentData="content://com.paad.earthquakeprovider/earthquakes" >
  8: 
  9: </searchable>

(3)在Content Provider中定义散列映射。散列映射用来提供支持搜素建议的投影。

  1:   private static final HashMap<String, String> SEARCH_PROJECTION_MAP;
  2:   static {
  3:     SEARCH_PROJECTION_MAP = new HashMap<String, String>();
  4:     SEARCH_PROJECTION_MAP.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
  5:         KEY_SUMMARY + "AS" + SearchManager.SUGGEST_COLUMN_TEXT_1);
  6:     SEARCH_PROJECTION_MAP.put("_id", KEY_ID + " AS " + "_id");
  7:   }

(4)修改UriMatcher来支持搜索。

  1:   // 创建用来区分不同URI请求的常量
  2:   private static final int QUAKES = 1;
  3:   private static final int QUAKE_ID = 2;
  4:   private static final int SEARCH = 3;
  5: 
  6:   private static final UriMatcher uriMatcher;
  7:   // 分配UriMatcher对象,其中‘earthquakes’结尾的URI对应对所有地震的请求
  8:   // 在‘earthquakes’后面带有‘/[rowID]’的URI标识单个地震行。
  9:   static {
 10:     uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 11:     uriMatcher.addURI("com.paad.earthquakeprovider", "earthquakes", QUAKES);
 12:     uriMatcher.addURI("com.paad.earthquakeprovider", "earthquakes/#",
 13:         QUAKE_ID);
 14:     uriMatcher.addURI("com.paad.earthquakeprovider",
 15:         SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH);
 16:     uriMatcher.addURI("com.paad.earthquakeprovider",
 17:         SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SEARCH);
 18:     uriMatcher.addURI("com.paad.earthquakeprovider",
 19:         SearchManager.SUGGEST_URI_PATH_SHORTCUT, SEARCH);
 20:     uriMatcher.addURI("com.paad.earthquakeprovider",
 21:         SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SEARCH);
 22:   }

(5)同样修改getType方法,为搜索结果返回适当的MIME类型。

  1:   @Override
  2:   public String getType(Uri uri) {
  3:     switch (uriMatcher.match(uri)) {
  4:     case QUAKES:
  5:       return "vnd.android.cursor.dir/vnd.paad.earthquake";
  6:     case QUAKE_ID:
  7:       return "vnd.android.cursor.item/vnd.paad.earthquake";
  8:     case SEARCH:
  9:       return SearchManager.SUGGEST_MIME_TYPE;
 10:     default:
 11:       throw new IllegalArgumentException("Unsupported URI:" + uri);
 12:     }
 13:   }

(6)对Content Provider修改支持搜索。

  1:   @Override
  2:   public Cursor query(Uri uri, String[] projection, String selection,
  3:       String[] selectionArgs, String sortOrder) {
  4:     SQLiteDatabase db = dbHelper.getReadableDatabase();
  5: 
  6:     SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
  7: 
  8:     qb.setTables(EarthquakeDatabaseHelper.EARTHQUAKE_TABLE);
  9: 
 10:     // 如果这是一个行查询,把结果集限制为传入的行
 11:     switch (uriMatcher.match(uri)) {
 12:     case QUAKE_ID:
 13:       qb.appendWhere(KEY_ID + "=" + uri.getPathSegments().get(1));
 14:       break;
 15:     case SEARCH:
 16:       qb.appendWhere(KEY_SUMMARY + " LIKE \"%"
 17:           + uri.getPathSegments().get(1) + "%\"");
 18:       qb.setProjectionMap(SEARCH_PROJECTION_MAP);
 19:       break;
 20:     default:
 21:       break;
 22:     }
 23: 
 24:     // 如果没有指定排序顺序,就按日期、时间顺序
 25:     String orderBy;
 26:     if (TextUtils.isEmpty(sortOrder))
 27:       orderBy = KEY_DATE;
 28:     else
 29:       orderBy = sortOrder;
 30: 
 31:     // 对底层数据库应用查询
 32:     Cursor c = qb.query(db, projection, selection, selectionArgs, null,
 33:         null, orderBy);
 34: 
 35:     // 注册当游标结果集改变时将通知的上下文ContentResolver
 36:     c.setNotificationUri(getContext().getContentResolver(), uri);
 37: 
 38:     // 返回查询结果的游标
 39:     return c;
 40:   }

(7)创建一个搜索结果的Activity。

  1: package com.paad.earthquake;
  2: 
  3: import android.annotation.SuppressLint;
  4: import android.app.ListActivity;
  5: import android.app.LoaderManager;
  6: import android.app.SearchManager;
  7: import android.content.CursorLoader;
  8: import android.content.Intent;
  9: import android.content.Loader;
 10: import android.database.Cursor;
 11: import android.os.Bundle;
 12: import android.widget.SimpleCursorAdapter;
 13: 
 14: @SuppressLint("NewApi")
 15: public class EarthquakeSearchResults extends ListActivity implements
 16:     LoaderManager.LoaderCallbacks<Cursor> {
 17: 
 18:   private SimpleCursorAdapter adapter;
 19: 
 20:   private static String QUERY_EXTRA_KEY = "QUERY_EXTRA_KEY";
 21: 
 22:   @Override
 23:   protected void onCreate(Bundle savedInstanceState) {
 24:     super.onCreate(savedInstanceState);
 25: 
 26:     adapter = new SimpleCursorAdapter(this,
 27:         android.R.layout.simple_list_item_1, null,
 28:         new String[] { EarthquakeProvider.KEY_SUMMARY },
 29:         new int[] { android.R.id.text1 }, 0);
 30:     setListAdapter(adapter);
 31:   }
 32: 
 33:   @Override
 34:   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
 35:   }
 36: 
 37:   @Override
 38:   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
 39:   }
 40: 
 41:   @Override
 42:   public void onLoaderReset(Loader<Cursor> loader) {
 43: 
 44:   }
 45: 
 46: }
 47: 

(8)更新onCreate方法以初始化Cursor Loader。

  1:   @Override
  2:   protected void onCreate(Bundle savedInstanceState) {
  3:     super.onCreate(savedInstanceState);
  4: 
  5:     adapter = new SimpleCursorAdapter(this,
  6:         android.R.layout.simple_list_item_1, null,
  7:         new String[] { EarthquakeProvider.KEY_SUMMARY },
  8:         new int[] { android.R.id.text1 }, 0);
  9:     setListAdapter(adapter);
 10: 
 11:     getLoaderManager().initLoader(0, null, this);
 12: 
 13:     parseIntent(getIntent());
 14:   }
 15: 
 16:   private void parseIntent(Intent intent) {
 17:     if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
 18:       String searchQuery = intent.getStringExtra(SearchManager.QUERY);
 19: 
 20:       Bundle args = new Bundle();
 21:       args.putString(QUERY_EXTRA_KEY, searchQuery);
 22: 
 23:       getLoaderManager().restartLoader(0, args, this);
 24:     }
 25:   }

(9)重写onCreateLoader方法。

  1:   @Override
  2:   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
  3:     String query = "0";
  4: 
  5:     if (args != null) {
  6:       query = args.getString(QUERY_EXTRA_KEY);
  7:     }
  8: 
  9:     String[] projection = { EarthquakeProvider.KEY_ID,
 10:         EarthquakeProvider.KEY_SUMMARY };
 11:     String where = EarthquakeProvider.KEY_SUMMARY + " LIKE \"%" + query
 12:         + "%\"";
 13:     String[] whereArgs = null;
 14:     String sortOrder = EarthquakeProvider.KEY_SUMMARY + "COLLATE LOCALIZED ASC";
 15:     
 17:     return new CursorLoader(this, EarthquakeProvider.CONTENT_URI, projection, where, whereArgs, sortOrder);
 18:   }

(10)配置Manifest节点,确保其中添加了Intent Filter用于DEFAULT类别中的操作。同时还需要一个meta-data标记,用于可搜索XML资源。

  1:         <activity android:name=".EarthquakeSearchResults"
  2:             android:label="Earthquake Search"
  3:             android:launchMode="singleTop">
  4:             <intent-filter>
  5:                 <action android:name="android.intent.action.SEARCH" />
  6:                 <category android:name="android.intent.category.DEFAULT" />
  7:             </intent-filter>
  8:             <meta-data 
  9:                 android:name="android.app.searchable"
 10:                 android:resource="@xml/searchable" />
 11:         </activity>

(11)添加meta-data.

  1: <application
  2:         android:icon="@drawable/ic_launcher"
  3:         android:label="@string/app_name" >
  4:         <meta-data
  5:             android:name="android.app.default_searchable"
  6:             android:value=".EarthquakeSearchReslts" />

(12)在main.xml布局中定义搜索视图。

  1: <?xml version="1.0" encoding="utf-8"?>
  2: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3:   android:orientation="vertical"
  4:   android:layout_width="match_parent"
  5:   android:layout_height="match_parent">
  6:   <SearchView
  7:       android:id="@+id/searchView"
  8:       android:iconifiedByDefault="false"
  9:       android:background="#FFF"
 10:       android:layout_width="match_parent"
 11:       android:layout_height="wrap_content" ></SearchView>
 12:     
 13:   <fragment android:name="com.paad.earthquake.EarthquakeListFragment"
 14:     android:id="@+id/EarthquakeListFragment"
 15:     android:layout_width="match_parent" 
 16:     android:layout_height="match_parent" 
 17:   />
 18: </LinearLayout>

(13)回到Earthquake Activity,并把搜索视图连接到Earthquake Activity的onCreate中的可搜索定义searchable。

  1:     @Override
  2:     public void onCreate(Bundle savedInstanceState) {
  3:         super.onCreate(savedInstanceState);
  4:         setContentView(R.layout.main);
  5:         
  6:         updateFromPreference();
  7:         
  8:         //使用Search Manager 获取与此Activity关联的SearchableInfo
  9:         SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
 10:         SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
 11:         
 12:         //将Activity的SearchableInfo与搜索视图进行绑定
 13:         SearchView searchView = (SearchView)findViewById(R.id.searchView);
 14:         searchView.setSearchableInfo(searchableInfo);
 15:     }

实例到此结束了啊。

Screenshot_2014-09-24-17-30-47

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

1. 使用Media StoreContent Provider

任何时候向文件系统添加新的多媒体文件时,还应该使用内容扫描器把他添加到Media Store中,这样,该文件就可以被其他应用程序使用。要从Media Store访问媒体文件,可以使用MediaStore类包含的Audio、Vidio和Image子类,这些子类又分别包含他们的子类,用来为每个媒体提供程序提供列名和内容URI。

Media Store将存储在设备的内部卷和外部卷上的媒体隔离开来。每个Media Store的子类都使用一下的形式为内部或者外部存储的媒体提供一个URI:

MediaStore.<mediatype>.Media.EXTERNAL_CONTENT_URI

MediaStore.<mediatype>.Media.INTERNAL_CONTENT_URI

2. 使用Contacts Contract Content Provider

(1)Contacts Contract Content Provider 简介

使用一个三次数据模型来存储数据,将数据和联系人联系起来,并把同一个人联系信息聚集起来,这是通过下面ContactContracts子类实现的:

image

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

(2)读取联系人详情。

首先需要Manifest配置:<uses-permission android:name=”android.permission.READ_CONTACTS” />

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

(3)使用Intent创建和选择联系人

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

(4)直接修改和增强联系人详情

首先需要Manifest配置:<uses-permission android:name=”android.permission.WRITE_CONTACTS” />

Contacts Contract Content Provider的可扩展特性允许向存储为RawContact的任意账户添加任意的Data表行。

3. 使用Calendar Content Provider

(1)查询Calendar

要访问Calendar Content Provider,必须在应用程序的清单文件中包含:<uses-permission android:name=”android.permission.READ_CALENDAR” />

使用ConstentResolver,通过他们的常量CONTENT_URI常量来查询任何Calendar Provider表。每个表都在CalendarContract类中公开:

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

(2)使用Intent创建和编辑日历项

 

Q&A

【错误描述】
    在用Eclipse开发过程中,为了兼容Android2.2和4.0以上版本,我在使用Notification类时做了2个版本的代码,代码根据系统版本不同执行相应模块,结果,等我输完代码,发现系统提示了一个这么的错误。

【原因分析】
    不详,可能和Run Android Lint有点关系吧。就是创建项目时,我们设置了最低版本API Level,比如我的是8,因此,Eclipse检查我调用的API后,发现版本号不能向低版本兼容,比如我用的“Notification.Builder”是Level 11 以上才有的,自然超过了8,所以提示错误。

【解决方案】
   右键点击项目->Android tools ->Clear Link Markers.即可临时解决,但是如果调试用的模拟器是低版本的,则在调试完后还有这个错误。
  如果把manifest文件中的user-sdk的android:minSdkVersion改为报错的那个高版本就没事。比如下面:
<uses-sdk
        android:minSdkVersion="11"   //这个之前是8
        android:targetSdkVersion="17" />

P302

posted @ 2014-09-25 00:20  今天昔水  阅读(1116)  评论(0编辑  收藏  举报