web-dev-qa-db-ja.com

アクセスできるStorageVolumeとアクセスできないStorageVolumeを確認するにはどうすればよいですか?

バックグラウンド

グーグル(悲しいことに) ストレージのアクセス許可を台無しにする計画 アプリが標準を使用してファイルシステムにアクセスできないようにするファイルAPI(およびファイルパス)。多くは against it であり、アプリがストレージにアクセスする方法を変更し、多くの点で制限された制限付きのAPIです。

その結果、将来は完全にSAF(ストレージアクセスフレームワーク)を使用する必要がありますAndroidバージョン(Android Qでは、少なくとも一時的に、 フラグを使用してください 通常のストレージ権限を使用します)、さまざまなストレージボリュームを処理し、そこにあるすべてのファイルに到達する場合。

たとえば、ファイルマネージャーを作成してデバイスのすべてのストレージボリュームを表示し、ユーザーがアクセスを許可できるものを表示したいとします。すでにそれぞれにアクセス権がある場合は、それを入力するだけです。そのようなことは非常に正当なことのようですが、私にはそれを行う方法が見つからないので。

問題

API 24( here )以降、ようやくすべてのストレージボリュームを一覧表示できるようになりました。

_    val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val storageVolumes = storageManager.storageVolumes
_

そして、初めて、ストレージボリュームへのアクセスをリクエストするインテントを持つことができます( here )。したがって、たとえば、プライマリにアクセスを許可するようにユーザーに要求する場合(これは実際にはそこから始まり、実際には何も要求しない)、次のように使用できます。

_startActivityForResult(storageManager.primaryStorageVolume.createOpenDocumentTreeIntent(), REQUEST_CODE__DIRECTORTY_PERMISSION)
_

startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), REQUEST_CODE__DIRECTORTY_PERMISSION)の代わりに、ユーザーが正しいものを選択することを期待しています。

そして、最終的にユーザーが選択したものへのアクセスを取得するために、次のようにします。

_@TargetApi(Build.VERSION_CODES.KitKat)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_CODE__DIRECTORTY_PERMISSION && resultCode == Activity.RESULT_OK && data != null) {
        val treeUri = data.data ?: return
        contentResolver.takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        val pickedDir = DocumentFile.fromTreeUri(this, treeUri)
        ...
_

これまでのところ、さまざまなストレージボリュームに対する許可を要求できます...

ただし、許可を取得したものと許可していないものを知りたい場合に問題が発生します。

私が見つけたもの

  1. Googleの「Scoped Directory Access」に関するビデオがあります( here )。StorageVolumeクラスについて具体的に説明しています。 StorageVolumeのマウントイベントのリッスンについての情報も提供しますが、アクセスしたものの識別については何も通知しません。

  2. StorageVolumeクラスの唯一のIDは uuid ですが、何も返さないこともあります。実際、さまざまなケースでnullを返します。たとえば、プライマリストレージの場合。

  3. createOpenDocumentTreeIntent関数を使用しているときに、内部にUriが隠れていることに気づきました。これはエクストラの中にあり、「Android.provider.extra.INITIAL_URI」というキーに含まれています。たとえば、プライマリストレージでその値を確認すると、次のようになりました。

    content://com.Android.externalstorage.documents/root/primary

  4. OnActivityResultで返されたUriを見ると、#2と少し似ていますが、表示したtreeUri変数では異なります。

    content://com.Android.externalstorage.documents/tree/primary%3A

  5. これまでにアクセスできるもののリストを取得するには、 this を使用できます。

    val persistedUriPermissions = contentResolver.persistedUriPermissions

UriPermission のリストが返されます。それぞれにUriがあります。悲しいことに、それを使用すると、#3と同じになります。これは、StorageVolumeから取得したものと実際には比較できません。

_content://com.Android.externalstorage.documents/tree/primary%3A
_

ご覧のとおり、ストレージボリュームのリストとユーザーが付与するものとの間のマッピングは見つかりません。

createOpenDocumentTreeIntentの機能はユーザーをStorageVolumeに送信するだけなので、ユーザーがストレージボリュームを選択したかどうかさえもわかりませんが、代わりにフォルダーを選択することは可能です。

私が持っている唯一のことは、ここで他の質問で見つけた回避策のチャンクであり、特に私が実際にFile APIとfile-pathにアクセスできないので、それらは信頼できるとは思わない。

あなたがそれらが有用だと思う場合に備えて、私はそれらをここに書きました:

_@TargetApi(VERSION_CODES.Lollipop)
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
    final String docId = DocumentsContract.getTreeDocumentId(treeUri);
    final int end = docId.indexOf(':');
    String result = end == -1 ? null : docId.substring(0, end);
    return result;
}

