前言
上一篇文章梳理了一下Service,这一篇文章梳理一下同为四大组件之一的ContentProvider,也试着使用在线MarkDown编辑器。正常开发中,我们使用ContentProvider较少,大家也不会太过于关注。国内能看到的几篇文章也是比较老旧了,demo中也有一些错误,所以写一篇“像样”的,凑凑数。
正文
概述ContentProvider
依旧去API文档中先拿到官方的概述,并做一点蹩脚的翻译和注解:
Content providers are one of the primary building blocks of Android applications, providing content to applications. They encapsulate data and provide it to applications through the singleContentResolver interface. A content provider is only required if you need to share data between multiple applications. For example, the contacts data is used by multiple applications and must be stored in a content provider. If you don’t need to share data amongst multiple applications you can use a database directly via SQLiteDatabase.
Content Provider 是Android应用程序的主要组成部分之一(四大组件之一,大家可以多体会组件这个词的含义),对应用程序提供内容。它们封装数据,并且通过singleContentResolver接口向应用程序提供这些数据。如果您需要在多个应用程序之间共享数据,才需要content provider组件。例如,联系人数据被多个应用程序使用,所以必须存储在content provider中。如果你不需要在多个应用程序之间共享数据,可以直接通过SQLiteDatabase使用数据库。
When a request is made via a ContentResolver the system inspects the authority of the given URI and passes the request to the content provider registered with the authority. The content provider can interpret the rest of the URI however it wants. The UriMatcher class is helpful for parsing URIs.
当通过ContentResolver发起一个请求时,系统检查给定的URI的身份权限(这个词意不是很原汁原味,大家应该能体会)、并将请求转发给注册了相关身份权限的content provider。contentprovider可以从URI的剩余部分解析出请求的意图(举个例子,就像一条webAPI)。UriMatcher类用于解析URI。
同时,API文档也告诉了我们以下几个成员函数是需要实现的
? onCreate() which is called to initialize the provider
? query(Uri, String[], String, String[], String) which returns data to the caller
? insert(Uri, ContentValues) which inserts new data into the content provider
? update(Uri, ContentValues, String, String[]) which updates existing data in the content provider
? delete(Uri, String, String[]) which deletes data from the content provider
? getType(Uri) which returns the MIME type of data in the content provider
本次不带大家详细的看API文档的内容,也不去分析Android中的源码,我们就讨论一下ContentProvider的使用场景、使用中要避免的问题、以及具体的实现细节
ContentProvider的使用场景
我们在上面提到过:
providing content to applications;
If you don’t need to share data amongst multiple applications you can use a database directly via SQLiteDatabase
ContentProvider粗糙的翻译一下就是内容提供者,它本身是Android制定的一套规范,按照这个规范,你可以将自己变成内容的提供者、具有向别的应用提供内容的能力请注意我这里的措辞,我试图表达:“我具有向你提供的能力,你有需求就按照标准向我索取,我不会向你主动发送”。
所以,ContentProvider的使用场景是这样的:“一个应用拥有一定的数据内容,这些内容会被别的应用所需要”,如同API文档中举的例子:联系人。
API文档中也很明确的指出一点:如果你不需要在多个应用之间共享内容,那就不需要使用ContentProvider(因为它本身就是为了共享数据内容的),直接使用数据库,这里我们留一个话头,之后再谈。
ContentProvider如何描述内容
如果你对ContentProvider做过一点了解,就会知道实现一个ContentProvider的过程中使用了Sqlite数据库。
使用ContentProvider是共享数据规范的做法(言下之意是还有非常规,例如用IPC机制实现一个remote service),Android设计的时候,在这一块注意力放在如何规范的提供内容,而并不过于关心I/O,所以使用表这样的形式来描述内容(这样的设计很好,既可以很直接的提供表数据,也可以利用数据得到更丰富的内容,例如实际的图片、视频,当然这种设计也不是一两句话就能说详尽的)。这里再接前面的话头,API中说不需要共享就直接使用数据库不要形而上的理解,各种数据都有合适的保存形式,适合用数据库的就用数据库,本身就是一个文件没有必要将二进制数据保存到数据库表,当然用数据库保存一下文件的基本信息是很好的、很规范的。
ContentProvider使用注意点
线程安全
这一点在好些访问量超高的文章中都没有得到体现,哪怕一丝丝提及,但凡有并发的数据访问都要考虑线程安全,这应当是条件反射。
API中如是说:
Data access methods (such as insert(Uri, ContentValues) and update(Uri, ContentValues, String, String[])) may be called from many threads at once, and must be thread-safe. Other methods (such as onCreate()) are only called from the application main thread, and must avoid performing lengthy operations. See the method descriptions for their expected thread behavior.
操作数据的方法(例如增删改查)有可能在多个线程并发(这里注意不仅仅是一个进程多个线程、还有多个进程的可能),所以实现增删改查等方法时必须线程安全(实现线程安全就行了,不要乱想,千万不要想着单个应用中控制并发,是邪路)
构造器部分
public ContentProvider ()
Construct a ContentProvider instance. Content providers must be declared in the manifest, accessed with ContentResolver, and created automatically by the system, so applications usually do not create ContentProvider instances directly.
At construction time, the object is uninitialized, and most fields and methods are unavailable. Subclasses should initialize themselves in onCreate(), not the constructor.
为了创建(一个可用的)ContentProvider实例,ConentProvider必须在清单文件中声明,获取内容的途径,而且它是被系统自动创建的,所以应用程序一般不
在实例被构造时,对象还没有被实例化,绝大多数成员和方法都不可使用,存在依赖关系的类应当在onCreate中实例化,而不是在构造器中(我们能想象Android在背后又做了一些事情,才让content provider真正起到作用)。
Content providers are created on the application main thread at application launch time. The constructor must not perform lengthy operations, or application startup will be delayed.
Content providers在应用程序启动的时候由应用程序主线程创建,所以构造器中不能做耗时的操作,否则应用程序的启动会有滞后感(俗称卡)。
访问内容的部分
其实这些操作概括一下就是增删改查,在使用数据库的时候,我们一般使用transaction来实现“安全的”访问,在ContentProvider中,我们实现线程安全就可以了,对增删改查使用同步块,不用担心多个访问者产生冲突。这一点在API中也算重笔叙述的,然而我浏览了几篇文章没有发现相应作者提到这些。
ContentProvider的实现
这里声明一下,demo我是直接从一个已有的文章中拉出来的,只是原作者的文章写的很长,而且一些demo中关键的地方并没有好好说明(而且写的不对,疑似从不同的项目中拷贝的,容易产生误解)。
为了实现一个ContentProvider,我们至少要实现这几个类:
- 抽象类ContentProvider的实现类
- 注意包含Authority的URI
- 注意线程安全
- 继承自SQLiteOpenHelper的子类
- 正常实现,真正使用的时候注意规范定义常量,便于维护
- 描述内容的类
- Authority
- 数据库、表的相关信息
- URIMatcher
并且要在清单中进行注册
let’s see the f**king code,uh ha ha~.
ContentProvider子类
package com.example.mycontentprovider;
import com.example.mycontentprovider.ContentData.UserTableData;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;
/**
* @ClassName: ExampleContentProvider
* @Description: TODO
* @date 2016年5月10日 上午10:19:48
*
* @author leobert.lan
* @version 1.0
*/
public class ExampleContentProvider extends ContentProvider {
// attention:lowercase 注意小写哦,后面是完整的类路径名
public static final Uri CONTENT_URI = Uri.parse("content://com.example.mycontentprovider.examplecontentprovider");
private DBOpenHelper dbOpenHelper = null;
private static final UriMatcher uriMatcher = UserTableData.uriMatcher;
@Override
public boolean onCreate() {
dbOpenHelper = new DBOpenHelper(this.getContext(), ContentData.DATABASE_NAME, ContentData.DATABASE_VERSION);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
synchronized (ExampleContentProvider.class) {
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
switch (uriMatcher.match(uri)) {
case UserTableData.TEACHERS:
return db.query("teacher", projection, selection, selectionArgs, null, null, sortOrder);
case UserTableData.TEACHER:
// 进行解析,返回值为10
long personid = ContentUris.parseId(uri);
String where = "_ID=" + personid;// 获取指定id的记录
where += !TextUtils.isEmpty(selection) ? " and (" + selection + ")" : "";// 把其它条件附加上
return db.query("teacher", projection, where, selectionArgs, null, null, sortOrder);
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case UserTableData.TEACHERS:
return UserTableData.CONTENT_TYPE;
case UserTableData.TEACHER:
return UserTableData.CONTENT_TYPE_ITME;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
synchronized (ExampleContentProvider.class) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
long id = 0;
switch (uriMatcher.match(uri)) {
case UserTableData.TEACHERS:
// 返回的是记录的行号,主键为int,实际上就是主键值
id = db.insert("teacher", null, values);
return ContentUris.withAppendedId(uri, id);
case UserTableData.TEACHER:
id = db.insert("teacher", null, values);
String path = uri.toString();
// 替换掉id
return Uri.parse(path.substring(0, path.lastIndexOf("/")) + id);
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
synchronized (ExampleContentProvider.class) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
int count = 0;
switch (uriMatcher.match(uri)) {
case UserTableData.TEACHERS:
count = db.delete("teacher", selection, selectionArgs);
break;
case UserTableData.TEACHER:
// 下面的方法用于从URI中解析出id,对这样的路径content://hb.android.teacherProvider/teacher/10
// 进行解析,返回值为10
long personid = ContentUris.parseId(uri);
String where = "_ID=" + personid; // 删除指定id的记录
where += !TextUtils.isEmpty(selection) ? " and (" + selection + ")" : ""; // 把其它条件附加上
count = db.delete("teacher", where, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
db.close();
return count;
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
synchronized (ExampleContentProvider.class) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
int count = 0;
switch (uriMatcher.match(uri)) {
case UserTableData.TEACHERS:
count = db.update("teacher", values, selection, selectionArgs);
break;
case UserTableData.TEACHER:
// 下面的方法用于从URI中解析出id,对这样的路径content://com.ljq.provider.personprovider/person/10
// 进行解析,返回值为10
long personid = ContentUris.parseId(uri);
String where = "_ID=" + personid;// 获取指定id的记录
where += !TextUtils.isEmpty(selection) ? " and (" + selection + ")" : "";// 把其它条件附加上
count = db.update("teacher", values, where, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
db.close();
return count;
}
}
}
我们需要注意的是:线程安全(getType就没有必要了,就是做了一个uri的解析),包含Authority的URI
数据库工具类:
我check了一下sql没问题,这个简单的实现没有太多讲解
package com.example.mycontentprovider;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
/**
* @ClassName: DBOpenHelper
* @Description: TODO
* @date 2016年5月10日 上午10:27:07
*
* @author leobert.lan
* @version 1.0
*/
public class DBOpenHelper extends SQLiteOpenHelper {
// 在SQLiteOepnHelper的子类当中,必须有该构造函数,用来创建一个数据库;
public DBOpenHelper(Context context, String name, CursorFactory factory, int version) {
// 必须通过super调用父类当中的构造函数
super(context, name, factory, version);
// TODO Auto-generated constructor stub
}
public DBOpenHelper(Context context, String name, int version) {
this(context, name, null, version);
}
/**
* 只有当数据库执行创建 的时候,才会执行这个方法。如果更改表名,也不会创建,只有当创建数据库的时候,才会创建改表名之后 的数据表
*/
@Override
public void onCreate(SQLiteDatabase db) {
System.out.println("create table");
db.execSQL("create table " + ContentData.UserTableData.TABLE_NAME + "(" + ContentData.UserTableData._ID
+ " INTEGER PRIMARY KEY autoincrement," + ContentData.UserTableData.NAME + " varchar(20),"
+ ContentData.UserTableData.TITLE + " varchar(20)," + ContentData.UserTableData.DATE_ADDED + " long,"
+ ContentData.UserTableData.SEX + " boolean)" + ";");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
描述内容的类:
这个版本必须大于4的说法果然吓到了我,哈哈。
package com.example.mycontentprovider;
import android.content.UriMatcher;
import android.net.Uri;
import android.provider.BaseColumns;
/**
* @ClassName: ContentData
* @Description: TODO
* @date 2016年5月10日 上午10:21:46
*
* @author leobert.lan
* @version 1.0
*/
public class ContentData {
public static final String AUTHORITY = "com.example.mycontentprovider.examplecontentprovider";
public static final String DATABASE_NAME = "teacher.db";
// 创建 数据库的时候,都必须加上版本信息;并且必须大于4 --你吓到我了,然而我试了一下,
public static final int DATABASE_VERSION = 1;
public static final String USERS_TABLE_NAME = "teacher";
public static final class UserTableData implements BaseColumns {
public static final String TABLE_NAME = "teacher";
// Uri,外部程序需要访问就是通过这个Uri访问的,这个Uri必须的唯一的。
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/teacher");
// 数据集的MIME类型字符串则应该以vnd.android.cursor.dir/开头
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/hb.android.teachers";
// 单一数据的MIME类型字符串应该以vnd.android.cursor.item/开头
public static final String CONTENT_TYPE_ITME = "vnd.android.cursor.item/hb.android.teacher";
/* 自定义匹配码 */
public static final int TEACHERS = 1;
/* 自定义匹配码 */
public static final int TEACHER = 2;
public static final String TITLE = "title";
public static final String NAME = "name";
public static final String DATE_ADDED = "date_added";
public static final String SEX = "SEX";
public static final String DEFAULT_SORT_ORDER = "_id desc";
public static final UriMatcher uriMatcher;
static {
// 常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//列表数据
uriMatcher.addURI(ContentData.AUTHORITY, "teacher", TEACHERS);
//单条数据
uriMatcher.addURI(ContentData.AUTHORITY, "teacher/#", TEACHER);
}
}
}
到这里我们已经可以准备测试了,把清单文件补充好
清单文件(局部)
<provider
android:name="com.example.mycontentprovider.ExampleContentProvider"
android:exported="true"
android:authorities="com.example.mycontentprovider.examplecontentprovider" />
这里注意一下:你可以将authority认为“域名”一样的东西,把所有的都check一下没有写错。 exported这个属性,你在应用内测试的话(这也就是测试的时候玩玩,谁也不干这傻事)设置false(默认false)没有问题,但真正使用的时候(应用之间共享)设置false会抛出无法访问的错误。
我们写一点测试部分,输出结果粗糙了一点,经验不甚丰富的同学可以将cursor中的内容全部取出转对象,序列化输出一下(用fastjson之类的很方便,没必要重写toString)。
布局部分:
activity_test.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="${relativePackage}.${activityClass}" >
<Button
android:id="@+id/test_btn_insert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="insert" />
<Button
android:id="@+id/test_btn_query"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="query" />
<Button
android:id="@+id/test_btn_queryall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="queryall" />
<Button
android:id="@+id/test_btn_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="update" />
<Button
android:id="@+id/test_btn_delete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="delete" />
</LinearLayout>
逻辑部分
package com.example.mycontentprovider;
import java.util.Date;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class TestActivity extends Activity {
private final static String TAG = "testactivity";
private Button btnInsert, btnQuery, btnQueryAll, btnUpdate, btnDelete;
Uri uri = Uri.parse("content://com.example.mycontentprovider.examplecontentprovider/teacher");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
btnInsert = (Button) findViewById(R.id.test_btn_insert);
btnQuery = (Button) findViewById(R.id.test_btn_query);
btnQueryAll = (Button) findViewById(R.id.test_btn_queryall);
btnUpdate = (Button) findViewById(R.id.test_btn_update);
btnDelete = (Button) findViewById(R.id.test_btn_delete);
btnInsert.setOnClickListener(new InsertListener());
btnQuery.setOnClickListener(new QueryListener());
btnQueryAll.setOnClickListener(new QueryAllListener());
btnUpdate.setOnClickListener(new UpdateListener());
btnDelete.setOnClickListener(new DeleteListener());
}
class UpdateListener implements OnClickListener {
@Override
public void onClick(View v) {
ContentResolver cr = getContentResolver();
ContentValues cv = new ContentValues();
cv.put("name", "test_update_name");
cv.put("date_added", (new Date()).toString());
int _index = cr.update(uri, cv, "_ID=?", new String[] { "3" });
Log.d(TAG, "updated" + ":" + _index);
}
}
class DeleteListener implements OnClickListener {
@Override
public void onClick(View v) {
ContentResolver cr = getContentResolver();
cr.delete(uri, "_ID=?", new String[] { "2" });
}
}
class QueryAllListener implements OnClickListener {
@Override
public void onClick(View v) {
ContentResolver cr = getContentResolver();
Cursor c = cr.query(uri, null, null,null, null);
Log.d(TAG, "count" + ":" + c.getCount());
c.close();
}
}
class InsertListener implements OnClickListener {
public void onClick(View v) {
ContentResolver cr = getContentResolver();
ContentValues cv = new ContentValues();
cv.put("title", "jiaoshou");
cv.put("name", "jiaoshi");
cv.put("sex", true);
Uri uri2 = cr.insert(uri, cv);
Log.d(TAG, "insert" + ":" + uri2.toString());
}
}
class QueryListener implements OnClickListener {
public void onClick(View v) {
// TODO Auto-generated method stub
ContentResolver cr = getContentResolver();
// 查找id为1的数据
Cursor c = cr.query(uri, null, "_ID=?", new String[] { "1" }, null);
// 这里必须要调用 c.moveToFirst将游标移动到第一条数据,不然会出现index -1 requested , with a
// size of 1错误;cr.query返回的是一个结果集。
if (c.moveToFirst() == false) {
// 为空的Cursor
Log.d(TAG, "query:nothing");
c.close();
return;
}
int name = c.getColumnIndex("name");
Log.d(TAG, "query:name"+c.getString(name));
c.close();
}
}
}
当然咯,重建一个应用去测才有意义!顺带考虑一下实现交互友好(不可的的情况下不要crash,不要没反应,具体形式不管)
ContentResolver
我们在刚才的demo中看到了这个类,内容解析器,这是和ContentProvider配套使用的,提供者通过ContentProvider提供内容,请求者通过ContentResolver进行访问、解析。其实我们用的多的是这个,获取手机中的联系人、图片、视频、音频……
Authority
API文档中动不动就谈到这个词,看了上面的demo我们有这样的体会:特别像Restful API 的感觉,这个Authority就像域名,指向提供“服务”的对象,你的意图包装在uri中,urimatcher就像路由。
附一部分API,节选了一些重要的,有兴趣的可以选读
Public Methods 节选
public abstract boolean onCreate ()
Added in API level 1
Implement this to initialize your content provider on startup. This method is called for all registered content providers on the application main thread at application launch time. It must not perform lengthy operations, or application startup will be delayed.
You should defer nontrivial initialization (such as opening, upgrading, and scanning databases) until the content provider is used (via query(Uri, String[], String, String[], String),insert(Uri, ContentValues), etc). Deferred initialization keeps application startup fast, avoids unnecessary work if the provider turns out not to be needed, and stops database errors (such as a full disk) from halting application launch.
If you use SQLite, SQLiteOpenHelper is a helpful utility class that makes it easy to manage databases, and will automatically defer opening until first use. If you do use SQLiteOpenHelper, make sure to avoid calling getReadableDatabase() or getWritableDatabase() from this method. (Instead, override onOpen(SQLiteDatabase) to initialize the database when it is first opened.)
Returns
? true if the provider was successfully loaded, false otherwise
public AssetFileDescriptor openAssetFile (Uri uri, String mode, CancellationSignal signal)
Added in API level 19
This is like openFile(Uri, String), but can be implemented by providers that need to be able to return sub-sections of files, often assets inside of their .apk. This method can be called from multiple threads, as described in Processes and Threads.
If you implement this, your clients must be able to deal with such file slices, either directly with openAssetFileDescriptor(Uri, String), or by using the higher-levelContentResolver.openInputStream or ContentResolver.openOutputStream methods.
The returned AssetFileDescriptor can be a pipe or socket pair to enable streaming of data.
If you are implementing this to return a full file, you should create the AssetFileDescriptor with UNKNOWN_LENGTH to be compatible with applications that cannot handle sub-sections of files.
For use in Intents, you will want to implement getType(Uri) to return the appropriate MIME type for the data returned here with the same URI. This will allow intent resolution to automatically determine the data MIME type and select the appropriate matching targets as part of its operation.
For better interoperability with other applications, it is recommended that for any URIs that can be opened, you also support queries on them containing at least the columns specified byOpenableColumns.
Parameters
uri The URI whose file is to be opened.
mode Access mode for the file. May be “r” for read-only access, “w” for write-only access (erasing whatever data is currently in the file), “wa” for write-only access to append to any existing data, “rw” for read and write access on any existing data, and “rwt” for read and write access that truncates any existing file.
signal A signal to cancel the operation in progress, or null if none. For example, if you are downloading a file from the network to service a “rw” mode request, you should periodically callthrowIfCanceled() to check whether the client has canceled the request and abort the download.
Returns
? Returns a new AssetFileDescriptor which you can use to access the file.
Throws
FileNotFoundException
Throws FileNotFoundException if there is no file associated with the given URI or the mode is invalid.
SecurityException
Throws SecurityException if the caller does not have permission to access the file.
See Also
? openFile(Uri, String)
? openFileHelper(Uri, String)
? getType(android.net.Uri)
public AssetFileDescriptor openAssetFile (Uri uri, String mode)
Added in API level 3
This is like openFile(Uri, String), but can be implemented by providers that need to be able to return sub-sections of files, often assets inside of their .apk. This method can be called from multiple threads, as described in Processes and Threads.
If you implement this, your clients must be able to deal with such file slices, either directly with openAssetFileDescriptor(Uri, String), or by using the higher-levelContentResolver.openInputStream or ContentResolver.openOutputStream methods.
The returned AssetFileDescriptor can be a pipe or socket pair to enable streaming of data.
If you are implementing this to return a full file, you should create the AssetFileDescriptor with UNKNOWN_LENGTH to be compatible with applications that cannot handle sub-sections of files.
For use in Intents, you will want to implement getType(Uri) to return the appropriate MIME type for the data returned here with the same URI. This will allow intent resolution to automatically determine the data MIME type and select the appropriate matching targets as part of its operation.
For better interoperability with other applications, it is recommended that for any URIs that can be opened, you also support queries on them containing at least the columns specified byOpenableColumns.
Parameters
uri The URI whose file is to be opened.
mode Access mode for the file. May be “r” for read-only access, “w” for write-only access (erasing whatever data is currently in the file), “wa” for write-only access to append to any existing data, “rw” for read and write access on any existing data, and “rwt” for read and write access that truncates any existing file.
Returns
? Returns a new AssetFileDescriptor which you can use to access the file.
Throws
FileNotFoundException
Throws FileNotFoundException if there is no file associated with the given URI or the mode is invalid.
SecurityException
Throws SecurityException if the caller does not have permission to access the file.
See Also
? openFile(Uri, String)
? openFileHelper(Uri, String)
? getType(android.net.Uri)
public ParcelFileDescriptor openFile (Uri uri, String mode)
Added in API level 1
Override this to handle requests to open a file blob. The default implementation always throws FileNotFoundException. This method can be called from multiple threads, as described inProcesses and Threads.
This method returns a ParcelFileDescriptor, which is returned directly to the caller. This way large data (such as images and documents) can be returned without copying the content.
The returned ParcelFileDescriptor is owned by the caller, so it is their responsibility to close it when done. That is, the implementation of this method should create a new ParcelFileDescriptor for each call.
If opened with the exclusive “r” or “w” modes, the returned ParcelFileDescriptor can be a pipe or socket pair to enable streaming of data. Opening with the “rw” or “rwt” modes implies a file on disk that supports seeking.
If you need to detect when the returned ParcelFileDescriptor has been closed, or if the remote process has crashed or encountered some other error, you can use open(File, int, android.os.Handler, android.os.ParcelFileDescriptor.OnCloseListener), createReliablePipe(), or createReliableSocketPair().
For use in Intents, you will want to implement getType(Uri) to return the appropriate MIME type for the data returned here with the same URI. This will allow intent resolution to automatically determine the data MIME type and select the appropriate matching targets as part of its operation.
For better interoperability with other applications, it is recommended that for any URIs that can be opened, you also support queries on them containing at least the columns specified byOpenableColumns. You may also want to support other common columns if you have additional meta-data to supply, such as DATE_ADDED in MediaStore.MediaColumns.
Parameters
uri The URI whose file is to be opened.
mode Access mode for the file. May be “r” for read-only access, “rw” for read and write access, or “rwt” for read and write access that truncates any existing file.
Returns
? Returns a new ParcelFileDescriptor which you can use to access the file.
Throws
FileNotFoundException
Throws FileNotFoundException if there is no file associated with the given URI or the mode is invalid.
SecurityException
Throws SecurityException if the caller does not have permission to access the file.
See Also
? openAssetFile(Uri, String)
? openFileHelper(Uri, String)
? getType(android.net.Uri)
? parseMode(String)
public ParcelFileDescriptor openFile (Uri uri, String mode, CancellationSignal signal)
Added in API level 19
Override this to handle requests to open a file blob. The default implementation always throws FileNotFoundException. This method can be called from multiple threads, as described inProcesses and Threads.
This method returns a ParcelFileDescriptor, which is returned directly to the caller. This way large data (such as images and documents) can be returned without copying the content.
The returned ParcelFileDescriptor is owned by the caller, so it is their responsibility to close it when done. That is, the implementation of this method should create a new ParcelFileDescriptor for each call.
If opened with the exclusive “r” or “w” modes, the returned ParcelFileDescriptor can be a pipe or socket pair to enable streaming of data. Opening with the “rw” or “rwt” modes implies a file on disk that supports seeking.
If you need to detect when the returned ParcelFileDescriptor has been closed, or if the remote process has crashed or encountered some other error, you can use open(File, int, android.os.Handler, android.os.ParcelFileDescriptor.OnCloseListener), createReliablePipe(), or createReliableSocketPair().
For use in Intents, you will want to implement getType(Uri) to return the appropriate MIME type for the data returned here with the same URI. This will allow intent resolution to automatically determine the data MIME type and select the appropriate matching targets as part of its operation.
For better interoperability with other applications, it is recommended that for any URIs that can be opened, you also support queries on them containing at least the columns specified byOpenableColumns. You may also want to support other common columns if you have additional meta-data to supply, such as DATE_ADDED in MediaStore.MediaColumns.
Parameters
uri The URI whose file is to be opened.
mode Access mode for the file. May be “r” for read-only access, “w” for write-only access, “rw” for read and write access, or “rwt” for read and write access that truncates any existing file.
signal A signal to cancel the operation in progress, or null if none. For example, if you are downloading a file from the network to service a “rw” mode request, you should periodically callthrowIfCanceled() to check whether the client has canceled the request and abort the download.
Returns
? Returns a new ParcelFileDescriptor which you can use to access the file.
Throws
FileNotFoundException
Throws FileNotFoundException if there is no file associated with the given URI or the mode is invalid.
SecurityException
Throws SecurityException if the caller does not have permission to access the file.
See Also
? openAssetFile(Uri, String)
? openFileHelper(Uri, String)
? getType(android.net.Uri)
? parseMode(String)
public AssetFileDescriptor openTypedAssetFile (Uri uri, String mimeTypeFilter, Bundle opts)
Added in API level 11
Called by a client to open a read-only stream containing data of a particular MIME type. This is like openAssetFile(Uri, String), except the file can only be read-only and the content provider may perform data conversions to generate data of the desired type.
The default implementation compares the given mimeType against the result of getType(Uri) and, if they match, simply calls openAssetFile(Uri, String).
See ClipData for examples of the use and implementation of this method.
The returned AssetFileDescriptor can be a pipe or socket pair to enable streaming of data.
For better interoperability with other applications, it is recommended that for any URIs that can be opened, you also support queries on them containing at least the columns specified byOpenableColumns. You may also want to support other common columns if you have additional meta-data to supply, such as DATE_ADDED in MediaStore.MediaColumns.
Parameters
uri The data in the content provider being queried.
mimeTypeFilter The type of data the client desires. May be a pattern, such as \/, if the caller does not have specific type requirements; in this case the content provider will pick its best type matching the pattern.
opts Additional options from the client. The definitions of these are specific to the content provider being called.
Returns
? Returns a new AssetFileDescriptor from which the client can read data of the desired type.
Throws
FileNotFoundException
Throws FileNotFoundException if there is no file associated with the given URI or the mode is invalid.
SecurityException
Throws SecurityException if the caller does not have permission to access the data.
IllegalArgumentException
Throws IllegalArgumentException if the content provider does not support the requested MIME type.
See Also
? getStreamTypes(Uri, String)
? openAssetFile(Uri, String)
? compareMimeTypes(String, String)
public AssetFileDescriptor openTypedAssetFile (Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
Added in API level 19
Called by a client to open a read-only stream containing data of a particular MIME type. This is like openAssetFile(Uri, String), except the file can only be read-only and the content provider may perform data conversions to generate data of the desired type.
The default implementation compares the given mimeType against the result of getType(Uri) and, if they match, simply calls openAssetFile(Uri, String).
See ClipData for examples of the use and implementation of this method.
The returned AssetFileDescriptor can be a pipe or socket pair to enable streaming of data.
For better interoperability with other applications, it is recommended that for any URIs that can be opened, you also support queries on them containing at least the columns specified byOpenableColumns. You may also want to support other common columns if you have additional meta-data to supply, such as DATE_ADDED in MediaStore.MediaColumns.
Parameters
uri The data in the content provider being queried.
mimeTypeFilter The type of data the client desires. May be a pattern, such as \/, if the caller does not have specific type requirements; in this case the content provider will pick its best type matching the pattern.
opts Additional options from the client. The definitions of these are specific to the content provider being called.
signal A signal to cancel the operation in progress, or null if none. For example, if you are downloading a file from the network to service a “rw” mode request, you should periodically call throwIfCanceled() to check whether the client has canceled the request and abort the download.
Returns
? Returns a new AssetFileDescriptor from which the client can read data of the desired type.
Throws
FileNotFoundException
Throws FileNotFoundException if there is no file associated with the given URI or the mode is invalid.
SecurityException
Throws SecurityException if the caller does not have permission to access the data.
IllegalArgumentException
Throws IllegalArgumentException if the content provider does not support the requested MIME type.
See Also
? getStreamTypes(Uri, String)
? openAssetFile(Uri, String)
? compareMimeTypes(String, String)
public abstract Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
Added in API level 1
Implement this to handle query requests from clients. This method can be called from multiple threads, as described in Processes and Threads.
Example client call:
// Request a specific record.
Cursor managedCursor = managedQuery(
ContentUris.withAppendedId(Contacts.People.CONTENT_URI, 2),
projection, // Which columns to return.
null, // WHERE clause.
null, // WHERE clause value substitution
People.NAME + ” ASC”); // Sort order.
Example implementation:
// SQLiteQueryBuilder is a helper class that creates the
// proper SQL syntax for us.
SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder();
// Set the table we‘re querying.
qBuilder.setTables(DATABASE_TABLE_NAME);
// If the query ends in a specific record number, we‘re
// being asked for a specific record, so set the
// WHERE clause in our query.
if((URI_MATCHER.match(uri)) == SPECIFIC_MESSAGE){
qBuilder.appendWhere("_id=" + uri.getPathLeafId());
}
// Make the query.
Cursor c = qBuilder.query(mDb,
projection,
selection,
selectionArgs,
groupBy,
having,
sortOrder);
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
Parameters
uri The URI to query. This will be the full URI sent by the client; if the client is requesting a specific record, the URI will end in a record number that the implementation should parse and add to a WHERE or HAVING clause, specifying that _id value.
projection The list of columns to put into the cursor. If null all columns are included.
selection A selection criteria to apply when filtering rows. If null then all rows are included.
selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in order that they appear in the selection. The values will be bound as Strings.
sortOrder How the rows in the cursor should be sorted. If null then the provider is free to define the sort order.
Returns
? a Cursor or null.
public Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal)
Added in API level 16
Implement this to handle query requests from clients with support for cancellation. This method can be called from multiple threads, as described in Processes and Threads.
Example client call:
// Request a specific record.
Cursor managedCursor = managedQuery(
ContentUris.withAppendedId(Contacts.People.CONTENT_URI, 2),
projection, // Which columns to return.
null, // WHERE clause.
null, // WHERE clause value substitution
People.NAME + ” ASC”); // Sort order.
Example implementation:
// SQLiteQueryBuilder is a helper class that creates the
// proper SQL syntax for us.
SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder();
// Set the table we‘re querying.
qBuilder.setTables(DATABASE_TABLE_NAME);
// If the query ends in a specific record number, we‘re
// being asked for a specific record, so set the
// WHERE clause in our query.
if((URI_MATCHER.match(uri)) == SPECIFIC_MESSAGE){
qBuilder.appendWhere("_id=" + uri.getPathLeafId());
}
// Make the query.
Cursor c = qBuilder.query(mDb,
projection,
selection,
selectionArgs,
groupBy,
having,
sortOrder);
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
If you implement this method then you must also implement the version of query(Uri, String[], String, String[], String) that does not take a cancellation signal to ensure correct operation on older versions of the Android Framework in which the cancellation signal overload was not available.
Parameters
uri The URI to query. This will be the full URI sent by the client; if the client is requesting a specific record, the URI will end in a record number that the implementation should parse and add to a WHERE or HAVING clause, specifying that _id value.
projection The list of columns to put into the cursor. If null all columns are included.
selection A selection criteria to apply when filtering rows. If null then all rows are included.
selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in order that they appear in the selection. The values will be bound as Strings.
sortOrder How the rows in the cursor should be sorted. If null then the provider is free to define the sort order.
cancellationSignal A signal to cancel the operation in progress, or null if none. If the operation is canceled, then OperationCanceledException will be thrown when the query is executed.
Returns
? a Cursor or null.
public abstract int update (Uri uri, ContentValues values, String selection, String[] selectionArgs)
Added in API level 1
Implement this to handle requests to update one or more rows. The implementation should update all rows matching the selection to set the columns according to the provided values map. As a courtesy, call notifyChange() after updating. This method can be called from multiple threads, as described in Processes and Threads.
Parameters
uri The URI to query. This can potentially have a record ID if this is an update request for a specific record.
values A set of column_name/value pairs to update in the database. This must not be null.
selection An optional filter to match rows to update.
Returns
? the number of rows affected.
后记
体会了一下markdown,再次体会了这个道理:
新事物的兴起必然有旧事物不足的地方(哪怕只是旧事物不适应了),而对新事物的畏惧无非起源于以下两种:对旧事物的过于依赖;对旧事物还不熟悉。——节选自leobert的语录。
haha~
demo部分我没有费太多心思,要是读者觉得这摘过来修改的demo不爽,评论区说一声,我会抽空重新规范的实现一下,并上传到download专区,给邮箱要的就算了,真心忙不过来。