如何在 Android 11 上使用 Media Store API 获取隐藏文件夹中的文件

Posted

技术标签:

【中文标题】如何在 Android 11 上使用 Media Store API 获取隐藏文件夹中的文件【英文标题】:How to fetch files inside hidden folder using Media Store API on Android 11 【发布时间】:2021-12-29 01:21:12 【问题描述】:

我需要在外部存储上的 WhatsApp 文件夹中获取数据。 由于我的目标是 API 级别 30,因此我不再能够访问外部存储上的 WhatsApp 文件夹。我已经实现了Storage Access Framework 并得到了android/media 文件夹UriDocument File。使用listFiles() 我可以列出文件,但使用filter()sortedByDescending() 函数会变得非常慢。

我尝试了什么?

使用带有投影和选择参数的光标加载器,但它只是 适用于WhatsApp ImagesWhatsApp Videos等非隐藏文件夹

返回隐藏文件夹.Statuses的空光标

尝试将MediaStore.Video.Media.EXTERNAL_CONTENT_URI 替换为MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)

需要什么?

列出 .Statuses 文件夹中的图像和视频,就像我在 HomeActivity.java 中使用 Media Store 列出 WhatsApp 图像一样

下面是我的代码

在此活动中,我获得了对 Android/媒体的许可,并设置了所有 WhatsApp 文件夹 URI 以用于状态获取和其他用途,但从 WhatsApp 图像文件夹中获取了投影和选择的 WhatsApp 图像

