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返回. 這樣就實現了文件的查詢功能.