web-dev-qa-db-ja.com

新しいLollipopAPIを使用して、すべてのSDカードにアクセスするにはどうすればよいですか?

バックグラウンド

Lollipop以降、アプリは実際のSDカードにアクセスできるようになります(KitKatでアクセスできず、以前のバージョンではまだ公式にはサポートされていませんでした) ここ について質問しました。

問題

SDカードをサポートするLollipopデバイスを見るのは非常にまれになり、エミュレーターにはSDカードサポートをエミュレートする機能が実際にはない(または機能しない)ため、テストにかなりの時間がかかりました。 。

とにかく、SDカードにアクセスするために通常のファイルクラスを使用する代わりに(許可を得たら)、DocumentFileを使用してUrisを使用する必要があるようです。

これにより、URIをパスに、またはその逆に変換する方法が見つからないため、通常のパスへのアクセスが制限されます(さらに、非常に面倒です)。また、現在のSDカードにアクセスできるかどうかを確認する方法がわからないため、ユーザーにSDカード(またはSDカード)への読み取り/書き込みの許可をいつ要求するかがわかりません。

私が試したこと

現在、これは私がすべてのSDカードへのパスを取得する方法です:

  /**
   * returns a list of all available sd cards paths, or null if not found.
   *
   * @param includePrimaryExternalStorage set to true if you wish to also include the path of the primary external storage
   */
  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  public static List<String> getExternalStoragePaths(final Context context,final boolean includePrimaryExternalStorage)
    {
    final File primaryExternalStorageDirectory=Environment.getExternalStorageDirectory();
    final List<String> result=new ArrayList<>();
    final File[] externalCacheDirs=ContextCompat.getExternalCacheDirs(context);
    if(externalCacheDirs==null||externalCacheDirs.length==0)
      return result;
    if(externalCacheDirs.length==1)
      {
      if(externalCacheDirs[0]==null)
        return result;
      final String storageState=EnvironmentCompat.getStorageState(externalCacheDirs[0]);
      if(!Environment.MEDIA_MOUNTED.equals(storageState))
        return result;
      if(!includePrimaryExternalStorage&&VERSION.SDK_INT>=VERSION_CODES.HONEYCOMB&&Environment.isExternalStorageEmulated())
        return result;
      }
    if(includePrimaryExternalStorage||externalCacheDirs.length==1)
      {
      if(primaryExternalStorageDirectory!=null)
        result.add(primaryExternalStorageDirectory.getAbsolutePath());
      else
        result.add(getRootOfInnerSdCardFolder(externalCacheDirs[0]));
      }
    for(int i=1;i<externalCacheDirs.length;++i)
      {
      final File file=externalCacheDirs[i];
      if(file==null)
        continue;
      final String storageState=EnvironmentCompat.getStorageState(file);
      if(Environment.MEDIA_MOUNTED.equals(storageState))
        result.add(getRootOfInnerSdCardFolder(externalCacheDirs[i]));
      }
    return result;
    }


private static String getRootOfInnerSdCardFolder(File file)
  {
  if(file==null)
    return null;
  final long totalSpace=file.getTotalSpace();
  while(true)
    {
    final File parentFile=file.getParentFile();
    if(parentFile==null||parentFile.getTotalSpace()!=totalSpace)
      return file.getAbsolutePath();
    file=parentFile;
    }
  }

これは、私が到達できるUrisを確認する方法です。

final List<UriPermission> persistedUriPermissions=getContentResolver().getPersistedUriPermissions();

SDカードにアクセスする方法は次のとおりです。

startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE),42);

