背景
があるAndroid 10および11のさまざまなストレージ制限これには新しい権限(外部ストレージの管理) はすべてのファイルにアクセスできるようになりました (ただし、実際にはすべてのファイルへのアクセスは許可されません)。一方、以前のストレージ権限は、メディア ファイルへのアクセスのみを許可するように縮小されました。
- アプリは「メディア」サブフォルダに自由にアクセスできます。
- アプリは「データ」サブフォルダ、特にコンテンツにアクセスできません。
- 「obb」フォルダについては、アプリのインストールが許可されていれば、そこにアクセスできます(そこにファイルをコピーするため)。そうでない場合はアクセスできません。
- USB またはルートを使用すれば、引き続きアクセスできます。また、エンド ユーザーは、組み込みのファイル マネージャー アプリ「Files」を介してアクセスできます。
問題
この制限を何とか克服したアプリに気づいた(ここ)と呼ばれる探検「Android/data」フォルダに入ると、(何らかの方法で SAF を直接使用して)アクセス権の付与を求められます。アクセス権を付与すると、「Android」フォルダ内のすべてのフォルダ内のすべてにアクセスできるようになります。
つまり、まだ到達する方法があるかもしれないが、問題は、何らかの理由で同じことを実行するサンプルを作成できなかったことです。
私が見つけたもの、試したこと
このアプリはAPI 29(Android 10)をターゲットにしているようですが、まだ新しい権限を使用しておらず、フラグが設定されているようです。リクエストLegacyExternalStorage彼らが使用する同じトリックが API 30 をターゲットにするときに機能するかどうかはわかりませんが、Android 11 を搭載した Pixel 4 で実行している私のケースでは、問題なく動作すると言えます。
そこで私も同じことをやってみました:
Android API 29 をターゲットとし、レガシー フラグを含む (あらゆる種類の) ストレージ権限が付与されたサンプル POC を作成しました。
「Android」フォルダへの直接アクセスを要求しようとしました(ここ)、残念ながら何らかの理由で機能しませんでした(DCIM フォルダーに移動し続けましたが、理由はわかりません)。
val androidFolderDocumentFile = DocumentFile.fromFile(File(primaryVolume.directory!!, "Android"))
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
.putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidFolderDocumentFile.uri)
startActivityForResult(intent, 1)
色々なフラグの組み合わせを試してみました。
アプリを起動すると、自分で手動で「Android」フォルダにアクセスしますが、うまく機能しなかったため、他のアプリと同様にこのフォルダへのアクセスを許可しました。
結果を取得するときに、パス内のファイルとフォルダーを取得しようとしますが、取得に失敗します。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d("AppLog", "resultCode:$resultCode")
val uri = data?.data ?: return
if (!DocumentFile.isDocumentUri(this, uri))
return
grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val fullPathFromTreeUri = FileUtilEx.getFullPathFromTreeUri(this, uri) // code for this here: https://stackoverflow.com/q/56657639/878126
val documentFile = DocumentFile.fromTreeUri(this, uri)
val listFiles: Array<DocumentFile> = documentFile!!.listFiles() // this returns just an array of a single folder ("media")
val androidFolder = File(fullPathFromTreeUri)
androidFolder.listFiles()?.forEach {
Log.d("AppLog", "${it.absoluteFile} children:${it.listFiles()?.joinToString()}") //this does find the folders, but can't reach their contents
}
Log.d("AppLog", "granted uri:$uri $fullPathFromTreeUri")
}
したがって、DocumentFile.fromTreeUri を使用すると、役に立たない「media」フォルダーしか取得できず、File クラスを使用すると、「data」フォルダーと「obb」フォルダーもあることしか確認できませんでしたが、その内容にアクセスできませんでした...
つまり、これはまったくうまくいきませんでした。
その後、このトリックを使用する別のアプリを見つけました。「ミックスプローラーこのアプリでは、「Android」フォルダを直接要求できませんでした (おそらく試みさえしなかった) が、許可すると、そのフォルダとそのサブフォルダへのフル アクセスが付与されます。また、API 30 をターゲットにしているため、API 29 をターゲットにしているからといって機能するわけではありません。
コードにいくつか変更を加えると、各サブフォルダーへのアクセスを個別に要求できることに気づきました (つまり、「データ」の要求と「obb」の新しい要求)。ただし、これはここで表示されるアプリが行うものではありません。
つまり、「Android」フォルダにアクセスするには、この Uri を Intent.EXTRA_INITIAL_URI のパラメータとして使用します。
val androidUri=Uri.Builder().scheme("content").authority("com.android.externalstorage.documents")
.appendEncodedPath("tree").appendPath("primary:").appendPath("document").appendPath("primary:Android").build()
ただし、一度アクセスすると、ファイル経由でも SAF 経由でも、そこからファイルのリストを取得できなくなります。
しかし、私が書いたように、奇妙なことに、代わりに「Android/data」にアクセスする同様の操作を試みた場合、そのコンテンツを取得できるようになります。
val androidDataUri=Uri.Builder().scheme("content").authority("com.android.externalstorage.documents")
.appendEncodedPath("tree").appendPath("primary:").appendPath("document").appendPath("primary:Android/data").build()
質問
- 実際に「Android」フォルダーにアクセスし、サブフォルダーとその内容を取得できるように、Intent を直接「Android」フォルダーに要求するにはどうすればよいですか?
- これに代わる別の方法はありますか? adb や root を使用して、この特定のフォルダーへの SAF アクセスを許可できますか?
ベストアンサー1
X-plore での動作は次のとおりです。
オンの場合Build.VERSION.SDK_INT>=30
、[内部ストレージ]/Android/data にアクセスできず、javaFile.canRead()
またはFile.canWrite()
false が返されるため、このフォルダー内のファイル (およびおそらく obb) に対して代替ファイル システムに切り替える必要があります。
ストレージ アクセス フレームワークがどのように機能するかはすでにご存知だと思いますので、具体的に何を行う必要があるかについて詳しく説明します。
このフォルダに対する保存済みの権限があるかどうかを確認するために呼び出しますContentResolver.getPersistedUriPermissions()
。最初は権限がないので、ユーザーに権限を要求します。
アクセスを要求するには、 を使用します。ここでは、Android フォルダーにアクセスする必要があるため、ピッカーがプライマリ ストレージ上の Android フォルダーで直接起動するように設定しますstartActivityForResult
。アプリが API30 をターゲットとする場合、ピッカーはストレージのルートを選択できません。また、Android フォルダーへのアクセス許可を取得することで、1 つのアクセス許可要求で、内部のフォルダーとフォルダーの両方を操作できます。Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).putExtra(DocumentsContract.EXTRA_INITIAL_URI, DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Android"))
EXTRA_INITIAL_URI
data
obb
ユーザーが 2 回クリックして確認すると、 のonActivityResult
URI がデータに取得されますcontent://com.android.externalstorage.documents/tree/primary%3AAndroid
。 ユーザーが正しいフォルダーを確認したことを確認するために必要なチェックを行います。 次に、 を呼び出してcontentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION)
アクセス許可を保存すれば準備完了です。
そこで に戻りますContentResolver.getPersistedUriPermissions()
。ここには付与された権限のリストが含まれています (他にもある可能性があります)。上で付与した権限は次のようになります: UriPermission {uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid, modeFlags=3}
( で取得したのと同じ URI onActivityResult
)。 からのリストを反復処理してgetPersistedUriPermissions
目的の URI を見つけ、見つかった場合はそれを使用して作業し、見つからない場合はユーザーに付与を依頼します。
ここで、この「ツリー」URI と Android フォルダー内のファイルへの相対パスを使用してContentResolver
作業します。フォルダーを一覧表示する例を次に示します。は、許可された「ツリー」URI への相対パスです。 (ファイルを一覧表示する) または(個々のファイルを操作するため)を使用して最終的な URI を構築します。例: の場合、データ フォルダー内のファイルを一覧表示するのに適した uri= が得られます。次に、を呼び出してデータを処理して、フォルダーの一覧を取得します。DocumentsContract
data
data/
DocumentsContract.buildChildDocumentsUriUsingTree()
DocumentsContract.buildDocumentUriUsingTree()
DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri), DocumentsContract.getTreeDocumentId(treeUri)+"/data/")
content://com.android.externalstorage.documents/tree/primary%3AAndroid/document/primary%3AAndroid%2Fdata%2F/children
ContentResolver.query(uri, ...)
Cursor
同様の方法で、おそらく既にご存知の、読み取り/書き込み/名前変更/移動/削除/作成などの他の SAF 機能も、ContentResolver
または メソッドを使用して操作しますDocumentsContract
。
詳細:
- 必要ありません
android.permission.MANAGE_EXTERNAL_STORAGE
- ターゲットAPI 29または30で動作します
- プライマリストレージでのみ機能し、外部SDカードでは機能しません。
- データフォルダ内のすべてのファイルについては、SAFを使用する必要があります(Javaファイルは機能しません)。Androidフォルダを基準としたファイル階層を使用するだけです。
- 将来、Googleは「セキュリティ」の意図でこの穴を修正する可能性があり、セキュリティアップデート後には機能しなくなる可能性があります。
編集: Cheticamp に基づくサンプルコードGithubサンプルサンプルでは、「Android」フォルダの各サブフォルダの内容 (およびファイル数) が表示されます。
class MainActivity : AppCompatActivity() {
private val handleIntentActivityResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode != Activity.RESULT_OK)
return@registerForActivityResult
val directoryUri = it.data?.data ?: return@registerForActivityResult
contentResolver.takePersistableUriPermission(
directoryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
if (checkIfGotAccess())
onGotAccess()
else
Log.d("AppLog", "you didn't grant permission to the correct folder")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
val openDirectoryButton = findViewById<FloatingActionButton>(R.id.fab_open_directory)
openDirectoryButton.setOnClickListener {
openDirectory()
}
}
private fun checkIfGotAccess(): Boolean {
return contentResolver.persistedUriPermissions.indexOfFirst { uriPermission ->
uriPermission.uri.equals(androidTreeUri) && uriPermission.isReadPermission && uriPermission.isWritePermission
} >= 0
}
private fun onGotAccess() {
Log.d("AppLog", "got access to Android folder. showing content of each folder:")
@Suppress("DEPRECATION")
File(Environment.getExternalStorageDirectory(), "Android").listFiles()?.forEach { androidSubFolder ->
val docId = "$ANDROID_DOCID/${androidSubFolder.name}"
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(androidTreeUri, docId)
val contentResolver = this.contentResolver
Log.d("AppLog", "content of:${androidSubFolder.absolutePath} :")
contentResolver.query(childrenUri, null, null, null)
?.use { cursor ->
val filesCount = cursor.count
Log.d("AppLog", "filesCount:$filesCount")
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val mimeIndex = cursor.getColumnIndex("mime_type")
while (cursor.moveToNext()) {
val displayName = cursor.getString(nameIndex)
val mimeType = cursor.getString(mimeIndex)
Log.d("AppLog", " $displayName isFolder?${mimeType == DocumentsContract.Document.MIME_TYPE_DIR}")
}
}
}
}
private fun openDirectory() {
if (checkIfGotAccess())
onGotAccess()
else {
val primaryStorageVolume = (getSystemService(STORAGE_SERVICE) as StorageManager).primaryStorageVolume
val intent =
primaryStorageVolume.createOpenDocumentTreeIntent().putExtra(EXTRA_INITIAL_URI, androidUri)
handleIntentActivityResult.launch(intent)
}
}
companion object {
private const val ANDROID_DOCID = "primary:Android"
private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
private val androidUri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
private val androidTreeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
}
}