Android4.4中引入了Storage Access Framework存储访问框架,简称(SAF)。SAF为用户浏览手机中存储的内容提供了方便,这些内容不仅包括文档、图片,视频、音频、下载,而且还包括所有由特定ContentProvider(须具有约定的API)提供的内容. 每一种特定内容都有对应的Document provider, 这些Document provider其实是DocumentsProvider的子类. android系统中已经内置了几个这样的Document provider,比如关于下载、图片以及视频的Document provider.
在4.4以上的版本, 可以用一下代码启动SAF:
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);//ACTION_OPEN_DOCUMENT
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/jpeg");
startActivity(intent);
假设我们需要做一个类似文件管理器的应用, 网络上大部分实现思路是从根目录遍历设备上所有的文件, 然后根据mimType来对文件进行分类, 这里提供另外一种思路, 就是使用DocumentsProvider, 实现方式类似与Storage Access Framework存储访问框架.
例如读取根目录下所有文件的功能(使用DocumentsProvider, 必须在4.4以上版本, 如果想不受版本限制需要将相关类copy自行修改):
1.自定义LocalStorageProvider 继承自 DocumentsProvider
public class LocalStorageProvider extends DocumentsProvider {
/**
* 默认root需要查询的项
*/
private final static String[] DEFAULT_ROOT_PROJECTION = new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_SUMMARY,
Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_ICON,
Root.COLUMN_AVAILABLE_BYTES};
/**
* 默认Document需要查询的项
*/
private final static String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME, Document.COLUMN_FLAGS, Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE,
Document.COLUMN_LAST_MODIFIED};
/**
* 进行读写权限检查
*/
static boolean isMissingPermission(@Nullable Context context) {
if (context == null) {
return true;
}
if (ContextCompat.checkSelfPermission(context,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// 通知root的Uri失去权限, 禁止相关操作
context.getContentResolver().notifyChange(
DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY), null);
return true;
}
return false;
}
/*
* 在此方法中组装一个cursor, 他的内容就是home与sd卡的路径信息,
* 并将home与sd卡的信息存到数据库中
*/
@Override
public Cursor queryRoots(final String[] projection) throws FileNotFoundException {
if (getContext() == null || ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
return null;
}
//创建一个查询cursor, 来设置需要查询的项, 如果"projection"为空, 那么使用默认项
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
// 添加home路径
File homeDir = Environment.getExternalStorageDirectory();
if (TextUtils.equals(Environment.getExternalStorageState(), Environment.MEDIA_MOUNTED)) {
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, homeDir.getAbsolutePath());
row.add(Root.COLUMN_DOCUMENT_ID, homeDir.getAbsolutePath());
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.home));
row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
}
// 添加SD卡路径
File sdCard = new File("/storage/extSdCard");
String storageState = EnvironmentCompat.getStorageState(sdCard);
if (TextUtils.equals(storageState, Environment.MEDIA_MOUNTED) ||
TextUtils.equals(storageState, Environment.MEDIA_MOUNTED_READ_ONLY)) {
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, sdCard.getAbsolutePath());
row.add(Root.COLUMN_DOCUMENT_ID, sdCard.getAbsolutePath());
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.sd_card));
row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY);
row.add(Root.COLUMN_ICON, R.drawable.ic_sd_card);
row.add(Root.COLUMN_SUMMARY, sdCard.getAbsolutePath());
row.add(Root.COLUMN_AVAILABLE_BYTES, new StatFs(sdCard.getAbsolutePath()).getAvailableBytes());
}
return result;
}
@Override
public boolean isChildDocument(final String parentDocumentId, final String documentId) {
return documentId.startsWith(parentDocumentId);
}
@Override
public Cursor queryChildDocuments(final String parentDocumentId, final String[] projection,
final String sortOrder) throws FileNotFoundException {
// 判断是否缺少权限
if (LocalStorageProvider.isMissingPermission(getContext())) {
return null;
}
// 创建一个查询cursor, 来设置需要查询的项, 如果"projection"为空, 那么使用默认项
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;
}
@Override
public Cursor queryDocument(final String documentId, final String[] projection) throws FileNotFoundException {
if (LocalStorageProvider.isMissingPermission(getContext())) {
return null;
}
// 创建一个查询cursor, 来设置需要查询的项, 如果"projection"为空, 那么使用默认项
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, new File(documentId));
return result;
}
private void includeFile(final MatrixCursor result, final File file) throws FileNotFoundException {
final 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(final String documentId) throws FileNotFoundException {
if (LocalStorageProvider.isMissingPermission(getContext())) {
return null;
}
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";
}
@Override
public boolean onCreate() {
return true; // 这里需要返回true
}
}
2.xml中的配置如下:
<provider
android:name=".LocalStorageProvider"
android:authorities="com.xxx.xxx.authorities"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
并添加权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
使用自定义LocalStorageProvider, 和使用一般ContentProvider一样
getContext().getContentResolver().query(Uri, null, null, null, null);
在上面的例子中, 只是重载了查询相关的接口, 实现查询功能. 如果想实现打开/重命名/删除等操作, 可以重载openDocument / renameDocument / deleteDocument 接口.
例子中的逻辑很清楚, 当我们开始查询文档时, 首先会进入queryRoots接口, 这时我们获得一个cursor, 含有根路径与sd卡路径信息, 当我查询文档(假如查询根路径), 就会进入queryChildDocuments接口, 在此接口中查询当前路径下所有文件与文件夹并将相关信息组装成一个curor返回. 这样就实现了文件的查询功能.