public void onActivityResult(int requestCode,int resultCode,Intent resultData)
  {
  if(resultCode!=RESULT_OK)
    return;
  Uri treeUri=resultData.getData();
  DocumentFile pickedDir=DocumentFile.fromTreeUri(this,treeUri);
  grantUriPermission(getPackageName(),treeUri,Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  getContentResolver().takePersistableUriPermission(treeUri,Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  }

質問

  1. 現在のSDカードにアクセスできるかどうかを確認し、アクセスできない場合は、どういうわけかユーザーにアクセス許可を求めることはできますか?

  2. DocumentFile URIと実際のパスを変換する公式の方法はありますか? this answer を見つけましたが、私の場合はクラッシュし、さらにハックっぽく見えます。

  3. 特定のパスについてユーザーに許可を要求することは可能ですか? 「はい/いいえを受け入れますか?」というダイアログだけを表示することもできますか?

  4. 権限が付与された後、DocumentFile APIの代わりに通常のファイルAPIを使用することは可能ですか?

  5. ファイル/ファイルパスが与えられた場合、それにアクセスするための許可を要求する(そしてそれが以前に与えられているかどうかを確認する)こと、またはそのルートパスを要求することは可能ですか?

  6. エミュレータにSDカードを持たせることはできますか?現在、「SDカード」と記載されていますが、プライマリ外部ストレージとして機能しているので、新しいAPIを試して使用するために、セカンダリ外部ストレージを使用してテストしたいと思います。

それらの質問のいくつかについては、一方が他方に答えるのに大いに役立つと思います。

25

現在のSDカードにアクセスできるかどうかを確認し、アクセスできない場合は、どういうわけかユーザーにアクセス許可を求めることはできますか?

これには、検出、カードの種類、カードがマウントされているかどうかの確認、アクセスの要求の3つの部分があります。

デバイスの製造元が優れている場合は、null引数を指定して getExternalFiles を呼び出すことで「外部」ストレージのリストを取得できます(関連するjavadocを参照)。

彼らはいい人ではないかもしれません。そして、誰もが定期的に更新される最新の、最もホットで、バグのないOSバージョンを持っているわけではありません。そのため、そこにリストされていないディレクトリ(OTG USBストレージなど)が存在する可能性があります。疑わしい場合は、ファイル/proc/self/mountsからOSマウントの完全なリストを取得できます。このファイルはLinuxカーネルの一部であり、その形式は文書化されています ここ/proc/self/mountsの内容をバックアップとして使用できます。解析し、使用可能なファイルシステム(fat、ext3、ext4など)を見つけ、getExternalFilesDirsの出力で重複を削除します。 「その他のディレクトリ」など、目立たない名前で残りのオプションをユーザーに提供します。これにより、外部、内部、あらゆるストレージが可能になります。


[〜#〜]編集[〜#〜]:上記のアドバイスをした後、私はついに自分でそれに従うことを試みました。これまでは問題なく動作しましたが、/proc/self/mountsの代わりに/pros/self/mountinfoを解析した方がよいことに注意してください(前者は引き続き利用可能です)最新のLinuxでは、しかし後でより良い、より堅牢な代替品です)。また、マウントリストの内容に基づいて仮定を行う場合は、 原子性の問題 も考慮に入れてください。


canRead および canWrite を呼び出すことにより、ディレクトリが読み取り可能/書き込み可能かどうかを素朴に確認できます。これが成功した場合、余分な作業を行う意味はありません。そうでない場合は、永続的なUri権限を持っているか、持っていないかのどちらかです。

「許可を得る」というのは醜い部分です。 AFAIK、SAFインフラストラクチャ内でそれをクリーンに行う方法はありません。 Intent.ACTION_PICKmayが機能するように聞こえますが(Uriを受け入れ、そこから選択するため)、機能しません。たぶん、これはバグと見なすことができ、Androidバグトラッカーとして報告する必要があります。

ファイル/ファイルパスが与えられた場合、それにアクセスするための許可を要求する(そしてそれが以前に与えられているかどうかを確認する)こと、またはそのルートパスを要求することは可能ですか?

これがACTION_PICKの目的です。繰り返しになりますが、SAFピッカーは箱から出してACTION_PICKをサポートしていません。サードパーティのファイルマネージャは可能性がありますが、実際に実際にアクセスを許可するものはほとんどありません。気になったら、これもバグとして報告することができます。


[〜#〜] edit [〜#〜]:この回答はAndroid Nougatが出る前に書かれました。As API 24の場合でも、特定のディレクトリへのきめ細かいアクセスを要求することはできませんが、少なくともボリューム全体へのアクセスを動的に要求できます。 ボリュームを決定 、ファイルを含み、アクセスを要求します。 getAccessIntentnull引数(セカンダリボリュームの場合)を使用するか、WRITE_EXTERNAL_STORAGE権限(プライマリボリュームの場合)を要求します。


DocumentFile URIと実際のパスを変換する公式の方法はありますか?私はこの答えを見つけました、しかしそれは私の場合に墜落しました、そしてそれはハックのように見えます。

いいえ、ありません。 Storage Access Frameworkの作成者の気まぐれに、いつまでも従順であり続ける必要があります。 悪の笑い

実際には、はるかに簡単な方法があります。URIを開いて、作成された記述子のファイルシステムの場所を確認するだけです(簡単にするために、Lollipopのみのバージョン)。

public String getFilesystemPath(Context context, Uri uri) {
  ContentResolver res = context.getContentResolver();

  String resolved;
  try (ParcelFileDescriptor fd = res.openFileDescriptor(someSafUri, "r")) {
    final File procfsFdFile = new File("/proc/self/fd/" + fd.getFd());

    resolved = Os.readlink(procfsFdFile.getAbsolutePath());

    if (TextUtils.isEmpty(resolved)
          || resolved.charAt(0) != '/'
          || resolved.startsWith("/proc/")
          || resolved.startsWith("/fd/"))
    return null;
  } catch (Exception errnoe) {
    return null;
  }
}

上記のメソッドが場所を返す場合でも、Fileで使用するにはアクセスが必要です。場所が返されない場合、問題のURIはファイルを参照しません(一時的なものであっても)。それは、ネットワークストリーム、Unixパイプなどです。上記のメソッドのバージョンは、古いAndroidバージョンから この回答 から取得できます。anyUri、any UriをopenFileDescriptorで開くことができる限り(たとえば、Intent.CATEGORY_OPENABLEからのものである限り)、SAFだけでなくContentProviderも使用できます。

公式のLinuxAPIの一部をそのように考える場合、上記のアプローチは公式と見なすことができることに注意してください。多くのLinuxソフトウェアがこれを使用しており、一部のAOSPコード(Launcher3テストなど)でも使用されていることを確認しました。


[〜#〜] edit [〜#〜]:Android Nougatが導入した数 セキュリティの変更 、特にアプリケーションのプライベートディレクトリの権限の変更。これは、API 24以降、Uriが内部のファイルを参照する場合、上記のスニペットは常に例外で失敗することを意味します。アプリケーションのプライベートディレクトリ。これは意図したとおりです。そのパスをまったく知る必要はありません。他の方法でファイルシステムのパスを決定しても、にアクセスすることはできません。そのパスを使用するファイル。他のアプリケーションがあなたと協力してファイルのアクセス許可を誰でも読み取り可能なものに変更しても、アクセスすることはできません。これは、 Linuxはファイルへのアクセスを許可しないためです。パス内のディレクトリの1つに検索アクセス権がない場合 。したがって、ContentProviderからファイル記述子を受信することが、それらにアクセスする唯一の方法です。


権限が付与された後、DocumentFile APIの代わりに通常のファイルAPIを使用することは可能ですか?

できません。少なくともヌガーではありません。通常のファイルAPIは、パーミッションのためにLinuxカーネルに送られます。 Linuxカーネルによると、外部SDカードには制限付きの権限があり、アプリがそれを使用できないようになっています。 Storage Access Frameworkによって与えられるアクセス許可は、SAF(IIRCがいくつかのxmlファイルに格納されている)によって管理され、カーネルはそれらについて何も知りません。外部ストレージにアクセスするには、中間パーティ(ストレージアクセスフレームワーク)を使用する必要があります。 Linuxカーネルには、ディレクトリサブツリーへのアクセスを管理するための独自のメカニズム( bind-mounts と呼ばれます)がありますが、Storage Access Frameworkの作成者は、それについて知らないか、使用したくないことに注意してください。それ。

Uriから作成されたファイル記述子を使用することで、ある程度のアクセス(Fileで実行できるほとんどすべてのもの)を取得できます。 この回答 を読むことをお勧めします。一般的なファイル記述子の使用とAndroidに関連するいくつかの役立つ情報が含まれている場合があります。

11
user1643723

これにより、URIをパスに、またはその逆に変換する方法が見つからないため、通常のパスへのアクセスが制限されます(さらに、非常に面倒です)。また、現在のSDカードにアクセスできるかどうかを確認する方法がわからないため、ユーザーにSDカード(またはSDカード)への読み取り/書き込みの許可を求めるタイミングがわかりません。

API 19(KitKat)以降、nonpublic Android class StorageVolumeを使用して、リフレクションを使用して質問に対する回答を取得できます。

public static Map<String, String> getSecondaryMountedVolumesMap(Context context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Object[] volumes;

    StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
    Method getVolumeListMethod = sm.getClass().getMethod("getVolumeList");
    volumes = (Object[])getVolumeListMethod.invoke(sm);

    Map<String, String> volumesMap = new HashMap<>();
    for (Object volume : volumes) {
        Method getStateMethod = volume.getClass().getMethod("getState");
        String mState = (String) getStateMethod.invoke(volume);

        Method isPrimaryMethod = volume.getClass().getMethod("isPrimary");
        boolean mPrimary = (Boolean) isPrimaryMethod.invoke(volume);

        if (!mPrimary && mState.equals("mounted")) {
            Method getPathMethod = volume.getClass().getMethod("getPath");
            String mPath = (String) getPathMethod.invoke(volume);

            Method getUuidMethod = volume.getClass().getMethod("getUuid");
            String mUuid = (String) getUuidMethod.invoke(volume);

            if (mUuid != null && mPath != null)
                volumesMap.put(mUuid, mPath);
        }
    }
    return volumesMap;
}

このメソッドは、マウントされたすべての非プライマリストレージ(USB OTGを含む)を提供し、それらのUUIDをパスにマップします。これにより、DocumentUriを実際のパスに(またはその逆に)変換できます。

@TargetApi(Build.VERSION_CODES.Lollipop)
public static String convertToPath(Uri treeUri, Context context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    String documentId = DocumentsContract.getTreeDocumentId(treeUri);
    if (documentId != null){
        String[] split = documentId.split(":");
        String uuid = null;
        if (split.length > 0)
            uuid = split[0];
        String pathToVolume = null;
        Map<String, String> volumesMap = getSecondaryMountedVolumesMap(context);
        if (volumesMap != null && uuid != null)
            pathToVolume = volumesMap.get(uuid);
        if (pathToVolume != null) {
            String pathInsideOfVolume = split.length == 2 ? IFile.SEPARATOR + split[1] : "";
            return pathToVolume + pathInsideOfVolume;
        }
    }
    return null;
}

グーグルは彼らの醜いSAFを扱うより良い方法を私たちに与えるつもりはないようです。

EDIT:このアプローチはAndroid 6.0 .. ..

3
isabsent