private static String getDocumentPathFromTreeUri(final Uri treeUri) {
    final String docId = DocumentsContract.getTreeDocumentId(treeUri);
    //TODO avoid using spliting of a string (because it uses extra strings creation)
    final String[] split = docId.split(":");
    if ((split.length >= 2) && (split[1] != null))
        return split[1];
    else
        return File.separator;
}

public static String getFullPathOfDocumentFile(Context context, DocumentFile documentFile) {
    String volumePath = getVolumePath(context, getVolumeIdFromTreeUri(documentFile.getUri()));
    if (volumePath == null)
        return null;
    DocumentFile parent = documentFile.getParentFile();
    if (parent == null)
        return volumePath;
    final LinkedList<String> fileHierarchy = new LinkedList<>();
    while (true) {
        fileHierarchy.add(0, documentFile.getName());
        documentFile = parent;
        parent = documentFile.getParentFile();
        if (parent == null)
            break;
    }
    final StringBuilder sb = new StringBuilder(volumePath).append(File.separator);
    for (String fileName : fileHierarchy)
        sb.append(fileName).append(File.separator);
    return sb.toString();
}

/**
 * Get the full path of a document from its tree URI.
 *
 * @param treeUri The tree RI.
 * @return The path (without trailing file separator).
 */
public static String getFullPathFromTreeUri(Context context, final Uri treeUri) {
    if (treeUri == null)
        return null;
    String volumePath = getVolumePath(context, getVolumeIdFromTreeUri(treeUri));
    if (volumePath == null)
        return File.separator;
    if (volumePath.endsWith(File.separator))
        volumePath = volumePath.substring(0, volumePath.length() - 1);
    String documentPath = getDocumentPathFromTreeUri(treeUri);
    if (documentPath.endsWith(File.separator))
        documentPath = documentPath.substring(0, documentPath.length() - 1);
    if (documentPath.length() > 0)
        if (documentPath.startsWith(File.separator))
            return volumePath + documentPath;
        else return volumePath + File.separator + documentPath;
    return volumePath;
}

/**
 * Get the path of a certain volume.
 *
 * @param volumeId The volume id.
 * @return The path.
 */
private static String getVolumePath(Context context, final String volumeId) {
    if (VERSION.SDK_INT < VERSION_CODES.Lollipop)
        return null;
    try {
        final StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
        if (VERSION.SDK_INT >= VERSION_CODES.N) {
            final Class<?> storageVolumeClazz = StorageVolume.class;
            final Method getPath = storageVolumeClazz.getMethod("getPath");
            final List<StorageVolume> storageVolumes = storageManager.getStorageVolumes();
            for (final StorageVolume storageVolume : storageVolumes) {
                final String uuid = storageVolume.getUuid();
                final boolean primary = storageVolume.isPrimary();
                // primary volume?
                if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {
                    return (String) getPath.invoke(storageVolume);
                }
                // other volumes?
                if (uuid != null && uuid.equals(volumeId))
                    return (String) getPath.invoke(storageVolume);
            }
            return null;
        }
        final Class<?> storageVolumeClazz = Class.forName("Android.os.storage.StorageVolume");
        final Method getVolumeList = storageManager.getClass().getMethod("getVolumeList");
        final Method getUuid = storageVolumeClazz.getMethod("getUuid");
        //noinspection JavaReflectionMemberAccess
        final Method getPath = storageVolumeClazz.getMethod("getPath");
        final Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
        final Object result = getVolumeList.invoke(storageManager);
        final int length = Array.getLength(result);
        for (int i = 0; i < length; i++) {
            final Object storageVolumeElement = Array.get(result, i);
            final String uuid = (String) getUuid.invoke(storageVolumeElement);
            final Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
            // primary volume?
            if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {
                return (String) getPath.invoke(storageVolumeElement);
            }
            // other volumes?
            if (uuid != null && uuid.equals(volumeId))
                return (String) getPath.invoke(storageVolumeElement);
        }
        // not found.
        return null;
    } catch (Exception ex) {
        return null;
    }
}
_