class HomeActivity : AppCompatActivity(), InternetListener, PurchasesUpdatedListener,
    CoroutineScope 
    private val exceptionHandler = CoroutineExceptionHandler  context, exception ->
        Toast.makeText(this, exception.message, Toast.LENGTH_LONG).show()

    
    private val dataRepository: DataRepository by inject()
    val tinyDB: TinyDB by inject()

    val REQUEST_CODE = 12123

    init 
        newNativeAdSetUp = null
    

    val sharedViewModel by viewModel<SharedViewModel>()

    val viewModel by viewModel<HomeViewModel>()


    val handler = CoroutineExceptionHandler  _, exception ->
        Log.d("CoroutineException", "$exception handled !")
    
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job + handler
    private lateinit var job: Job
    val sdk30PermissionListener = object : PermissionListener 
        override fun onPermissionGranted() 
            openDocumentTree()
        

        override fun onPermissionDenied(deniedPermissions: MutableList<String>?) 
        
    


    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)

        handlePermissionsByVersion()
    

    private fun handlePermissionsByVersion() 

        if (SDK_INT >= Build.VERSION_CODES.R) 
            if ((ContextCompat.checkSelfPermission(
                    this,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
                )
                        == PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission(
                    this,
                    Manifest.permission.READ_EXTERNAL_STORAGE
                )
                        == PackageManager.PERMISSION_GRANTED)
            ) 
                //if granted load whatsapp images and some uris setup to viewmodel
                loadWhatsAppImages()
                if (arePermissionsGranted()) 
                    if (dataRepository.mrWhatsAppImages == null || dataRepository.mrWhatsAppBusinessImages == null) 
                        setUpWAURIs()
                    
                
             else 
                TedPermission.with(this)
                    .setPermissionListener(sdk30PermissionListener)
                    .setDeniedMessage("If you reject permission,you can not use this service\n\nPlease turn on permissions at [Setting] > [Permission]")
                    .setPermissions(
                        Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.READ_EXTERNAL_STORAGE
                    )
                    .check()
            

        
    

    override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) 
        super.onActivityResult(requestCode, resultCode, data)


        if (resultCode == RESULT_OK && requestCode == REQUEST_CODE) 
            if (data != null) 
                //this is the uri user has provided us
                val treeUri: Uri? = data.data
                if (treeUri != null) 
                    sharedViewModel.treeUri = treeUri
                    val decoded = Uri.decode(treeUri.toString())
                    Log.i(LOGTAG, "got uri: $treeUri.toString()")
                    // here we should do some checks on the uri, we do not want root uri
                    // because it will not work on Android 11, or perhaps we have some specific
                    // folder name that we want, etc
                    if (Uri.decode(treeUri.toString()).endsWith(":")) 
                        showWrongFolderSelection()
                        return
                    
                    if (!decoded.equals(Constants.WHATSAPP_MEDIA_URI_DECODED)) 
                        showWrongFolderSelection()
                        return
                    
                    // here we ask the content resolver to persist the permission for us
                    val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
                            Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    contentResolver.takePersistableUriPermission(
                        treeUri,
                        takeFlags
                    )
                    val treeUriAsString = treeUri.toString()
                    tinyDB.putString("FOLDER_URI", treeUriAsString)
                    if (SDK_INT >= Build.VERSION_CODES.R) 
                        setupPaths()
                    

                
            
        
    

    private fun setupPaths() 
        setUpOverlay()
        fetchWhatsAppRootURIs(
            this,
            sharedViewModel,
            dataRepository,
            tinyDB
        ) 
            fetchWhatsAppBusinessRootURIs(
                this,
                sharedViewModel,
                dataRepository,
                tinyDB
            ) 
                tinyDB.putBoolean("WARootPathsDone", true)
                removeOverlay()
            
        


    

    override fun onDestroy() 
        dialogHandler.removeCallbacksAndMessages(null)
        super.onDestroy()
    
    val loadmanagerImages = object : LoaderManager.LoaderCallbacks<Cursor> 
        val whatsAppImagesArrayList = arrayListOf<File>()


        override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> 
            var location: File = File(
                Environment.getExternalStorageDirectory()
                    .toString() + Constants.whatsapp_images_path
            )
            if (!location.exists()) 
                location = File(
                    Environment.getExternalStorageDirectory()
                        .toString() + Constants.whatsapp_images_path11
                )
            

            if (location != null && location.exists()) 
                whatsAppImagesArrayList.clear()
                Timber.e("checkLoaded-onCreateLoader $id")
                if (id == 0) 
                    var folder = location.absolutePath
                    val projection = arrayOf(
                        MediaStore.MediaColumns.DATA,
                        MediaStore.MediaColumns.DATE_MODIFIED
                    )
                    val selection = MediaStore.Images.Media.DATA + " like ? "
                    val selectionArgs: String = "%$folder%"

                    return CursorLoader(
                        this@HomeActivity,
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                        projection,
                        selection,
                        arrayOf(selectionArgs),
                        "$MediaStore.Images.Media.DATE_MODIFIED DESC"
                    )
                
            

            return null!!
        

        override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) 
            Timber.e("checkLoaded-onLoadFinished")
            var absolutePathOfImage: String
            if (loader.id == 0) 
                cursor?.let 
                    val columnIndexData = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
                    GlobalScope.launch(Dispatchers.Main + exceptionHandler) 

                        async(Dispatchers.IO + exceptionHandler) 
                            while (!cursor.isClosed && cursor.moveToNext() == true) 
                                absolutePathOfImage = cursor.getString(columnIndexData!!)
                                whatsAppImagesArrayList.add(File(absolutePathOfImage))

                            
                        .await()
                        LoaderManager.getInstance(this@HomeActivity).destroyLoader(0)
                        Timber.e("checkLoaded-Completion")
                        galleryViewModel.whatsAppImagesList.postValue(whatsAppImagesArrayList)
                    


                
            
        

        override fun onLoaderReset(loader: Loader<Cursor>) 
        

    

    fun loadWhatsAppImages() 
        try 
            tinyDB.putBoolean("whatsAppMediaLoadCalled", true)
            LoaderManager.getInstance(this).initLoader(
                0,
                null,
                loadmanagerImages
            )
         catch (e: RuntimeException) 
            Log.e("exVideos ", "ex : $e.localizedMessage")
        

    


    companion object 
        const val ANDROID_DOCID = "primary:Android/media/"
        const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
        private val androidUri = DocumentsContract.buildDocumentUri(
            EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
        )
        val androidTreeUri = DocumentsContract.buildTreeDocumentUri(
            EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
        )
    

    private fun openDocumentTree() 
        val uriString = tinyDB.getString("FOLDER_URI", "")
        when 
            uriString == "" -> 
                Log.w(LOGTAG, "uri not stored")
                askPermission()
            
            arePermissionsGranted() -> 
            
            else -> 
                Log.w(LOGTAG, "uri permission not stored")
                askPermission()
            
        
    
    // this will present the user with folder browser to select a folder for our data
    private fun askPermission() 
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
        intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidUri)
        startActivityForResult(intent, REQUEST_CODE)
    

    private fun arePermissionsGranted(): Boolean 
        var uriString = tinyDB.getString("FOLDER_URI", "")
        val list = contentResolver.persistedUriPermissions
        for (i in list.indices) 
            val persistedUriString = list[i].uri.toString()
            if (persistedUriString == uriString && list[i].isWritePermission && list[i].isReadPermission) 
                return true
            
        
        return false
    

    private fun showWrongFolderSelection() 
        val layoutInflaterAndroid = LayoutInflater.from(this)
        val mView = layoutInflaterAndroid.inflate(R.layout.layout_dialog_wrong_folder, null)
        val builder = AlertDialog.Builder(this, R.style.ThemePageSearchDialog)
        builder.setView(mView)
        val alertDialog = builder.show()
        alertDialog.setCancelable(false)
        val btnOk = mView.findViewById(R.id.tvExit) as TextView
        val tvCancel = mView.findViewById(R.id.tvCancel) as TextView
        btnOk.setOnClickListener 
            alertDialog.dismiss()
            openDocumentTree()
        
        tvCancel.setOnClickListener 
            alertDialog.dismiss()
        

    

    private fun setUpWAURIs() 
        dataRepository.mrWhatsAppImages =
            getDocumentFileFromStringURI(
                this,
                tinyDB.getString("mrWhatsAppImages")
            )
        dataRepository.mrWhatsAppVN =
            getDocumentFileFromStringURI(
                this,
                tinyDB.getString("mrWhatsAppVN")
            )
        dataRepository.mrWhatsAppDocs =
            getDocumentFileFromStringURI(
                this,
                tinyDB.getString("mrWhatsAppDocs")
            )
        dataRepository.mrWhatsAppVideo =
            getDocumentFileFromStringURI(
                this,
                tinyDB.getString("mrWhatsAppVideo")
            )
        dataRepository.mrWhatsAppAudio =
            getDocumentFileFromStringURI(
                this,
                tinyDB.getString("mrWhatsAppAudio")
            )
        dataRepository.WhatsAppStatuses =
            getDocumentFileFromStringURIStatuses(
                this,
                tinyDB.getString("WhatsAppStatuses")
            )



        dataRepository.mrWhatsAppBusinessImages =
            getDocumentFileFromStringURI(
                this,
                tinyDB.getString("mrWhatsAppBusinessImages")
            )
        dataRepository.mrWhatsAppBusinessVN =
            getDocumentFileFromStringURI(
                this,
                tinyDB.getString("mrWhatsAppBusinessVN")
            )
        dataRepository.mrWhatsAppBusinessDocs =
            getDocumentFileFromStringURI(
                this,
                tinyDB.getString("mrWhatsAppBusinessDocs")
            )
        dataRepository.mrWhatsAppBusinessVideo =
            getDocumentFileFromStringURI(
                this,
                tinyDB.getString("mrWhatsAppBusinessVideo")
            )
        dataRepository.mrWhatsAppBusinessAudio =
            getDocumentFileFromStringURI(
                this,
                tinyDB.getString("mrWhatsAppBusinessAudio")
            )
        dataRepository.WhatsAppBusinessStatuses =
            getDocumentFileFromStringURIStatuses(
                this,
                tinyDB.getString("WhatsAppBusinessStatuses")
            )
    

    fun setUpOverlay() 
        val dialogfragment = FullScreenLoadingDialog()
        dialogfragment.isCancelable = false
        dialogfragment.setisAdmobAd(true)
        val ft: FragmentTransaction =
            supportFragmentManager.beginTransaction()
        ft.add(dialogfragment, "DialogFragment_FLAG")
        ft.commitAllowingStateLoss()
    

    fun removeOverlay() 
        val fragment: Fragment? = supportFragmentManager.findFragmentByTag("DialogFragment_FLAG")
        if (fragment != null && fragment is DialogFragment) 
            fragment.dismissAllowingStateLoss()
        
    
    fun fetchWhatsAppRootURIs(
        context: Context,
        sharedViewModel: SharedViewModel,
        dataRepository: DataRepository,
        tinyDB: TinyDB, completed: () -> Unit

    ) 
        val selectedPackageName = Constants.WHATSAPP_PKG_NAME
        val selectedRootName = Constants.WHATSAPP_ROOT_NAME
        var waImages: DocumentFile? = null
        var waVN: DocumentFile? = null
        var waDocs: DocumentFile? = null
        var waVideos: DocumentFile? = null
        var waAudio: DocumentFile? = null
        var waStatus: DocumentFile? = null
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && sharedViewModel.treeUri != null) 
            CoroutineScope(Dispatchers.Main).launch 
                async(Dispatchers.IO) 
                    val dir = DocumentFile.fromTreeUri(
                        context,
                        sharedViewModel.treeUri!!
                    )
                    dir?.listFiles()?.forEach 
                        if (it.name.equals(selectedPackageName)) 
                            it.listFiles().forEach 
                                if (it.name.equals(selectedRootName)) 
                                    it.listFiles().forEach 
                                        if (it.name.equals(Constants.WHATSAPP_MEDIA_FOLDER_NAME)) 
                                            it.listFiles().forEach 
                                                if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_IMAGES)) 
                                                    waImages = it

                                                 else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_VN)) 
                                                    waVN = it

                                                 else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_DOCUMENTS)) 
                                                    waDocs = it
                                                 else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_VIDEO)) 
                                                    waVideos = it

                                                 else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_AUDIO)) 
                                                    waAudio = it

                                                 else if (it.name.equals(Constants.FOLDER_NAME_STATUSES)) 
                                                    waStatus = it

                                                

                                            


                                        
                                    
                                
                            
                        


                    
                .await()
                Timber.e("processStatusFetch:Done")
                tinyDB.putString("mrWhatsAppImages", waImages?.uri.toString())
                tinyDB.putString("mrWhatsAppVN", waImages?.uri.toString())
                tinyDB.putString("mrWhatsAppDocs", waImages?.uri.toString())
                tinyDB.putString("mrWhatsAppVideo", waImages?.uri.toString())
                tinyDB.putString("mrWhatsAppAudio", waImages?.uri.toString())
                tinyDB.putString("WhatsAppStatuses", waStatus?.uri.toString())

                dataRepository.mrWhatsAppImages = waImages
                dataRepository.mrWhatsAppVN = waVN
                dataRepository.mrWhatsAppDocs = waDocs
                dataRepository.mrWhatsAppVideo = waVideos
                dataRepository.mrWhatsAppAudio = waAudio
                dataRepository.WhatsAppStatuses = waStatus
                completed()


            
        
    

