在Android上面对存储空间的访问目前主要有两种方式:MediaStore和SAF。MediaStore主要是用来对媒体文件做存储处理。SAF主要是用来对文档的目录进行处理。当我们查找某种条件比较具体的文件时候可以使用MediaStore–例如查找整个应用中的apk文件。但是如果我们需要查找或修改某个文件夹中的内容,或者查看某个文件时候,使用SAF较好。但是MediaStore使用不需要权限(如果访问外置存储卡而不是图片这种地址的话依然需要授权),而SAF需要经过用户授权
使用SAF的方式和使用File有点不太一样。
SAF整体内容较多,在实践过程中只涉及了其中一部分。这里对这一部分进行详细记录。需求如下,获取根目录下面的Whatsapp目录下面的内容,由于需要授权问题,所以需要将权限进行保留,以后每次使用时候也要进行权限判断(这里使用DataStore-proto进行权限保存,关于DataStore-proto使用参考Android中的DataStore-Proto_Mr_Tony的专栏-CSDN博客)。
路径定义如下:
//通过SAF方式进行文档读写,文档格式为
// content://com.example/root:sdcard/recent
// 不能为以下方式,其中冒号需要用UrlEncode编码,其编码为 %3A
// content://com.example/root/sdcard/recent/
// 最终需要转为Uri进行使用const val SAF_ROOT = "content://com.android.externalstorage.documents/tree/primary%3A"
const val SAF_ROOT = "content://com.android.externalstorage.documents/tree/primary%3A"
const val SAF_WHATS_APP = SAF_ROOT+"WhatsApp" //WhatsApp子目录Media
在../app/build.gradle
中添加以下依赖
plugins {
id "com.google.protobuf" version "0.8.12"
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.0'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
implementation 'androidx.documentfile:documentfile:1.0.1'
//协程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
//dataStore安全类型存储
implementation "androidx.datastore:datastore:1.0.0"
implementation "com.google.protobuf:protobuf-javalite:3.14.0"
}
protobuf {
protoc {
if (osdetector.os == "osx") {//m1芯片需要单独处理
artifact = 'com.google.protobuf:protoc:3.14.0:osx-x86_64'
} else {
artifact = 'com.google.protobuf:protoc:3.14.0'
}
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
权限跳转需要使用Intent跳转到新的页面进行授权。这里通过使用ResultApi进行授权。这里将该功能单独写了个模块使其与其它业务分离
object ResultApiRoute {
const val FLAG_DOCUMENT = 5 //用于标志document文件
class ResultSAFStorePermissionContact: ActivityResultContract<Uri, Uri>() {
private var mContext: Context ?= null
//创建跳转的Intent
override fun createIntent(context: Context, input: Uri): Intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION //授予打开权限类型
putExtra(DocumentsContract.EXTRA_INITIAL_URI,input)//授予目录路径
mContext = context
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri {
val dirUri = intent?.data ?: Uri.EMPTY
mContext?.contentResolver?.takePersistableUriPermission(//获取永久授权
dirUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
return dirUri
}
}
//用于获取SAF中指定目录的访问权限
//注意跳转时机不要在刚注册就去跳转
class ResultSAFStorePermissionObserver(private val registry : ActivityResultRegistry,private val flag: Int,private val initCallBack: (ResultSAFStorePermissionObserver) -> Unit, private val callback: ActivityResultCallback<Uri>)
: DefaultLifecycleObserver {
private lateinit var getContent : ActivityResultLauncher<Uri>
override fun onCreate(owner: LifecycleOwner) {
//这里使用flag将key区分,如果是同一个key的话只会注册一个,即使重复创建新的构造函数也是如此
getContent = registry.register(flag.toString(), owner,ResultSAFStorePermissionContact(),callback)
initCallBack.invoke(this)
}
//查看视频详情将原先到数据传递过来并返回选择的数据
fun goSAFStoreActivity(uri: Uri) {
getContent.launch(uri)
}
}
}
在../app/src/main/
下面创建proto
的文件夹,在下面创建datastore.proto
的文件。内容如下:
syntax = "proto3";
option java_package = "com.test.datastore";
option java_multiple_files = true;
message SAFDataStore {
bool hasSAFPermission = 1;//是否有saf授权
}
重新build生成新的文件。编写kotlin代码,如下:
//RockeyDataStore的序列化操作
//写法参考
//https://developer.android.google.cn/topic/libraries/architecture/datastore?hl=zh-cn
object SAFSerializer: Serializer<SAFDataStore> {
override val defaultValue: SAFDataStore
get() = SAFDataStore.getDefaultInstance()
override suspend fun readFrom(input: InputStream): SAFDataStore {
try {
return SAFDataStore.parseFrom(input)
}catch (exception: InvalidProtocolBufferException){
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: SAFDataStore, output: OutputStream) {
t.writeTo(output)
}
}
创建全局使用单例:
//存储类型管理
val Context.safDataStore: DataStore<SAFDataStore> by dataStore(
fileName = "datastore.proto",
serializer = RockeySerializer
)
class SAFStoreViewModel : ViewModel(){
//跳转到SAF文档管理页面的监听
private var filePermissionObserver: ResultApiRoute.ResultSAFStorePermissionObserver? = null
/**
* 检查文档的权限
* 这里将是否授权存在本地,这样就避免了每次去授权
* 该函数需要在onStart生命周期之前调用
* @param uri: 检查目标文件的授权
*/
private fun checkDocumentPermission(context: FragmentActivity, uri: Uri) {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
exception.printStackTrace()
}//注册个协程异常处理器监听异常
val content = handler + Dispatchers.IO
viewModelScope.launch(context = content) {
val hasPermission: Boolean = context.rockeyDataStore.data.map {
it.hasSAFPermission
}.catch { e ->
e.printStackTrace()
}
.first()
if (hasPermission){
//已经获取授权
}else{//没有授权则进行授权获取
filePermissionObserver?.goSAFStoreActivity(uri)
}
}
}
//更新文档权限授权方式
private suspend fun updateDocumentPermission(context: Context, uri: Uri){
val isHasPermission = uri != Uri.EMPTY
Log.e("YM--->存入的授权","-->hasPermission:$isHasPermission")
context.safDataStore.updateData {
it.toBuilder()
.setHasSAFPermission(isHasPermission)
.build()
}
}
/**
* 该函数需要在STARTED生命周期之前调用
* 必须用在主线程中
*/
@MainThread
fun addObserver(act: FragmentActivity, uri: Uri) {
filePermissionObserver = ResultApiRoute.ResultSAFStorePermissionObserver(act.activityResultRegistry,
ResultApiRoute.FLAG_DOCUMENT,
{
//注册成功后回调用
checkDocumentPermission(act, uri)
},
{//权限授予结果后回调,注意该逻辑中没有处理权限拒绝的情况
viewModelScope.launch(context = Dispatchers.IO) {
updateDocumentPermission(act,uri)
}
})
act.lifecycle.addObserver(filePermissionObserver!!)
}
}
class MainActivity: AppCompatActivity() {
private val viewModel: SAFStoreViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
viewModel.addObserver(this,Uri.parse(SAF_WHATS_APP))
}
}