質問

StorageVolumeのリストと許可されたUriPermissionのリストをどのようにマッピングできますか?

言い換えると、StorageVolumeのリストが与えられた場合、自分がアクセスできるものとアクセスできないものをどのようにして知ることができます。

14

ここにあなたが望むものを得る別の方法があります。リフレクションまたはファイルパスを使用せずに投稿したような回避策です。

エミュレータで、アクセスを許可した次の項目が表示されます。

persistedUriPermissions配列の内容(URIの値のみ):

0 uri = content://com.Android.externalstorage.documents/tree/primary%3A
1 uri = content://com.Android.externalstorage.documents/tree/1D03-2E0E%3ADownload
2 uri = content://com.Android.externalstorage.documents/tree/1D03-2E0E%3A
3 uri = content://com.Android.externalstorage.documents/tree/primary%3ADCIM
4 uri = content://com.Android.externalstorage.documents/tree/primary%3AAlarms

"%3A"はコロン( ":")です。そのため、URIは、「<volume>」がボリュームのUUIDであるボリュームに対して次のように構成されているようです。

uri = "content://com.Android.externalstorage.documents/tree/ <volume>:"

URIがボリュームの直下のディレクトリである場合、構造は次のようになります。

uri = "content://com.Android.externalstorage.documents/tree/ <volume>:<directory>"

構造のより深いディレクトリの場合、形式は次のとおりです。

uri = "content://com.Android.externalstorage.documents/tree/ <volume>:<directory>/<directory>/<directory> ..."

したがって、これらの形式のURIからボリュームを抽出するだけです。抽出されたボリュームは、StorageManager.storageVolumesのキーとして使用できます。次のコードはこれを行います。

これにはもっと簡単な方法があるはずだと私には思えます。ストレージボリュームとURIの間のAPIに不足しているリンケージが存在する必要があります。このテクニックがすべての状況をカバーしているとは言えません。

storageVolume.uuidから返されるUUIDについても質問します。これは32ビット値のようです。 UUIDの長さは128ビットだと思いました。これはUUIDの代替フォーマットですか、それともUUIDから派生したものですか?おもしろい、そしてもうすぐ降りる! :(

MainActivity.kt

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

        val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
        var storageVolumes = storageManager.storageVolumes
        val storageVolumePathsWeHaveAccessTo = HashSet<String>()

        checkAccessButton.setOnClickListener {
            checkAccessToStorageVolumes()
        }

        requestAccessButton.setOnClickListener {
            storageVolumes = storageManager.storageVolumes
            val primaryVolume = storageManager.primaryStorageVolume
            val intent = primaryVolume.createOpenDocumentTreeIntent()
            startActivityForResult(intent, 1)
        }
    }

    private fun checkAccessToStorageVolumes() {
        val storageVolumePathsWeHaveAccessTo = HashSet<String>()
        val persistedUriPermissions = contentResolver.persistedUriPermissions
        persistedUriPermissions.forEach {
            storageVolumePathsWeHaveAccessTo.add(it.uri.toString())
        }
        val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
        val storageVolumes = storageManager.storageVolumes

        for (storageVolume in storageVolumes) {
            val uuid = if (storageVolume.isPrimary) {
                // Primary storage doesn't get a UUID here.
                "primary"
            } else {
                storageVolume.uuid
            }
            val volumeUri = uuid?.let { buildVolumeUriFromUuid(it) }
            when {
                uuid == null -> 
                    Log.d("AppLog", "UUID is null for ${storageVolume.getDescription(this)}!")
                storageVolumePathsWeHaveAccessTo.contains(volumeUri) -> 
                    Log.d("AppLog", "Have access to $uuid")
                else -> Log.d("AppLog", "Don't have access to $uuid")
            }
        }
    }

    private fun buildVolumeUriFromUuid(uuid: String): String {
        return DocumentsContract.buildTreeDocumentUri(
            "com.Android.externalstorage.documents",
            "$uuid:"
        ).toString()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        Log.d("AppLog", "resultCode:$resultCode")
        val uri = data?.data ?: return
        val takeFlags =
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        contentResolver.takePersistableUriPermission(uri, takeFlags)
        Log.d("AppLog", "granted uri: ${uri.path}")
    }
}
1
Cheticamp

