从头开始构建和访问 Android 内容提供程序的绝对初学者指南






4.84/5 (17投票s)
Android 中的内容提供器(Content Provider)从未如此简单。
图 1.1 Android 中的内容提供器和内容解析器
1. 背景
1.1 为什么需要内容提供器和内容解析器
当你在 Google 搜索中输入“Android Content Provider”时,你会得到大量关于 Android 内容提供器的结果、教程和资源。这些资源通常以内容提供器的定义以及如何使用它作为开篇。
如果你是通过 Google 或其他参考资料找到这篇文章的,你可能会忍不住问“为什么又来一个内容提供器的教程?”你可能会想关闭这个标签并继续前进。但如果你真的读了上面的几行字,并且仍在阅读这一行,那么我向你保证,你不会在不收藏此标签的情况下离开。
简而言之,内容提供器是 Android 允许应用程序与其他应用程序共享数据的方式。
在我深入探讨内容提供器的一些技术方面之前,让我们首先尝试回答“为什么需要内容提供器?为什么一个应用程序需要另一个应用程序的数据?”
现在假设你想在预约应用中设置一个预约。你选择的那一天是你们国家的假期(如果你在印度,这样的假期会很多)。这会打乱你的日程。如果你的内置日历应用能与你分享假期,并且你的预约调度器能就此警告你,那该多好?当然,理想情况下它应该这样做。但是,除非你的日历应用的数据可以被你的预约应用获取,否则你创建的预约应用怎么会有这个功能呢?
一旦你创建了一个预约,你希望向所有相关人员发送自动通知邮件。想象一下,如果每个电子邮件 ID 都必须在 Gmail 中搜索并粘贴到你的应用程序中,那你的应用程序会变成什么样?那时还会有人使用你的应用程序吗?用户会期待什么?他会期待你的应用程序自动从联系人中获取电子邮件 ID,以减少他的工作。在这种情况下,你的应用程序又需要来自联系人的数据。再次——需要内容提供器。
——“好吧,我的应用程序没有做以上任何事情,我也不想写一个预约应用程序。走了。”
如果你想说这句话,请稍等一下。你的应用程序会访问图片吗?它会从存储中选择图片吗?如果会,当文件服务是另一个系统应用程序时,你如何从应用程序访问图片?答案是内容提供器。
——“不,我没有图片应用程序。我开发游戏。我所有的游戏组件都随我的应用程序一起部署。走了。”
等等!请稍等。你没有社交分享分数的功能吗?如果有,你真的不想向玩家提供向他的游戏伙伴发送电子邮件的选项吗?:) 当然你想,并且你需要为此获取联系人。你可能还需要提供一个界面,让玩家可以利用日历提供器的数据组织一场锦标赛。
——“但是等等,到目前为止,你只是阐明了学习如何使用其他应用程序的内容对我来说有多重要,这个主题不应该是关于内容消费者或其他什么的吗?我不需要知道如何创建记事本软件来打字,看在上帝的份上,我只需要知道如何使用它!”
是的,你部分正确。我们应该讨论消费内容。这就是所谓的**“内容解析器”**,它就像**内容提供器**的客户端应用程序。所以,是的,学习内容解析器很重要。但是如果你是一名应用程序开发人员,你可能会开发很多应用程序,你肯定会希望利用你另一个应用程序已经收集的一些数据来定制新应用程序的内容。例如,假设你有一个游戏,一个特定的用户购买了能量助推器,你难道不认为你的新免费增值应用程序提示能量助推器而不是购买更多生命会更有意义吗?或者,例如,你有一个“卡路里计量器”应用程序,用户输入他的卡路里摄入量来跟踪他的体重和卡路里摄入量。现在你正计划发布一个名为“步行计量器”的新应用程序。它有助于计算用户行走的距离,并根据此计算燃烧的卡路里。如果能从你之前的应用程序中获取用户的体重、他的每日卡路里摄入量,并告诉他成功减重了多少,那该多好?
所以内容提供器开启了令人着迷的功能。它节省了用户将相同信息提供给不同应用程序的宝贵时间,它使你的应用程序能够访问大量数据而无需专门获取数据,并且它为你提供了在特定领域发展应用程序的惊人能力!
如果你从未听说过或使用过内容提供器,那么在阅读完背景介绍后,你一定会想学习它,并且想从一篇让你相信它值得学习的文章中学习。如果你已经了解内容提供器并阅读了背景部分,那么你这样做是因为你对这个主题仍然有需要学习的地方。
那么,在了解了什么是内容提供器和内容解析器,以及它们如何有用和为什么使用之后,是时候最终揭示我们想用它们做什么了?
有很多教程会展示如何编写和使用内容提供器。但我经常惊讶于这些教程深度不足,无法真正帮助用户理解方法的具体技术细节,以及如何涵盖其他场景?例如,你很少会遇到一篇文章告诉你如何从多个表中获取数据?因此,在本教程中,我们的重点是帮助你从头开始构建自己的内容提供器和内容解析器。我们将提供逻辑,然后分解步骤,以便你可以自己构建它们。
所以本教程将保持简单、紧凑,并将为我们的OpinionMiningApp实现 ContentProvider。
1.2 内容提供器和内容解析器的正式介绍
回顾我们学习Android 数据库和记录处理时,我们知道 Android 支持两种类型的存储:内部存储和外部存储。Android 在其文件系统中为所有应用程序分配 100MB 到 500MB 的空间。当一个应用程序选择将其数据存储在内部存储中时,数据在设备和开发环境中对用户都是隐藏的,除非设备被 root。现在,当一个应用程序选择将其数据存储在内部存储中时,它的数据不能被其他应用程序使用。这有点像在沙盒中提供数据服务。此功能是 Android 最重要的安全功能之一。
但是当应用程序选择将数据存储在外部存储(在任何 SD 卡子目录中)时,该数据可以被其他应用程序甚至用户访问。
因此,在本次讨论中,我们将重点关注内部存储,否则其他应用程序无法共享。
这里还需要提及一点。我们知道 Android 提供了一个名为 SharedPreferences 的概念,通过它可以在应用程序之间共享偏好设置。但这有几个限制。其中一个主要限制是,共享数据的应用程序需要具有父子关系才能以非黑客模式共享 SharedPreferences。
因此,我们只剩下通过内容提供器(Content Provider)和内容解析器(Content Resolver)接口来利用一个应用程序数据的唯一选项。
图 1.1 概括了我们将要学习的内容。如果另一个应用程序使用内容提供器(Content Provider)提供对其数据的访问,则一个应用程序可以通过使用内容解析器(Content Resolver)接口查询另一个应用程序的数据、在应用程序数据中插入新记录、更新或删除记录。
2. 内容提供器
当你学习第一个 C 语言程序时,你学过如何创建头文件吗?当你学习 .Net 时,你学过如何构建 dll 文件吗?或者当你开始学习 Android 时,你学过如何创建可重新分发的 jar 文件组件吗?
如果以上问题的答案是否定的,那么问我们为什么要从学习内容提供器开始课程就非常有意义了?
然而,就像你不知道头文件及其作用就无法编写 C 语言程序,以及不知道头文件要点就无法构建简单的 Android 应用程序一样,我们不了解内容提供器的基础知识也无法使用内容解析器。
内容提供器是一个扩展 android.content.ContentProvider 类的类。所以你可以创建一个名为 MySimpleContentProvider 的新类,并使其继承 android.content.ContentProvider. 一旦你这样做,它会提示你定义由于继承而需要重写的方法。完成这些后,你将看到如下所示的类结构。
public class MySimpleContentProvider extends ContentProvider
{
@Override
public int delete(Uri arg0, String arg1, String[] arg2) {
// TODO Auto-generated method stub
return 0;
}
@Override
public String getType(Uri arg0) {
// TODO Auto-generated method stub
return null;
}
@Override
public Uri insert(Uri arg0, ContentValues arg1) {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean onCreate() {
// TODO Auto-generated method stub
return false;
}
@Override
public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3,
String arg4) {
// TODO Auto-generated method stub
return null;
}
@Override
public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
// TODO Auto-generated method stub
return 0;
}
}
上述实现阐明了 ContentProvider 是什么。它是一个提供 onCreate、update、query、insert、getType、delete 方法的类。那么这些方法是做什么的呢?
理解这一点有三种方式。第一种是阅读大量文档来了解它们的作用;第二种是先从一个例子开始,理解它们的作用;第三种是学习一种高效且结构化的技巧来了解它们。我将向你推荐第三种技术。所以为了了解它们的作用,只需将鼠标悬停在 onCreate 方法上,会弹出一个描述窗口。按下 **F2** 键。窗口将固定下来。请看下面的图。
图 2.1 查看 ContentProvider 方法的文档
Android 最好的优点之一是它的文档非常完善,我建议你在查阅外部文档之前,始终以这种方式查看方法、类或构造函数的定义和描述。
当客户端初始化任何扩展 ContentProvider 的类的实例时,会调用扩展类的 onCreate 方法。你可以在此代码中执行任何初始化操作,例如打开数据库连接或设置文件连接。但如前所述,请不要执行耗时或耗资源的操作。例如,此类操作包括调用 webservice 获取数据,或尝试获取大型数据库内容作为 String[]。由于 onCreate 是同步调用的,它会阻塞客户端直到完成。因此,你必须避免在此处进行资源密集型操作。
查看此文档还提示您,大多数数据库操作必须避免,直到调用 **query** 方法。所以让我们通过其文档来理解它做了什么。
图 2.2 query 方法文档
2.1 理解 ContentProvider query() 参数
query 是客户端请求数据的方法。例如,让我们考虑我们在Android 数据处理理解中用于 SQLite 部分的 OpinionData 表。
所以,假设如果 OpinionMining 应用程序要通过内容提供器公开其数据,供 ShareSimple 应用程序请求意见数据,那么在 OpinionMiningContentProvider 中,查询将采用以下几种可能的格式。
1) Select * from OpinionDataTable
2) Select **[Words,Weight]** from OpinionDataTable
3) Select Words,Weight from OpinionData table [where ( (Weight> 0) AND (Word like 'a%')) ] // 提取以“a”开头的正面评价词
4) Select Words,Weight from OpinionData table where Weight> 0 [order by Words asc]
5) Select * from OpinionDataTable where [No=1]
回想一下,在我们对 Android 数据处理的理解中,我们专注于开发通用性质的方法。也就是说,我们尝试以相似的格式开发文件、XML、SQLite 等不同存储类型的方法。
我想现在你已经理解了这种方法泛化的目的。
回到当前的讨论,正如你所看到的,任何查询都可能是上述格式之一,客户端可以从任何一种格式中选择。然而,该方法必须提供适用于任何格式的规定。
2.1.1 投影
在 2) 中,我们看到客户端希望了解特定列的信息。这称为投影。因此,在这种情况下,客户端的投影数据将是
String[]projection=new String[]{"Word","Weight"};
在内容提供器中,要根据用户的输入形成动态查询,我们必须采用类似于下面代码的逻辑。
String[]projection=arg1; // see document and observe your method's definition
String p="";
if(projection.length<=0)
{
p="*";
}
else
{
p=projection[0];
for (int i=1;i<projection.length;i++)
{
p=p+","+projection[i];
}
}
String Query="Select "+p+" from OpinionDataTable"
2.1.2 选择
查询方法的第三个参数是选择。这里客户端所要做的就是发送除“where”一词之外的整个字符串。所以如果客户端想根据与 3) 中完全相同的条件选择 OpinionDataTable,那么参数字符串将是“where ( (Weight> 0) AND (Word like 'a%'))”。
这可以通过两种方式完成:客户端可以保留
String selection= "where ( (Weight> 0) AND (Word like 'a%'))";
String[]selectionArgs=null;
因为字符串选择中没有变量。或者它可以在选择查询中留下变量的范围,并通过 selectionArgs 数组指定值。
String selection= "where ( (Weight> ?) AND (Word like ?))";
String []selectionArgs=new string[]{"0","'a%'"};
这里的“**?**”将通知内容提供者 selectionArgs 中的值应该附加到哪里。
现在请记住,在编写你的 ContentResolver 时,你可能并不总是确切知道相应的 ContentProvider 是如何构建的,因为那可能是一个第三方 ContentProvider。所以你必须假设普遍情况下,选择参数以第二种方式发送。然而,在编写你自己的 ContentProvider 时,你必须为这两种情况都提供支持。
因此,我们修改后的内容提供器(Content Provider),也考虑了选择(selection),如下所示。
/************************************* Handling Projection*********************/
String[]projection=arg1; // see document and observe your method's definition
String p="";
if(projection.length<=0)
{
p="*";
}
else
{
p=projection[0];
for (int i=1;i<projection.length;i++)
{
p=p+","+projection[i];
}
}
/******************************* Projection part over***************************************/
/******************************** Handling Selection*********************************/
String wherePart="";
String selection=arg2;
String []selectionArgs=agr3;
if(!selection.isEmpty() && selectionArgs.isEmpty())// if client has sent where part but no arg for that
{
wherePart=selection;
}
else
{
wherePart=selection.replace("?","%s");// In order to Utilize String.Format
wherePart=String.format(wherePart,(Object[])(selectionArgs));// Because String.Format expects second
//argument to be explicitly of Object[]
}
String Query="Select "+p+" from OpinionDataTable where "+wherePart;
2.1.3 排序顺序
对于客户端,你只需要发送变量和排序顺序,而不需要在 orderBy 变量中构造“order by”这个词。所以如果 (5) 是客户端的查询,
String orderBy=" Words ASC";
在 ContentProvider 端,查询的形成现在将修改为将 orderBy 字符串合并到查询字符串中。
/************************************* Handling Projection*********************/
String[]projection=arg1; // see document and observe your method's definition
String p="";
if(projection.length<=0)
{
p="*";
}
else
{
p=projection[0];
for (int i=1;i<projection.length;i++)
{
p=p+","+projection[i];
}
}
String Query="Select "+p+" from OpinionDataTable";
/******************************* Projection part over***************************************/
/******************************** Handling Selection*********************************/
String wherePart="";
String selection=arg2;
String []selectionArgs=agr3;
if(!selection.isEmpty() && selectionArgs.isEmpty())// if client has sent where part but no arg for that
{
wherePart=selection;
}
else
{
wherePart=selection.replace("?","%s");// In order to Utilize String.Format
wherePart=String.format(wherePart,(Object[])(selectionArgs));// Because String.Format expects second
//argument to be explicitly of Object[]
Query="Select "+p+" from OpinionDataTable where "+wherePart;
}
//////////////////////////////// Selection part over///////////////////////////
/************* Handling order by part******************************************************/
String orderBy=arg5;
if(!orderBy.isEmpty())
{
Query="Select "+p+" from OpinionDataTable where "+wherePart+" order by "+orderBy;
}
一旦查询形成,你可以以不同的方式和针对不同的数据类型来利用它。现在有几件非常有趣的事情需要注意。
假设你使用的是平面文件数据库或 XML 数据库或 SharedPreferences,那么如 1) 到 5) 所示的关系查询就不存在了。你需要通过编程逻辑执行操作。只有在 SQLite 的情况下,你才能从选择参数形成查询并执行。但有趣的是,如果你使用 SQLite,那么你将使用以下代码。
SQLiteDatabase db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READONLY);
Cursor dbCursor = db.query(tableName, null, null, null, null, null, null);
其中 db.query 具有以下参数。
图 2.3:SQLiteDatabase 对象的 Query 方法
所以你可以看到,你可以简单地执行查询,而无需通过上述逻辑将参数组合成一个查询。
String [] projections=arg1;
String selection=arg2;
String[]selectionArgs=arg3;
String groupBy=null;
String having=null;
String orderBy=arg4;
Cursor dbCursor = db.query("OpinionMiningTable", projection, selection, selectionArgs, null, null, orderBy);
就是这么简单!
但既然如此,我们为什么要学习从参数构建查询字符串的技术呢?好吧,考虑以下查询
"Select A.No,B.Name from A,B where A.No=B.No"
其中 A 和 B 是分别具有 No、{No,Name} 列的表。你如何在需要单个表名的 SQLite 中构建查询?
在这种情况下,你需要使用 **rawQuery()**,它可以执行涉及任意数量的表、视图或两者兼有的查询。
所以:
Cursor dbCursor=db.rawQuery(queryString,null);
将返回相同的结果。
2.1.4 统一资源标识符 (URI)
请注意我们在 **ContentProvider** 部分的讨论。你可能会注意到,在构建查询方法时,我们默认将 TableName 假定为 "OpinionMiningTable"。然而,你的内容提供器可能处理多个表。另请注意,在 SQLiteDatabase db 声明中,我们有一个名为 **DB_PATH** 的参数,如果 OpinionMining 使用内部存储,它将始终是 "**data/data/com.integratedideas.opinionmining/databases/**"。如果它在 Pictures 目录内的 OpinionMining 目录中使用外部存储,它将是 "sdcard/emulated0/Pictures/OpinionMining"。它可能为此使用任何其他目录或子目录。因此,通过 Uri,客户端应该指定数据库的确切位置路径。
从技术上讲,URI 字段的格式应该是
content://<authority>/<path>/<OPTIONAL_id>
其中
content:// 是关键字,必须是常量
<authority>:是提供内容的包名。所以在上面的例子中,<authority> 必须简单地替换为 com.integratedideas.opinionmining。在任何正常情况下,你都必须以相同的方式解释 authority。
<path>:通常是 <database name>/<TABLE_NAME>。现在 ContentResolver 不应该知道 ContentProvider 在哪里以及如何存储数据。所以 <path> 简单地表示 SQLite 中的数据库名称,平面文件数据中的文件名称,XML 数据中的 XML 文件名称,以及 SharedPreferences 中的 SharedPreferences 名称。
<OPTIONAL_id>:它是特定的记录编号。假设客户端想提取编号为 5 的 OpinionDataTable 记录,那么 URI 就变成了
content://com.integratedideas.opinionmining/OpinionSQLiteDatabase/OpinionWordTable/5
其中 OpinionSQLiteDatabase 是存储 OpinionWordTable 表的数据库名称。由于查询方法已经支持选择,因此这个字段通常不是必需的。在任何情况下,ContentResolver 类都必须解析 URI,提取字符串,提取字段,并相应地形成查询。
2.2 开发内容提供器
我们现在将为 OpinionMining 项目的 SQLite 数据库开发一个内容提供器。首先从此处下载项目并导入到 Eclipse 中。
现在创建一个名为 **ContentProviderForSQLite** 的新类,并使其继承 ContentProvider。
最后更新查询方法。删除未使用的方法的类如下所示。
public class ContentProviderForSQLite extends ContentProvider
{
public static String DB_PATH = "/storage/emulated/0/Pictures/OpinionMining/";
public static String DB_NAME = "%s";// This has to come through table name parameter
private SQLiteDatabase db;
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
{
//////////////////////// Parsing Parameters from Uri////////////////////////////
String id=null;
String a=uri.getAuthority();
List<String> pathSegs=uri.getPathSegments();
if(pathSegs.size()>2)
{
id=uri.getLastPathSegment();
if(selectionArgs.isEmpty())
{
selectionArgs=new String[]{id};
selection="No";
}
else
{ selection=selection+" AND No=?";
String [] tmp=new String[selectionArgs.length+];
for(int i=0;i<selectionArgs.Length;i++)
{
tmp[i]=selectionArgs[i];
}
tmp[tmp.length-1]=id;
}
}
DB_NAME=String.format(DB_NAME, pathSegs.get(0));
String tableName=pathSegs.get(1);
//////////////////////////////// Executing and returning Query/////////////
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READONLY);
Cursor dbCursor = db.query(tableName, projection, selection, selectionArgs, null, null, sortOrder);
return dbCursor;
// TODO Auto-generated method stub
}
}
正如你所看到的,ContentProvider 的作用是解析 URI,并从 URI 中获取授权、数据库路径和表名。如果 URI 包含 ID,那么它也应该提取该 ID。
现在当你看到这个实现时,它看起来与你网上看到的许多,或者我应该说大多数关于这个主题的资源都太不一样了,它们从不会告诉你如何为大多数实际目的构建 ContentResolver 类。我们在 2.1.1 中学到的内容将非常有帮助。
2.2.1 形成查询方法
String id=null;
String a=uri.getAuthority();
List<String> pathSegs=uri.getPathSegments();
if(pathSegs.size()>2)
{
id=uri.getLastPathSegment();
}
所以如果你的 Uri 没有 id,它的格式是:DatabaseName/tableName。如果它有 id,那么是:database/table/id 格式。getPathSegments() 返回路径的所有部分。所以如果你有 id,就会有超过两个字符串(第三个 id 字符串),如果你在 url 中有 id,你可以通过提取 getLastPathSegment() 来提取。
现在,如果 Id 存在,则意味着你的选择标准(如果有的话)应该被修改。
假设您的 Id 不是唯一的,所有带 'a' 的单词的 Id=1,所有带 'b' 的单词的 Id=2,依此类推,选择标准是 "where abs(weight)>2 And Id=1"
但由于用户在 Id 中指定了 1,他的选择字段将是 "abs(weight)>?",selectionArgs=new String[]{"2"}。因此在 ContentProvider 中,这需要修改,并且 Id 必须集成到其中,使得选择变为
"abs(weight)>? And No=?" 和 selectionArgs=new String[]{"2",Id}
另一方面,如果用户没有指定任何选择标准(如果指定了记录号,这是一种常见情况),那么选择和 selectionArgs 将如下所示
selection="No=?" selectionArgs=new String[]{Id};
所以在第一种情况下,整个选择字符串必须修改,并且必须声明一个新的数组,其中新的 Id 值必须附加到现有数组内容中。第二种情况是直接创建新的选择字符串以及 selectionArgs 数组。
if(selectionArgs.isEmpty())
{
selectionArgs=new String[]{id};
selection=" No=?";
}
else
{ selection=selection+" AND No=?";
String [] tmp=new String[selectionArgs.length+];
for(int i=0;i<selectionArgs.Length;i++)
{
tmp[i]=selectionArgs[i];
}
tmp[tmp.length-1]=id;
}
Now last part is to extract Database and table name and executing the query.
DB_NAME=String.format(DB_NAME, pathSegs.get(0));
String tableName=pathSegs.get(1);
//////////////////////////////// Executing and returning Query/////////////
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READONLY);
Cursor dbCursor = db.query(tableName, projection, selection, selectionArgs, null, null, sortOrder);
2.2.2 关于从多个表选择数据的注意事项
如果需要从多个表中获取数据,创建视图然后从视图中选择数据始终是一个明智的选择。以上面讨论的表 A 和 B 为例。使用 SQLite 浏览器或使用 createTable 查询,我们可以创建一个视图
create view ABmyView_view as ( select A.Id, B.Name from A,B where A.Id=B.Id)
然后在 URI 中,在 Uri 的 <tableName> 部分传递视图名称。“Select * from ABmyView”查询将返回与括号中指定的查询完全相同的结果。
要将一组数据插入多个表,您必须为每个表调用 insert 方法,并提供该表特定的 Uri 和参数。
2.2.2 暴露内容提供器
OpinionMining 项目中有这么多类。此外,应用程序包设置为在应用程序启动时启动 MainActivity。但是,主活动不扩展 ContentProvider,也不应该扩展(因为你的目标不是启动另一个应用程序,而是获取它的数据)。那么你如何暴露 ContentProvider 呢?
嗯,通过清单文件。所以编辑你的 AndroidManifest.xml,并在 application 标签内、任何 activity 标签之外添加以下标签。我总是倾向于将其放在 </application> 标签之前,以确保它位于正确的位置。
<provider android:name=".ContentProviderForSQLite"
android:authorities="com.integratedideas.opinionmining"
android:exported="true"
android:grantUriPermissions="true" />
android.name 是你的类名。请始终记住,扩展 ContentProvider 的类必须是 public 的。
android.authorities 应该是包名。虽然从技术上讲它可以是任何名称,但包名可以避免混淆。
exported=true 使其对其他应用程序可见。
grantUriPermission 允许其方法通过 uri 调用。
为了确保您的清单文件正确无误,以下是其完整内容。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.integratedideas.opinionmining"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="14" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.integratedideas.opinionmining.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider android:name=".ContentProviderForSQLite"
android:authorities="com.integratedideas.opinionmining"
android:exported="true"
android:grantUriPermissions="true" />
</application>
</manifest>
就是这样。现在只需将其安装到您的设备中。
3. 内容解析器
当内容提供器(ContentProvider)暴露时,它会全局暴露给整个 Android 系统。当你声明任何活动类时,它默认可以通过调用 **getContentResolver()** 方法获取 ContentResolver() 的实例,该实例可以访问任何内容提供器(ContentProvider)的内容。因此,要利用上述 OpinionMining 内容提供器(ContentProvider)的服务,你只需在任何客户端应用程序中从你想要测试的部分使用以下几行代码来测试它;
try
{
String s="content://com.integratedideas.opinionmining/OpinionSQLiteDatabase/OpinionDataTable";
Uri uri=new Uri.Builder().build().parse(s);
Cursor cursor = getContentResolver().query(uri, new String[]{"Word", "Weight"}, "Weight > ?", new String[]{"0"}, "Word ASC");
while(cursor.moveToNext())
{
String word=cursor.getString(0);
String weight=cursor.getString(1);
Log.i(word,weight);
}
}catch(Exception ex)
{
}
尽管 ContentResolver 中还有更多的标志和内容需要了解,但请相信,上述几行代码对于大多数涉及从 ContentProvider 获取数据的应用程序来说已经足够了。
但是请注意,ContentResolver 只能从有效的 Context 中实例化。所以 getContentResolver() 调用只能在 Activity 类中进行。但是如果你想从非 Activity 类中的方法中调用它怎么办?
在这种情况下,你必须在类中拥有一个 Context 对象,该对象必须通过活动(Activity)实例化,并提供活动的上下文(Context)。然后,可以通过该上下文调用 getContentResolver()。
public class MyClass
{
public String someMethod(Context context)
{
context.getContentResolver();
// Other code
}
}
public void onCreate()
{.
.
.
MyClass m=new MyClass();
m.someMethod(this);
}
4. 使用数据库记录修改方法
4.1 InsertMethod
首先让我们看看自动创建的插入方法以及它的样子。
@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO Auto-generated method stub
return null;
}
我们已经了解了 URI 部分。现在我们需要理解 **ContentValues** 部分。
在理解 ContentProvider 中 Insert 方法应该如何构建以及 ContentResolver 中应该如何解析它之前,让我们借用我们在OpinionMining App 的 4.2.4 节中开发的 InsertMethod 记录。
public int InsertRecord(String tableName,String []newData)
{
//db=this.getReadableDatabase();
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READONLY);
Cursor dbCursor = db.query(tableName, null, null, null, null, null, null);
String[] columnNames = dbCursor.getColumnNames();
db.close();
/// Now Create Content Based On Columns ..............
ContentValues values = new ContentValues();
for(int i=0;i<columnNames.length;i++)
{
values.put(columnNames[i], newData[i]);
}
////////// Once Content is created, reopen database to insert values///
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READWRITE);
db.insert(tableName, null, values);
db.close();
/////////////////////////////////////
return 1;
}
很明显,在插入时,您需要提供一组键值对,其中键表示列名,值表示它们的新值。
客户端可能并不总是知道表的列名。因此,客户端理想情况下应该调用查询方法并从游标中获取列名。然后它必须将新值与列名附加,就像这个循环一样
ContentValues values = new ContentValues();
for(int i=0;i<columnNames.length;i++)
{
values.put(columnNames[i], newData[i]);
}
一旦 URI 和 ContentValues 在插入方法之前可用,它就应该执行以下部分。
////////// Once Content is created, reopen database to insert values///
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READWRITE);
db.insert(tableName, null, values);
db.close();
/////////////////////////////////////
所以我们为 OpinionMining 的 SQLite 部分开发的 insert 方法现在将分为两部分:第一个块应该在客户端完成,第二个块在 insert 方法中。但是 insert 方法也应该包含我们上面开发的 URI 解析部分,以便从 URI 部分获取表名和数据库名。此外,由于这不是选择,URI 将只包含两个部分。
所以我们的 ContentProvider 的 **insert** 方法如下所示
@Override
public Uri insert(Uri uri, ContentValues values) {
/************* Uri Parsing****************/
List<String> pathSegs=uri.getPathSegments();
DB_NAME=String.format(DB_NAME, pathSegs.get(0));
String tableName=pathSegs.get(1);
////////////////////////////////////////////
/********************* Executing Database Insert Method***********/
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READWRITE);
int id=(int)db.insert(tableName, null, values);
db.close();
///////////////////////////////////////////////////////////////
/************* Insert returns Id of New Row, append it in incoming Uri and return***/
Uri newUri= Uri.parse(uri.toString()+"/"+id);
return newUri;
// TODO Auto-generated method stub
}
而客户端部分将是
////////////////// Insert Query//////////////////////////////////////
Log.i("Insert Query","calling");
String []newRow=new String[]{null,"tension","-2"};
String s="content://com.integratedideas.opinionmining/OpinionSQLiteDatabase/OpinionDataTable";
Uri uri=new Uri.Builder().build().parse(s);
Cursor cursor = getContentResolver().query(uri, null, null,null, null);
String []columns=cursor.getColumnNames();
String cols="";
ContentValues values=new ContentValues();
for(int i=0;i<columns.length;i++)
{
cols=cols+ " "+columns[i];
values.put(columns[i], newRow[i]);
}
Log.i("Columns are:",cols);
uri=getContentResolver().insert(uri, values);
Log.i("Result of Insert",uri.toString());
///////////////////////////////////////////////////////////////////////////////
这是 Logcat 截图,显示了插入查询在内容解析器中工作的过程。
图 4.1:插入查询结果
4.2 更新与删除
一旦理解了查询和插入方法,其余的方法就变得容易理解得多。以下是更新方法的工作方式
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs)
{
// TODO Auto-generated method stub
/************* Uri Parsing****************/
List<String> pathSegs=uri.getPathSegments();
DB_NAME=String.format(DB_NAME, pathSegs.get(0));
String tableName=pathSegs.get(1);
////////////////////////////////////////////
/************ Perform Update*************/
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READWRITE);
int n=db.update(tableName,values, selection, selectionArgs);
//db.update(table, values, whereClause, whereArgs)
db.close();
////////////////////////////////////////
return n;
}
我想现在你对上述查询的形成应该没有什么不理解的地方了。更新的客户端部分也相当简单。它涉及传递新值和选择参数。
假设我想更改我们在上一小节中插入的“tension”这个词的权重。我们的 SQL 查询将是
update OpinionDataTable set ( Weight=-4) where Word like 'tension'
因此
String selection="Word like ?";
//String[]selectionArgs=new String[]{"'tension'"};
ContentValues updateValue=new ContentValues();
updateValue.put("Weight",-4);
n=getContentResolver().update(uri, updateValue,selection,selectionArgs);
Log.i("Number of Rows Affected=",""+n);
这是删除方法
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// TODO Auto-generated method stub
/************* Uri Parsing****************/
List<String> pathSegs=uri.getPathSegments();
DB_NAME=String.format(DB_NAME, pathSegs.get(0));
String tableName=pathSegs.get(1);
////////////////////////////////////////////
/************* Now Perform Delete Operation*********/
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READWRITE);
int n=db.delete(tableName, selection, selectionArgs);
db.close();
//////////////////////////////////////////////////////
return n;
}
并在客户端中调用它
/////////////////////// Delete Query///////////////
String selection="Word like ?";
String[]selectionArgs=new String[]{"tension"};
int n=getContentResolver().delete(uri, selection, selectionArgs);
Log.i("No of rows deleted",""+n);
图 4.2:从 ContentResolver 调用 ContentProvider 方法的结果
5. Android 就绪内容提供器
我们学习了内容提供器(ContentProvider)的强大功能,以及它如何帮助访问已经从用户那里收集并存在于设备中其他应用程序的数据。Android 提供了大量的“应用程序”。例如,联系人、日历、闹钟、通话记录服务等。如果能够随时访问这些数据,那不是很好吗?
Android 让这一切变得非常容易。它有一个名为 **provider** 的包。它通过提供器类暴露了几个数据。那么,有哪些现成的内容提供器可以使用呢?
你所要做的就是在你的任何方法中,例如在一个 Activity 类的 onCreate 方法中,输入
android.provider
Eclipse 会弹出所有可用的提供器。请参阅图 5.1 查看。
图 5.1:现成的 Android 内容提供器
UserDictionary、Browser、ContactsContract、Settings、AlarmClock、CalenderContract、MediaStore 都是一些内容提供器。所以你可以使用它们。在本节中,我们将使用 ContactContracts,它提供来自联系人的数据。
5.1 使用联系人内容提供器
要使用 provider 包中的任何 ContentProvider,你需要通过 AndroidManifest.xml 为你的应用程序提供权限。要使用联系人以及能够读取和修改联系人,你应该提供以下权限
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
联系人的 Uri 是
content://com.android.contacts/contacts
那么我们可以获取哪些字段的详细信息呢?我们已经学会了如何调用 ContentProvider 的 query 方法。我们也知道查询会返回游标,并且可以从游标中获取列名。
所以我们可以编写一个代码块来获取联系人的列名。
Cursor cur = getContentResolver().query(Uri.parse(content://com.android.contacts/contacts), null, null, null, null);
String[]columns=cur.getColumnNames();
String s="";
//ContactsContract.CommonDataKinds.
for(int i=0;i<columns.length;i++)
{
s=s+columns[i]+" ";
}
Log.i("Columns",s);
然而,现阶段一个重要的问题是,我们到底应该如何知道 URI 呢?我们是否需要参考一些外部文档?
完全没有。Android 中的每个 ContentProvider 类都带有一个 Uri 类型的静态 **CONTENT_URI** 字段。你所要做的就是将其传递给 query 或其他方法的 Uri 字段。
所以
Cursor cur = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
这也会返回所有关于联系人的数据。请看下面的 LogCat 跟踪图像。
图 5.2:获取联系人列名
有趣的是,你可以看到一个名为 **display_name** 的列,它表示联系人的姓名。但是你找不到像 phone_number 或 email 这样的内容。这是因为它们都存储在不同的表中。由于 ContentProvider 的固有性质,不可能从多个表中获取数据。所以我们所要做的就是检查这个人是否有 phone_number,如果有,则通过当前 ID 从电话数据中获取详细信息。
因此,从逻辑上讲,如果你想提取所有联系人、他们的姓名、电话号码和电子邮件地址,你将需要编写嵌套操作。打开联系人游标,查找他是否有号码,如果有,则打开电话表并查找电话号码,然后打开电子邮件表并根据 ID 选择数据。
这里有一个很棒的方法可以返回你的所有联系人(姓名、电话、电子邮件)。但请记住,由于三重嵌套的 while 循环,这是一个非常耗时的过程,你必须使用 Executor 或 AsyncTask 来执行它们。
public ContactPersonDetails[] FetchAllContacts()
{
ArrayList<ContactPersonDetails> allContacts=new ArrayList<ContactPersonDetails>();
ContactPersonDetails person=new ContactPersonDetails();
ContentResolver cr = context.getContentResolver();
String [] projections=new String[]{ContactsContract.Contacts._ID,ContactsContract.Contacts.DISPLAY_NAME,ContactsContract.Contacts.HAS_PHONE_NUMBER,ContactsContract.CommonDataKinds.Phone.NUMBER,ContactsContract.CommonDataKinds.Email.DATA};
Cursor cur = cr.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
if (cur.getCount() > 0)
{
while (cur.moveToNext())
{
String id = cur.getString(
cur.getColumnIndex(ContactsContract.Contacts._ID));
person.ID=id;
person.Name = cur.getString(
cur.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
person.Phone="";
if (Integer.parseInt(cur.getString(cur.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER))) > 0)
{
try{
Cursor pCur = cr.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID +" = ?",
new String[]{id}, null);
while (pCur.moveToNext())
{
person.Phone = pCur.getString(
pCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
}
pCur.close();
}catch(Exception ex)
{
}
}
try
{
Cursor emailCur = cr.query(
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
null,
ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?",
new String[]{id}, null);
while (emailCur.moveToNext())
{
// This would allow you get several email addresses
// if the email addresses were stored in an array
person.Email = emailCur.getString(
emailCur.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA));
String emailType = emailCur.getString(
emailCur.getColumnIndex(ContactsContract.CommonDataKinds.Email.TYPE));
}
emailCur.close();
}catch(Exception ex)
{
}
allContacts.add(person);
person=new ContactPersonDetails();
}
}
ContactPersonDetails [] contactsArray=new ContactPersonDetails [allContacts.size()];
contactsArray=allContacts.toArray(contactsArray);
return contactsArray;
}
其中 ContactPerson details 是一个简单的类,定义如下:
public class ContactPersonDetails
{
public String ID;
public String Name;
public String Email;
public String Phone;
public static String GetIdFromNameInPersonArray(String name,ContactPersonDetails[]cpd)
{
for(int i=0;i<cpd.length;i++)
{
if(cpd[i].Name.equals(name))
{
return cpd[i].ID;
}
}
return null;
}
}
这是我的联系人截图
图 5.3:所有联系人的 LogCat
现在,如果你有兴趣了解如何在现有的内容提供器上应用投影、选择和排序顺序,这里有一个方法
public String[] FetchContactNamesAsProjectionAndSelectionCriteriaWithOrderingDemo(String searchPattern)
{
ArrayList<String> allNames=new ArrayList<String>();
ContentResolver cr = context.getContentResolver();
String [] projections=new String[]{ContactsContract.Contacts._ID,ContactsContract.Contacts.DISPLAY_NAME};
Cursor cur = cr.query(ContactsContract.Contacts.CONTENT_URI, projections, ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?",
new String[] {"%"+searchPattern+"%"}, ContactsContract.Contacts.DISPLAY_NAME + " ASC");
if (cur.getCount() > 0)
{
while (cur.moveToNext())
{
String id = cur.getString(
cur.getColumnIndex(ContactsContract.Contacts._ID));
String Name = cur.getString(
cur.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
allNames.add(Name);
}
String[] person=new String[allNames.size()];
person=allNames.toArray(person);
return person;
}
return null;
}
投影数组可以通过查看本节开头我们找出的列名来形成。
5.2 使用预制意图处理内容提供器
假设你想从你的应用中收集一些用户数据作为联系人。你会怎么做?你会为联系人设计一个全新的表单吗?Android 为你提供了一个更简单的选项。它提供了大量现成的 Intent,可以用于与内容提供器交互。我们以通过 Intent 插入联系人为例。
public void InsertContactThroughIntent(String name, String email, String phone)
{
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
intent.putExtra(ContactsContract.Intents.Insert.NAME, name);
intent.putExtra(ContactsContract.Intents.Insert.PHONE, phone);
intent.putExtra(ContactsContract.Intents.Insert.EMAIL, email);
context.startActivity(intent);
}
你所要做的就是调用这个方法,它会弹出一个 Intent 窗体,你可以在其中传递从你的应用程序收集的详细信息,或者只是将其留空并在窗体本身中插入。
**图 5.4**:通过 Intent 添加新联系人
6. 结论
在我学习 Android 的时候,我经常遇到内容丰富但指导如何完成任务方面不足的教程。在编写本教程时,我尽力详细说明 ContentProviders 的技术细节。本教程附带了一个客户端应用程序。下载 ContentResolverClient_App.zip
这是一个很实用的小工具,可以将地址名称拉取到自动完成文本框中,并显示您选择的联系人的照片(如果他有照片)。onCreate 和 onClick 部分包含了本教程中讨论的所有客户端调用部分。您可以取消注释特定部分以了解该部分是如何工作的!
|
||||