是否可以从 Android API 28 从数据库获取的 Uri 打开 PDF 文件?

Is it possible to open a PDF file from a database-fetched Uri from Android API 28?

我正在为 Android (>= API 28) 编写一个文件共享应用程序,我有以下用例:

  1. 用户从他们的本地存储上传文件。
  2. 用户关闭应用程序并在一段时间后重新打开它,导致应用程序向服务器请求文件列表以填充布局。
  3. 用户单击文件进行预览。

此时,最直观的步骤是,假设应用程序知道文件 URL,只需将其加载到指向 Google Docs' https://docs.google.com/viewerng/viewer?url=$myFileUrl 的 WebView 中或任何其他文件查看服务。

事实是,我不希望我的应用过度依赖外部服务,所以我想在文件上传的那一刻(第 1 步),一个 content:// Uri附加并存储在数据库中以在步骤 3 中使用它。

这样,如果用户没有重命名和移动他上传的文件,应用程序可以尝试在本地打开文件,从而节省一些带宽使用量。

正如@CommonsWare 在此 article 中所写,应用程序有权访问 Uri 的时间是有限的;在我的例子中,它只会持续到用户关闭应用程序。这意味着使用如下策略:

MainActivity.kt

// Getting the content:// Uri after a call to the server.
viewModel.selectedUrl.observe(this@MainActivity) { uri ->
   val newIntent = Intent(Intent.ACTION_VIEW)
   try {
      newIntent.data = Uri.parse(uri)
      newIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
      newIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
      try {
         startActivity(newIntent)
      } catch (e: ActivityNotFoundException) {
         Toast.makeText(this, e.toString(), Toast.LENGTH_LONG).show()
      }
   // ...
}

仅当文件已上传并立即打开时才有效。有没有可能以任何方式解决这个问题,或者我应该坚持不涉及任何 UriWebView 选项?

更新:文件是如何上传的?

在我的 MainActivity 中,我有一个 FAB,点击它会触发以下片段:

MainActivity.kt

val fileResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        viewModel.uploadFile(result, false) // This is the Uri.
    }

fileFab.setOnClickListener {
    val intent = Intent()
    intent.type = "application/*"
    intent.action = Intent.ACTION_OPEN_DOCUMENT
    fileResultLauncher.launch(intent)
}

...在各自的 ViewModel.ktRepository.kt 调用之后它最终在 Server class 中使用以下方法:

Server.kt

override fun uploadFile(uri: Uri, title: String, isSound: Boolean, callback: IFileUploadCallback) {
    Timber.d("Uploading document...")
    val inputStream = c.contentResolver.openInputStream(uri)
    var fileData: ByteArray? = null
    inputStream?.buffered()?.use {
        fileData = it.readBytes()
    }
    fileData?: return

    // Call to Volley
    // ...

感谢@blackapps 和@CommonsWare,我的 ViewModel class 中缺少以下行:

context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)

ViewModel.kt

fun uploadFile(result: ActivityResult) {
    if (result.resultCode == Activity.RESULT_OK) {
        val uri: Uri = result.data?.data!!
        context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
        viewModelScope.launch(Dispatchers.IO) {
            repository.uploadFile(uri, object: Server.IFileUploadCallback {
                override fun onSuccess(fileUrl: String, content: String) {
                    uploadedFileUrl.postValue(RecyclerViewItem.Document(fileUrl, content, uri))
                }
            })
        }
    }
}