編集:回避策を見つけましたが、いつかは機能しない可能性があります。

リフレクションを使用してStorageVolumeインスタンスの実際のパスを取得し、以前持っていたものを使用してpersistedUriPermissionsのパスを取得します。それらの間に交差がある場合、それはstorageVolumeにアクセスできることを意味します。

内部ストレージとSDカードの両方を備えたエミュレータで動作するようです。

うまくいけば、適切なAPIが得られ、リフレクションを使用する必要はありません。

それを行うためのより良い方法があり、そのようなトリックがない場合は、私に知らせてください。

だから、ここに例があります:

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
        val storageVolumes = storageManager.storageVolumes
        val primaryVolume = storageManager.primaryStorageVolume
        checkAccessButton.setOnClickListener {
            val persistedUriPermissions = contentResolver.persistedUriPermissions
            val storageVolumePathsWeHaveAccessTo = HashSet<String>()
            Log.d("AppLog", "got access to paths:")
            for (persistedUriPermission in persistedUriPermissions) {
                val path = FileUtilEx.getFullPathFromTreeUri(this, persistedUriPermission.uri)
                        ?: continue
                Log.d("AppLog", "path: $path")
                storageVolumePathsWeHaveAccessTo.add(path)
            }
            Log.d("AppLog", "storage volumes:")
            for (storageVolume in storageVolumes) {
                val volumePath = FileUtilEx.getVolumePath(storageVolume)
                if (volumePath == null) {
                    Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - failed to get volumePath")
                } else {
                    val hasAccess = storageVolumePathsWeHaveAccessTo.contains(volumePath)
                    Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - volumePath:$volumePath - gotAccess? $hasAccess")
                }
            }
        }
        requestAccessButton.setOnClickListener {
            val intent = primaryVolume.createOpenDocumentTreeIntent()
            startActivityForResult(intent, 1)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        Log.d("AppLog", "resultCode:$resultCode")
        val uri = data?.data ?: return
        val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        contentResolver.takePersistableUriPermission(uri, takeFlags)
        val fullPathFromTreeUri = FileUtilEx.getFullPathFromTreeUri(this, uri)
        Log.d("AppLog", "granted uri:$uri $fullPathFromTreeUri")
    }
}

FileUtilEx.Java

/**
 * Get the full path of a document from its tree URI.
 *
 * @param treeUri The tree RI.
 * @return The path (without trailing file separator).
 */
public static String getFullPathFromTreeUri(Context context, final Uri treeUri) {
    if (treeUri == null)
        return null;
    String volumePath = getVolumePath(context, getVolumeIdFromTreeUri(treeUri));
    if (volumePath == null)
        return File.separator;
    if (volumePath.endsWith(File.separator))
        volumePath = volumePath.substring(0, volumePath.length() - 1);
    String documentPath = getDocumentPathFromTreeUri(treeUri);
    if (documentPath.endsWith(File.separator))
        documentPath = documentPath.substring(0, documentPath.length() - 1);
    if (documentPath.length() > 0)
        if (documentPath.startsWith(File.separator))
            return volumePath + documentPath;
        else return volumePath + File.separator + documentPath;
    return volumePath;
}