这里我使用 .Statuses 文件夹 URI 来列出 DocumentFiles 并显示,但这样很慢

class StatusImageFragment : Fragment(), StatusListener, CoroutineScope 

    companion object 
        fun newInstance() = StatusImageFragment()
    

    val handler = CoroutineExceptionHandler  _, exception ->
        Log.d("CoroutineException", "$exception handled !")
    
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job + handler
    private lateinit var job: Job

    private var adapterSDK30 = StatusImageAdapterSDK30()
    private var no_image: ImageView? = null
    private var no_image_txt: TextView? = null

    val tinyDB: TinyDB by inject()
    val sharedViewModel by viewModel<SharedViewModel>()
    private val dataRepository: DataRepository by inject()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? 
        job = Job()
        return inflater.inflate(R.layout.status_image_fragment, container, false)
    

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) 
        super.onViewCreated(view, savedInstanceState)

        swipeRefresh(false, false)
    


    public fun swipeRefresh(isReloadRequired: Boolean, isFromModeChanged: Boolean) 
        try 
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) 
                if (isFromModeChanged) 
                    status_image_recycler.visibility = View.GONE
                    progressbar.visibility = View.VISIBLE
                    no_image?.let 
                        it.visibility = View.GONE
                    
                    no_image_txt?.let 
                        it.visibility = View.GONE
                    
                    go_to_app?.let 
                        it.visibility = View.GONE
                    
                 else 
                    if (adapterSDK30.listImages == null || adapterSDK30.listImages.size == 0) 
                        no_image?.let 
                            it.visibility = View.GONE
                        
                        no_image_txt?.let 
                            it.visibility = View.GONE
                        
                        go_to_app?.let 
                            it.visibility = View.GONE
                        
                        progressbar.visibility = View.VISIBLE
                    
                
                if (isReloadRequired) 
                    processStatusFetchFromChild(
                        sharedViewModel.statusImages.observe(viewLifecycleOwner, Observer 
                            val arrayList = it
                            adapterSDK30.listImages = arrayList
                            postFetchingExecutionSDK30()
                        )
                    )

                 else 
                    sharedViewModel.statusImages.observe(viewLifecycleOwner, Observer 
                        val arrayList = it
                        adapterSDK30.listImages = arrayList
                        adapterSDK30.listImages = it
                        postFetchingExecutionSDK30()
                    )
                

            
         catch (ex: Exception) 
            ex.printStackTrace()

        

    

    private fun postFetchingExecutionSDK30() 
        progressbar.visibility = View.GONE
        status_image_recycler.visibility = View.VISIBLE
        if (adapterSDK30!!.listImages != null && adapterSDK30!!.listImages.size > 0) 
            no_image?.let 
                it.visibility = View.GONE
            
            no_image_txt?.let 
                it.visibility = View.GONE
            
            go_to_app?.let 
                it.visibility = View.GONE
            
         else 
            no_image?.let 
                it.visibility = View.VISIBLE
            
            no_image_txt?.let 
                it.visibility = View.VISIBLE
            
            go_to_app?.let 
                it.visibility = View.VISIBLE
            
        
        adapterSDK30!!.notifyDataSetChanged()

        status_img_swipe.isRefreshing = false
    


    override fun onDestroyView() 
        job.cancel()
        super.onDestroyView()
    


    fun processStatusFetchFromChild(completed: () -> Unit) 
        val statusSelection = tinyDB.getInt(Constants.status_accounts)
        if (statusSelection == 0 || statusSelection == 1) 
            if (dataRepository.WhatsAppStatuses == null) 
                (activity as StatusActivity).setUpWAURIs()
            
            var documentFileStatuses: DocumentFile? = dataRepository.WhatsAppStatuses
            if (statusSelection == 1) 
                documentFileStatuses = dataRepository.WhatsAppBusinessStatuses
            
            if (documentFileStatuses != null) 
                launch(Dispatchers.Main) 
                    val statusImages1 = arrayListOf<DocumentFile>()

                    async(Dispatchers.IO) 
                        //this takes time ; want to fetch this same as WhatsApp Gallery
                        statusImages1.addAll(documentFileStatuses!!.listFiles().filter 
                            it.mimeType.equals(Constants.MIME_TYPE_IMG_PNG) || it.mimeType.equals(
                                Constants.MIME_TYPE_IMG_JPG
                            ) || it.mimeType.equals(Constants.MIME_TYPE_IMG_JPEG)
                        .sortedByDescending  it.lastModified() )
                    .await()
                    Timber.e("processStatusFetch:Done")
                    sharedViewModel.statusImages.postValue(statusImages1)
                    completed()
                
             else 
                Timber.e("processStatusFetch:Done")
                sharedViewModel.statusImages.postValue(arrayListOf<DocumentFile>())
                completed()
            
         else 
            Timber.e("processStatusFetch:Done")
            sharedViewModel.statusImages.postValue(arrayListOf<DocumentFile>())
            completed()


        
    


