我们如何访问 Android Q 中的扩展文件?

How can we access an expansion file in Android Q?

我正在编写一个需要扩展文件的应用程序,我想确保它与 Android Q 兼容。documentation provided 似乎没有解决 [=15] 中的更改=] Q. 在Android Q中,getExternalStorageDirectory()将无法使用,那么我们如何访问扩展文件?

从问题中链接的 documentation 中,我们知道扩展文件的名称具有以下形式:

[main|patch].<expansion-version>.<package-name>.obb

getObbDir()方法returns展开文件的具体位置如下形式:

<shared-storage>/Android/obb/<package-name>/

那么,问题是我们如何访问这些文件?

为了回答这个问题,我获取了一个包含五个 APK 文件的目录,并使用 JOBB 创建了一个名为 "main.314159.com.example.opaquebinaryblob.obb" 的 OBB 文件。我的目的是装载和读取此 OBB 文件以在小型演示应用程序中显示 APK 文件名和每个 APK 中的条目数(读取为 Zip 文件)。

演示应用程序还将尝试create/read 测试外部存储目录下各个目录中的文件。

以下是在 Pixel XL 模拟器上执行的 运行 最新可用版本 "Q" (Android 10.0 (Google APIs)).该应用程序具有以下特点:

  • targetSdkVersion 29
  • minSdkVersion 18
  • 没有明确的权限 清单中指定

我往前看了看这个小app在什么目录getObbDir() returns,发现是

/storage/emulated/0/Android/obb/com.example.opaquebinaryblob

所以我上传了我的 OBB 文件到

/storage/emulated/0/Android/obb/com.example.opaquebinaryblob/main.314159.com.example.opaquebinaryblob.obb

使用 Android Studio。这是文件结束的地方。

那么,我们可以挂载和读取这个OBB文件吗?我们可以 create/read 外部文件路径中其他目录中的文件吗?以下是应用程序在 API 29 上的报告:

唯一可访问的文件位于 /storage/emulated/0/Android/obb/com.example.opaquebinaryblob。层次结构中的其他文件无法创建或读取。 (不过,有趣的是,可以确定这些文件的存在。)

如上图,app打开OBB文件直接读取,无需挂载

当我们尝试装载 OBB 文件并转储其内容时,报告如下:

这是我们所期望的。简而言之,看起来 Android Q 正在限制对外部文件目录的访问,同时允许基于应用程序包名称的目标访问。

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var myObbFile: File
    private lateinit var mStorageManager: StorageManager

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

        obbDumpText.movementMethod = ScrollingMovementMethod()

        val sb = StringBuilder()

        val extStorageDir = Environment.getExternalStorageDirectory()
        sb.appendln("getExternalStorageDirectory() reported at $extStorageDir").appendln()
        myObbFile = File(obbDir, BLOB_FILE_NAME)

        val obbDir = obbDir
        sb.appendln("obbDir reported at $obbDir").appendln()
        myObbFile = File(obbDir, BLOB_FILE_NAME)

        val directoryPathList = listOf(
            "$extStorageDir",
            "$extStorageDir/Pictures",
            "$extStorageDir/Android/obb/com.example.anotherpackage",
            "$extStorageDir/Android/obb/$packageName"
        )
        var e: Exception?
        for (directoryPath in directoryPathList) {
            val fileToCheck = File(directoryPath, TEST_FILE_NAME)
            e = checkFileReadability(fileToCheck)
            if (e == null) {
                sb.appendln("$fileToCheck is accessible.").appendln()
            } else {
                sb.appendln(e.message)
                try {
                    sb.appendln("Trying to create $fileToCheck")
                    fileToCheck.createNewFile()
                    sb.appendln("Created $fileToCheck")
                    e = checkFileReadability(fileToCheck)
                    if (e == null) {
                        sb.appendln("$fileToCheck is accessible").appendln()
                    } else {
                        sb.appendln("e").appendln()
                    }
                } catch (e: Exception) {
                    sb.appendln("Could not create $fileToCheck").appendln(e).appendln()
                }
            }
        }

        if (!myObbFile.exists()) {
            sb.appendln("OBB file doesn't exist: $myObbFile").appendln()
            obbDumpText.text = sb.toString()
            return
        }

        e = checkFileReadability(myObbFile)
        if (e != null) {
            // Need to request READ_EXTERNAL_STORAGE permission before reading OBB file
            sb.appendln("Need READ_EXTERNAL_STORAGE permission.").appendln()
            obbDumpText.text = sb.toString()
            return
        }

        sb.appendln("OBB is accessible at")
            .appendln(myObbFile).appendln()

        mStorageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
        obbDumpText.text = sb.toString()
    }

    private fun dumpMountedObb(obbMountPath: String) {
        val obbFile = File(obbMountPath)

        val sb = StringBuilder().appendln("Dumping OBB...").appendln()
        sb.appendln("OBB file path is $myObbFile").appendln()
        sb.appendln("OBB mounted at $obbMountPath").appendln()
        val listFiles = obbFile.listFiles()
        if (listFiles == null || listFiles.isEmpty()) {
            Log.d(TAG, "No files in obb!")
            return
        }
        sb.appendln("Contents of OBB").appendln()
        for (listFile in listFiles) {
            val zipFile = ZipFile(listFile)
            sb.appendln("${listFile.name} has ${zipFile.entries().toList().size} entries.")
                .appendln()
        }
        obbDumpText.text = sb.toString()
    }

    private fun checkFileReadability(file: File): Exception? {
        if (!file.exists()) {
            return IOException("$file does not exist")
        }

        var inputStream: FileInputStream? = null
        try {
            inputStream = FileInputStream(file).also { input ->
                input.read()
            }
        } catch (e: IOException) {
            return e
        } finally {
            inputStream?.close()
        }
        return null
    }

    fun onClick(view: View) {
        mStorageManager.mountObb(
            myObbFile.absolutePath,
            null,
            object : OnObbStateChangeListener() {
                override fun onObbStateChange(path: String, state: Int) {
                    super.onObbStateChange(path, state)
                    val mountPath = mStorageManager.getMountedObbPath(myObbFile.absolutePath)
                    dumpMountedObb(mountPath)
                }
            }
        )
    }

    companion object {
        const val BLOB_FILE_NAME = "main.314159.com.example.opaquebinaryblob.obb"
        const val TEST_FILE_NAME = "TestFile.txt"
        const val TAG = "MainActivity"
    }
}

activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="16dp"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/obbDumpText"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scrollbars="vertical"
        android:text="Click the button to view content of the OBB."
        android:textColor="@android:color/black"
        app:layout_constraintBottom_toTopOf="@+id/dumpMountObb"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="spread_inside" />

    <Button
        android:id="@+id/dumpMountObb"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="Dump\nMounted OBB"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/obbDumpText"
        app:layout_constraintVertical_bias="0.79" />
</androidx.constraintlayout.widget.ConstraintLayout>

对于 follow-up 如所述 here:

Since Android 4.4 (API level 19), apps can read OBB expansion files without external storage permission. However, some implementations of Android 6.0 (API level 23) and later still require permission, so you will need to declare the READ_EXTERNAL_STORAGE permission in the app manifest and ask for permission at runtime...

这是否适用于 Android 问?不清楚。该演示表明它不适用于模拟器。我希望这是跨设备一致的。