SAF的使用

文章目录

  • 一、前言
  • 二、注意问题
  • 三、路径命名规则
  • 四、添加依赖
  • 五、授权
  • 六、使用DataStore-proto保存权限
  • 七、ViewModel中对授权功能对使用
  • 八、在Activity中的使用
  • 九、参考链接

一、前言

在Android上面对存储空间的访问目前主要有两种方式:MediaStoreSAFMediaStore主要是用来对媒体文件做存储处理。SAF主要是用来对文档的目录进行处理。当我们查找某种条件比较具体的文件时候可以使用MediaStore–例如查找整个应用中的apk文件。但是如果我们需要查找或修改某个文件夹中的内容,或者查看某个文件时候,使用SAF较好。但是MediaStore使用不需要权限(如果访问外置存储卡而不是图片这种地址的话依然需要授权),而SAF需要经过用户授权

二、注意问题

使用SAF的方式和使用File有点不太一样。

  1. File可以通过当前文件获取父文件,然后再获取同级文件。但是SAF就不能这样,只能获取到授权的文件夹下面的文件。获取父文件夹的话都是null
  2. SAF没有经过授权的话,无法获取到该文文件夹。哪怕知道具体的uri
  3. SAF授权过的文件夹、,即使把程序卸载再装上,哪怕没有再次授权依然可以通过uri获取到该文件
  4. SAF在11.0及其以下可以获取到存储卡中的根目录,但是12.0的话无法获取到外置存储卡的根目录。

三、路径命名规则

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//创建跳转的Intentoverride 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.EMPTYmContext?.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)}}
}

六、使用DataStore-proto保存权限

../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: SAFDataStoreget() = 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
)

七、ViewModel中对授权功能对使用

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.IOviewModelScope.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.EMPTYLog.e("YM--->存入的授权","-->hasPermission:$isHasPermission")context.safDataStore.updateData {it.toBuilder().setHasSAFPermission(isHasPermission).build()}}
/*** 该函数需要在STARTED生命周期之前调用* 必须用在主线程中*/@MainThreadfun 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!!)}
}

八、在Activity中的使用

class MainActivity: AppCompatActivity() {private val viewModel: SAFStoreViewModel by viewModels()override fun onCreate(savedInstanceState: Bundle?) {viewModel.addObserver(this,Uri.parse(SAF_WHATS_APP))}
}

九、参考链接

  1. Android中的DataStore-Proto_Mr_Tony的专栏-CSDN博客

  2. Android中的ResultApi跳转_Mr_Tony的专栏-CSDN博客

Published by

风君子

独自遨游何稽首 揭天掀地慰生平