请注意我使用的 WhatsApp 文件夹路径是

val whatsapp_images_path11 = "/Android/media/“ +"com.whatsapp" +"/WhatsApp/Media/WhatsAppImages/"

在这种情况下如何使用 MediaStore 以便我不需要使用列表的排序和过滤功能?获取 java.io 文件并不重要,只有我可以使用 URI。

【问题讨论】:

你没有告诉whatsapp文件夹的完整路径。 您没有告诉您是否能够使用 mediastore 列出隐藏文件夹。好吧,你做到了。它与 mediastore 完全没有关系。但是,问题是什么? @blackapps 我已经编辑了我的问题。添加了我使用的路径 【参考方案1】:

使用 DocumentFile 处理 SAF uris 确实很慢。

最好使用 DocumentsContract 来执行此操作。

它的速度大约是 DocumentFile 的 20 倍,大约是经典 File 类的东西。

应该可以将 MediaStore 用于隐藏文件夹。您无法使用媒体存储创建隐藏文件夹。但是,如果您设法使它们不使用 mediastore,您应该能够使用 mediastore 列出其中的文件。好吧,如果他们被扫描。如果它们属于您的应用。

【讨论】:

绝对将 MediaStore 用于隐藏文件夹应该是可能的,但我无法使用我共享的代码执行此操作。你有任何代码示例吗? DocumentsContract 允许从按 Mime 类型过滤并快速排序的文件夹中选择内容?就像 Java.io 文件一样? 您无法使用 DocumentsContract 选择文件。选择带有 ACTION_OPEN_DOCUMENT_TREE 的文件夹和带有 ACTION_OPEN_DOCUMENT 的文件。对于后者,您可以使用 mime 类型。 Using MediaStore for hidden folders should be possible but i am unable to do this using the code i shared.有可能。阅读我的答案!不要从不是来自您的应用程序的文件夹或文件开始。从属于您的应用的文件夹和文件开始。首先让您的 MediaStore 代码可用于您自己的文件。 但我不需要在我自己的文件上使用它,而且我共享的代码非常适用于其他应用程序(即 WhatsApp)文件。它完美地获取了 WhatsApp 图像

以上是关于如何在 Android 11 上使用 Media Store API 获取隐藏文件夹中的文件的主要内容,如果未能解决你的问题,请参考以下文章

android studio media player null对象引用

Android MediaPlayer 流错误:100:MEDIA_ERROR_SERVER_DIED

android如何实现返回桌面。。。

尝试在空对象上调用接口方法“android.media.session.ISessionController android.media.session.ISession.getController(

Android 11:无法从外部存储加载文件

AudioFormat在android.media.AudioFormat中不公开