public static String getVolumePath(StorageVolume storageVolume){
    if (VERSION.SDK_INT < VERSION_CODES.Lollipop)
        return null;
    try{
        final Class<?> storageVolumeClazz = StorageVolume.class;
        final Method getPath = storageVolumeClazz.getMethod("getPath");
        return (String) getPath.invoke(storageVolume);
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    return null;
}

/**
 * Get the path of a certain volume.
 *
 * @param volumeId The volume id.
 * @return The path.
 */
@SuppressLint("ObsoleteSdkInt")
private static String getVolumePath(Context context, final String volumeId) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Lollipop)
        return null;
    try {
        final StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
        if (Android.os.Build.VERSION.SDK_INT >= Android.os.Build.VERSION_CODES.N) {
            final Class<?> storageVolumeClazz = StorageVolume.class;
            //noinspection JavaReflectionMemberAccess
            final Method getPath = storageVolumeClazz.getMethod("getPath");
            final List<StorageVolume> storageVolumes = storageManager.getStorageVolumes();
            for (final StorageVolume storageVolume : storageVolumes) {
                final String uuid = storageVolume.getUuid();
                final boolean primary = storageVolume.isPrimary();
                // primary volume?
                if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {
                    return (String) getPath.invoke(storageVolume);
                }
                // other volumes?
                if (uuid != null && uuid.equals(volumeId))
                    return (String) getPath.invoke(storageVolume);
            }
            return null;
        }
        final Class<?> storageVolumeClazz = Class.forName("Android.os.storage.StorageVolume");
        final Method getVolumeList = storageManager.getClass().getMethod("getVolumeList");
        final Method getUuid = storageVolumeClazz.getMethod("getUuid");
        //noinspection JavaReflectionMemberAccess
        final Method getPath = storageVolumeClazz.getMethod("getPath");
        final Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
        final Object result = getVolumeList.invoke(storageManager);
        final int length = Array.getLength(result);
        for (int i = 0; i < length; i++) {
            final Object storageVolumeElement = Array.get(result, i);
            final String uuid = (String) getUuid.invoke(storageVolumeElement);
            final Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
            // primary volume?
            if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {
                return (String) getPath.invoke(storageVolumeElement);
            }
            // other volumes?
            if (uuid != null && uuid.equals(volumeId))
                return (String) getPath.invoke(storageVolumeElement);
        }
        // not found.
        return null;
    } catch (Exception ex) {
        return null;
    }
}

/**
 * Get the document path (relative to volume name) for a tree URI (Lollipop).
 *
 * @param treeUri The tree URI.
 * @return the document path.
 */
@TargetApi(VERSION_CODES.Lollipop)
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
    final String docId = DocumentsContract.getTreeDocumentId(treeUri);
    //TODO avoid using spliting of a string (because it uses extra strings creation)
    final String[] split = docId.split(":");
    if ((split.length >= 2) && (split[1] != null))
        return split[1];
    else
        return File.separator;
}

/**
 * Get the volume ID from the tree URI.
 *
 * @param treeUri The tree URI.
 * @return The volume ID.
 */
@TargetApi(VERSION_CODES.Lollipop)
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
    final String docId = DocumentsContract.getTreeDocumentId(treeUri);
    final int end = docId.indexOf(':');
    String result = end == -1 ? null : docId.substring(0, end);
    return result;
}

activity_main.xml

<LinearLayout
  xmlns:Android="http://schemas.Android.com/apk/res/Android" xmlns:tools="http://schemas.Android.com/tools" Android:layout_width="match_parent" Android:layout_height="match_parent"
  Android:gravity="center" Android:orientation="vertical" tools:context=".MainActivity">

  <Button
    Android:id="@+id/checkAccessButton" Android:layout_width="wrap_content" Android:layout_height="wrap_content" Android:text="checkAccess"/>

  <Button
    Android:id="@+id/requestAccessButton" Android:layout_width="wrap_content" Android:layout_height="wrap_content" Android:text="requestAccess"/>

</LinearLayout>

簡単な関数に入れるには、次のようにします。

/** for each storageVolume, tells if we have access or not, via a HashMap (true for each iff we identified it has access*/
fun getStorageVolumesAccessState(context: Context): HashMap<StorageVolume, Boolean> {
    val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val storageVolumes = storageManager.storageVolumes
    val persistedUriPermissions = context.contentResolver.persistedUriPermissions
    val storageVolumePathsWeHaveAccessTo = HashSet<String>()
    //            Log.d("AppLog", "got access to paths:")
    for (persistedUriPermission in persistedUriPermissions) {
        val path = FileUtilEx.getFullPathFromTreeUri(context, persistedUriPermission.uri)
                ?: continue
        //                Log.d("AppLog", "path: $path")
        storageVolumePathsWeHaveAccessTo.add(path)
    }
    //            Log.d("AppLog", "storage volumes:")
    val result = HashMap<StorageVolume, Boolean>(storageVolumes.size)
    for (storageVolume in storageVolumes) {
        val volumePath = FileUtilEx.getVolumePath(storageVolume)
        val hasAccess = volumePath != null && storageVolumePathsWeHaveAccessTo.contains(volumePath)
        result[storageVolume] = hasAccess
    }
    return result
}
0