如何在 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
文件夹Uri
和Document File
。使用listFiles()
我可以列出文件,但使用filter()
和sortedByDescending()
函数会变得非常慢。
我尝试了什么?
使用带有投影和选择参数的光标加载器,但它只是
适用于WhatsApp Images
和WhatsApp 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.media.session.ISessionController android.media.session.ISession.getController(