3

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

我试过什么?

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

  • 它为隐藏文件夹返回空光标.Statuses

  • 尝试替换MediaStore.Video.Media.EXTERNAL_CONTENT_URIMediaStore.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。

4

1 回答 1

-1

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

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

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

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

于 2021-11-18T08:17:55.957 回答