Android四大组件——ContentProvider
- 一、运行时权限
- 1.1 配置< uses-permission>作用:
- 1.2 运行时权限的作用
- 1.3 运行时权限的申请与处理
- 二、Content Provider
- 2.1 自定义Content Provider
- 2.1.1 注册ContentProvider
- 2.1.2 实现ContentProvider
- 2.2 ContentResolver
- 2.3 UriMatcher
- 2.4 ContentObserver
- 2.4.1 ContentService
- 2.4.2 注册ContentObserver
- 2.4.3 产生消息
- 2.4.4 创建ContentObserver
- 三、DocumentsProvider
- 3.1 存储访问框架SAF(storage access framework)
- 3.2 ParcelFileDescriptor
- 3.2.1 FileDescriptor
- 3.2.2 Parcel
- 3.2.3 生成ParcelFileDescriptor
- 3.3 自定义DocumentsProvider
- 3.3.1 注册DocumentsProvider
- 3.3.2 文件结构
- 3.3.3 MatrixCursor
- 3.3.4 实现DocumentsProvider
- 3.3.5 使用DocumentsProvider
- 四、FileProvider
- 4.1 使用场景
- 4.2 配置FileProvider
- 4.2.1 FileProvider注册:
- 4.2.2 使用FileProvider
一、运行时权限
1.1 配置< uses-permission>作用:
- android 6.0以下的系统没有运行时权限,可以通过< uses-permission>的方式直接申请权限;
- android 6.0以上的系统即使需要在运行时需要申请权限,也需要在androidManifest.xml文件中通过配置< uses-permission>来告知用户和系统该应用程序需要用到的权限;
- 所有需要的权限都配置在AndroidManifest.xml文件中,最终可以在应用程序管理界面查看到所有的权限以及相应的申请情况。
1.2 运行时权限的作用
针对android 6.0以上的系统,用户可以不用在安装时一次性授权,可以在使用过程中对某一项进行授权,并且即使该项不授权其他功能也能正常使用。
只有危险权限需要运行时授权。
权限组名 |
权限名 |
calendar |
read_calendar write_calendar |
camera |
camera |
contacts |
read_contacts write_contacts get_accounts |
location |
access_fine_loaction access_coarse_location |
microphone |
record_audio |
phone |
read_phone_state call_phone read_call_log write_call_log ado_voicemail use_sip process_outgoing_calls |
sensors |
body_sensors |
sms |
send_sms receive_sms read_sms receive_wap_push receive_mms |
storage |
read_external_storage write_external_storage |
注意:只要对权限组中的一个权限授权,会自动对该组中的其他所有权限授权,但只有在androidManifest.xml中引用过的权限才能使用。
1.3 运行时权限的申请与处理
- 检验是否授权
ContextCompat.checkSelfPermission(context, Manifest.permission.A)
并与PackageManager.PERMISSION_GRANTED比较
- 请求授权
ActivityCompat.requestPermissions,可以组成授权字符串数组一次性循环检查并请求授权。
- 处理请求结果
需要重写onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 并自动调用。
- 可在手机设置中重新修改授权
二、Content Provider
2.1 自定义Content Provider
主要用于不同的应用程序之间实现数据共享功能。继承自ContentProvider,需要重写的方法如下,由于数据存储形式的不同,对应的数据操作方式也不同,而ContentProvider将所有的数据操作都看作对表格的维护,统一了操作形式,将实际的数据操作方法封装在ContentProvider的CRUD中,可以在一定程度上将数据操作与数据真实的存储方式解耦。主要用于共享数据时提供操作数据的方式。
2.1.1 注册ContentProvider
android的四大组件均需要在AndroidManifest.xml文件中进行注册,而ContentProvider有以下可选属性:
- enable:表示是否启用该组件
- exported:表示是否允许外部应用访问该组件
- grandUriPermissions:在具有ContentProvider内容的访问权限的应用中调用其它不具有权限的应用来打开对应内容的Uri时,是否可以通过权限传递的方式来使不具有权限的应用获取权限。
- intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
2.1.2 实现ContentProvider
- public boolean onCreate()
当ContentResolver访问数据时内容。ContentProvider才会被初始化;返回true表示初始化成功。
- public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder)
其中,根据uri匹配对应的返回码,根据返回码判断具体的数据操作对象。projection表示查询的列名,selection、selectionArgs表示查询条件,sortOrder表示查询顺序。
String bookid = uri.getPathSegments().get(1);
将内容URI中的authority之后的部分以“/”分割,并将分割后的结果放入一个字符串列表,列表的第0个位置存放path,第1个位置存放id。
- public Uri insert(Uri uri, ContentValues values)
返回新纪录的URI
- public int update(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder)
返回受影响的数据记录条数。
- public int delete(Uri uri, String selection, String[] selectionArgs)
返回受影响的数据记录条数。
- public String getType(Uri uri)
返回MIME类型。
2.2 ContentResolver
想要访问content provider中共享的数据,就一定需要使用Content Resolver类,可通过context中的getContentResolver() 方法获得Resolver实例,并利用uri对对相应内容进行CRUD操作。
2.3 UriMatcher
UriMatcher用于注册和匹配URI,只有在UriMatcher中添加过的uri才能被其他应用程序访问。
static {
uriMatcher=new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(authority,"book",BOOK_All);
uriMatcher.addURI(authority,"book/#",BOOK_ITEM);
uriMatcher.addURI(authority,"*",TABLE_ALL);
}
其中,UriMatcher.NO_MATCH= -1,表示没有匹配的uri则返回-1。addURI中authority、path表示输入的uri,*:表示任意长度的字符,#:表示任意长度的数字;code表示返回的匹配码。
- 匹配uri
uriMatcher.match(uri) 得到相应的返回码
2.4 ContentObserver
ContentObserver用于监听数据的变化,当对应的uri数据发生变化时,会自动触发ContentObserver中的onChange()方法。
2.4.1 ContentService
ContentService是一个系统级别的消息中心,也是通过binder机制实现的,且transact的方式是one-way,不需要等待消息返回再执行其他操作。其作用为:
- 消息汇总:应用如果产生了一些消息,也可以向ContentService进行提交,供其进行消息传播。
- 消息分发:应用可以向ContentService注册ContentObserver,订阅自身感兴趣的消息;一旦有这类消息产生,ContentService会将消息分发该相应的ContentObserver并进行相应操作:
- 调用该ContentObserver的dispatchChange(selfChange, uri, userId)方法。
- dispatchChange()方法可能是在Binder线程中同步执行,也可能是发送到一个与Handler绑定的线程中执行。
private void dispatchChange(boolean selfChange, Uri uri, int userId) {
if (mHandler == null) {
onChange(selfChange, uri, userId);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri, userId));
}
}
2.4.2 注册ContentObserver
通过ContentResolver对象调用以下方法进行注册:
- public final void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer)
- uri:表示订阅的消息来源
- notifyForDescendants:表示是否监听uri的子节点的消息
- observer:表示注册的observer
2.4.3 产生消息
通过ContentResolver对象调用以下方法产生消息,一般在ContentProvider的CRUD操作结束时调用,表示数据发生变化:
- public void notifyChange(Uri uri, ContentObserver observer)
- uri:变化数据对应的uri;
- observer:发起数据变化消息的observer;若为null,则表示该消息没有发起者;若不为null,则表示该消息由传入的observer对象发起,如果希望该observer能收到自身发起的消息,需要重写public boolean deliverSelfNotifications()使其返回true(默认返回false)。
2.4.4 创建ContentObserver
public class MyContentObserver extends ContentObserver {
public MyContentObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
}
}
- 构造方法
需要传入handler对象,如果handler对象不为null,则onChange()方法在handler绑定的线程中执行,若handler绑定的线程为主线程,则可以更新UI;如果handler对象为null,则onChange()方法在binder线程中执行。
- onChange
传入参数表示收到的消息是否由自身发起,对应notifyChange中的observer;如果重写了deliverSelfNotifications()使其返回true,且该消息由自身发起,则selfChange为true。如果deliverSelfNotifications()返回false,且该消息由自身发起,该observer接收不到该消息。
三、DocumentsProvider
3.1 存储访问框架SAF(storage access framework)
将多个DocumentsProvider子类所提供的内容组合在一块,并提供统一的系统级浏览界面,可以进行CRUD操作。其组成部分如下:
- Document Provider
DocumentsProvider是一个继承自ContentProvider的抽象类,而其具体的实现类称作Document Provider,即文档提供者。
- Picker
系统级浏览界面,可根据Client的需求筛选符合条件的所有Document Provider并将其内容展示出来。DocumentUI,intent-filter中没有设置category为launcher。
- Client
发起调用并在onActivityResult()中接收数据以及对数据进行操作;
- 调用方式
startActivityForResult(intent, requestCode)
- ACTION_OPEN_DOCUMENT
- 查看文件必须设置文件类型
- intent.setType(" * / * ");
- ACTION_CREATE_DOCUMENT
- 新建文件:
- 发起 ACTION_CREATE_DOCUMENT的意图Intent
- 设置文件名intent.putExtra(Intent.EXTRA_TITLE, fileName)
- 设置保存路径的文件类型
- uri是DocumentUI返回的。
3.2 ParcelFileDescriptor
3.2.1 FileDescriptor
FileDescriptor是一个用于表述指向文件的引用的抽象化概念。在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。 当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。只适用于UNIX、Linux这样的操作系统,且Linux的设计思想是把一切设备都视作文件。
每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中可以看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。
FileDescriptor实际用途是创建FileInputStream 或 FileOutputStream。
3.2.2 Parcel
- 作用:Parcel是一个容器,它主要用于存储序列化数据,然后可以通过Binder在进程间传递这些数据。
- 使用原因:由于每个进程使用的是虚拟地址空间,所以如果在跨进程传递时传递的是对象引用,即对象对应的虚拟内存地址,则在另一个进程中是无法找到对应的数据的。而parcel则是将对象的主要特征提取并打包,传递给另一个进程后根据这些特征重新生成一个对象。
3.2.3 生成ParcelFileDescriptor
将FileDescriptor写入Parcel并在读取时返回一个ParcelFileDescriptor对象用于操作原始的文件描述符。ParcelFileDescriptor是原始描述符的一个复制,对象和fd不同,但是都操作于同一文件流,使用同一个文件位置指针。通过调用ParcelFileDescriptor的静态方法可以生成ParcelFileDescriptor对象。
- public static ParcelFileDescriptor open(File file, int mode)
- public static ParcelFileDescriptor open(File file, int mode, Handler handler,
final OnCloseListener listener)
- mode
- MODE_APPEND:在文档末尾继续写入;
- MODE_CREATE:如果文件不存在,则新建文件;
- MODE_READ_ONLY:以只读方式打开文档;
- MODE_READ_WRITE:以读写方式打开文档;
- MODE_TRUNCATE:打开文件时删除文件内容;
- MODE_WRITE_ONLY:以只写的方式打开文档。
- handler:调用ParcelFileDescriptor的close()方法触发listener的callback时,触发动作产生的线程上的handler。
- listener:在关闭ParcelFileDescriptor执行的callback方法。
3.3 自定义DocumentsProvider
3.3.1 注册DocumentsProvider
<provider
android:name="com.example.MyCloudProvider"
android:authorities="com.example.mycloudprovider"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS"
android:enabled="@bool/isAtLeastKitKat">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
- exported="true"
- 设置android:permission="android.permission.MANAGE_DOCUMENTS"
只有拥有该权限的用户才能访问该DocumentsProvider组件,而此处的权限只有系统才能获取,即DocumentsProvider只有系统级的浏览界面才能访问。
- android.content.action.DOCUMENTS_PROVIDER
3.3.2 文件结构
图1 文件结构
- 每个根目录都有一个唯一的 COLUMN_ROOT_ID;
- 每个根目录下都有唯一的一个Document。该Document下有 1 至 N 个Documents;
- 每个Document都有唯一的一个 COLUMN_DOCUMENT_ID;
- Document可以是特定的 MIME 类型,也可以是包含子Documents的目录,为MIME_TYPE_DIR 类型;
- 每个Document都可以具有不同的功能,用COLUMN_FLAGS表示。例如:
- FLAG_SUPPORTS_WRITE:支持写入
- FLAG_SUPPORTS_DELETE:支持删除
- FLAG_SUPPORTS_THUMBNAIL:支持缩略图
- . . .
3.3.3 MatrixCursor
当需要返回Cursor对象时,可以创建一张虚拟数据表,即MatrixCursor,该数据表可自动扩容。
- 构造方法
- public MatrixCursor(String[] columnNames)
- public MatrixCursor(String[] columnNames, int initialCapacity)
- columnNames:数据表对应的列名,顺序与字符串顺序一致;
- initialCapacity:数据表容量,默认为0。
- 填充数据
- public void addRow(Object[] columnValues)
顺序需与columnNames一致。数据一次性添加一行。
- public RowBuilder newRow()
生成数据行的构造器。数据依次添加。
- public RowBuilder add(String columnName, Object value)
3.3.4 实现DocumentsProvider
- public boolean onCreate()
返回true表示初始化成功。
- public Cursor queryRoots(String[] projection) throws FileNotFoundException
拼凑根节点的Cursor
- 传入参数
- 返回值
- Cursor
DocumentsContract.Root中列出的列名以及对应的值
- COLUMN_DOCUMENT_ID
- COLUMN_FLAGS
- FLAG_SUPPORTS_CREATE
- FLAG_SUPPORTS_SEARCH
- . . .
- . . .
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
MatrixCursor matrixCursor=new MatrixCursor(projection==null?DEFAULT_ROOT_PROJECTION:projection);
MatrixCursor.RowBuilder rowBuilder=matrixCursor.newRow();
File homeDir=new File(Environment.getExternalStorageDirectory(),"myfile");
rowBuilder.add(Root.COLUMN_DOCUMENT_ID,homeDir.getAbsolutePath());
rowBuilder.add(Root.COLUMN_ROOT_ID,homeDir.getAbsolutePath());
rowBuilder.add(Root.COLUMN_FLAGS,Root.FLAG_LOCAL_ONLY|Root.FLAG_SUPPORTS_SEARCH|Root.FLAG_SUPPORTS_CREATE);
rowBuilder.add(Root.COLUMN_TITLE,"My Application");
rowBuilder.add(Root.COLUMN_ICON,icon);
return matrixCursor;
}
- public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException
- 拼凑子节点的Cursor
- DocumentsContract.Document中列出的列名以及对应的值
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = new File(parentDocumentId);
for (File file : parent.listFiles()) {
if (!file.getName().startsWith(".")) {
includeFile(result, file);
}
}
return result;
}
private void includeFile(MatrixCursor result, File file) throws FileNotFoundException {
MatrixCursor.RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, file.getAbsolutePath());
row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
String mimeType = getDocumentType(file.getAbsolutePath());
row.add(Document.COLUMN_MIME_TYPE, mimeType);
int flags =file.canWrite()?
Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_RENAME
| (mimeType.equals(Document.MIME_TYPE_DIR) ? Document.FLAG_DIR_SUPPORTS_CREATE : 0): 0;
if (mimeType.startsWith("image/"))
flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_SIZE, file.length());
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException {
File file = new File(documentId);
if (file.isDirectory())
return Document.MIME_TYPE_DIR;
final int lastDot = file.getName().lastIndexOf('.');
if (lastDot >= 0) {
final String extension = file.getName().substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) {
return mime;
}
}
return "application/octet-stream";
}
- public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, new File(documentId));
return result;
}
- public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException
当通过ContentResolver调用openFileDescriptor(Uri uri, String mode)时系统会调用此方法。
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
return ParcelFileDescriptor.open(new File(documentId),ParcelFileDescriptor.parseMode(mode));
}
- Document.FLAG_SUPPORTS_THUMBNAIL需要重写public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException
3.3.5 使用DocumentsProvider
- 新建文件:
- 获取Uri:intent.getData() ;
- 需要重写public String createDocument(String parentDocumentId, String mimeType, String displayName)
- 删除文件
- 前提:Document.COLUMN_FLAGS包含SUPPORTS_DELETE
DocumentsContract.deleteDocument(getContentResolver(), intent.getData());
- 需要重写public void deleteDocument(String documentId) throws FileNotFoundException
- 数据处理
- 直接使用Uri;
- content://com.example.myapplication.MyDocumentsProvider/document/%2Fstorage%2Femulated%2F0%2Fmyfile%2F1.txt
- Uri与documentId有关;content://authority/document/documentId的URI形式。
- 通过authority找到对应的Provider,再调用Provider的createDocument()、deleteDocument()以及openDocument()方法。
- 通过Uri得到ParcelFileDescriptor;
- getContentResolver().openFileDescriptor(Uri uri, String mode);
- 系统自动调用DocumentsProvider的openDocument()方法;
- 通过Uri得到IO流;
- getContentResolver().openInputStream(uri);
- 函数内部也是通过FileDescriptor生成的IO流。
四、FileProvider
4.1 使用场景
- 需要使用file的Uri形式
- Uri.fromFile(File file)方法可以得到file的Uri形式,但其形式为file:///storage/emulated/0/myfile/11165797.jpg,即" file:// "+真实路径;
- Android 7.0+不允许暴露真实路径,所以不能使用file://的形式传递文件的标识。
4.2 配置FileProvider
4.2.1 FileProvider注册:
<provider
android:authorities="com.example.loki.myapplication.MyFileProvider"
android:grantUriPermissions="true"
android:name="android.support.v4.content.FileProvider">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
<!-- 路径匹配信息 -->
android:resource="@xml/filepath"
/>
</provider>
- authority:自定义,provider的标识;
- name:provider的类名,使用SDK中自带的类,android.support.v4.content.FileProvider
-
- name固定为android.support.FILE_PROVIDER_PATHS
- resource:自定义的xml文件,用于映射共享路径。在resource包下新建xml文件夹。
- name:表示虚拟目录
- path:在标签对应的目录下的真实路径
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="selfDefinition"
path="myflie"/>
</paths>
子节点 |
含义 |
路径 |
files-path |
getFileDir() |
/data/data/packageName/files/ |
cache-path |
getCacheDir() |
/data/data/packageName/cache/ |
external-path |
Environment.getExternalStorageDirectory() |
/sdcard/ |
external-files-path |
Environment.getExternalFilesDirs() |
/sdcard/Android/data/packageName/files/ |
external-cache-path |
Environment.getExternalCacheDirs() |
/sdcard/Android/data/packageName/cache/ |
4.2.2 使用FileProvider
- FileProvider.getUriForFile(Context context, String authority, File file)
- content://com.example.myapplication.MyFileProvider/selfDefinition/NewFolder/1.txt
- 将path中标注的路径替换为name中的虚拟路径