Android 4.4 (KitKat)では、GoogleはSDカードへのアクセスをかなり制限しました。
Android Lollipop(5.0)以降、開発者は に記載されているように、特定のフォルダーへのアクセスを許可するかどうかをユーザーに確認する新しいAPIを使用できますこのGoogleグループの投稿 .
この投稿は、次の2つのWebサイトにアクセスするように指示します。
これは内部の例のように見えますが(おそらく、APIデモで後で示されるでしょう)、何が起こっているのかを理解するのは非常に困難です。
これは新しいAPIの公式ドキュメントですが、使用方法についての十分な詳細は説明していません。
以下がその内容です:
ドキュメントのサブツリー全体への完全なアクセスが本当に必要な場合は、ACTION_OPEN_DOCUMENT_TREEを起動して、ユーザーがディレクトリを選択できるようにします。次に、結果のgetData()をfromTreeUri(Context、Uri)に渡し、ユーザーが選択したツリーでの作業を開始します。
DocumentFileインスタンスのツリーをナビゲートするとき、常にgetUri()を使用して、そのオブジェクトの基になるドキュメントを表すUriを取得し、openInputStream(Uri)などで使用できます。
KitKat以前を実行しているデバイスでコードを簡素化するために、DocumentsProviderの動作をエミュレートするfromFile(File)を使用できます。
新しいAPIについていくつか質問があります。
たくさんの良い質問、掘り下げましょう:)
KitKatのストレージアクセスフレームワークと対話するための優れたチュートリアルを次に示します。
https://developer.Android.com/guide/topics/providers/document-provider.html#client
Lollipopの新しいAPIとのやり取りは非常に似ています。ユーザーにディレクトリツリーの選択を促すには、次のようなインテントを起動できます。
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, 42);
次に、onActivityResult()で、ユーザーが選択したUriを新しいDocumentFileヘルパークラスに渡すことができます。選択したディレクトリ内のファイルをリストし、新しいファイルを作成する簡単な例を次に示します。
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (resultCode == RESULT_OK) {
Uri treeUri = resultData.getData();
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
// List all existing files inside picked directory
for (DocumentFile file : pickedDir.listFiles()) {
Log.d(TAG, "Found file " + file.getName() + " with size " + file.length());
}
// Create a new file and write into it
DocumentFile newFile = pickedDir.createFile("text/plain", "My Novel");
OutputStream out = getContentResolver().openOutputStream(newFile.getUri());
out.write("A long time ago...".getBytes());
out.close();
}
}
DocumentFile.getUri()
によって返されるUriは、さまざまなプラットフォームAPIで使用できる柔軟性を備えています。たとえば、Intent.setData()
とIntent.FLAG_GRANT_READ_URI_PERMISSION
を使用して共有できます。
ネイティブコードからそのUriにアクセスする場合は、ContentResolver.openFileDescriptor()
を呼び出してからParcelFileDescriptor.getFd()
またはdetachFd()
を使用して、従来のPOSIXファイル記述子整数を取得できます。
デフォルトでは、Storage Access Frameworksのインテントを介して返されるUrisは、not再起動後も保持されます。プラットフォームは、許可を永続化する機能を「提供」しますが、必要な場合は許可を「取得」する必要があります。上記の例では、次のように呼び出します。
getContentResolver().takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
ContentResolver.getPersistedUriPermissions()
APIを使用して、アプリがアクセス権を保持しているものをいつでも把握できます。永続的なUriにアクセスする必要がなくなった場合は、ContentResolver.releasePersistableUriPermission()
で解放できます。
いいえ、プラットフォームの古いバージョンに新しい機能をさかのぼって追加することはできません。
現在、これを示すUIはありませんが、詳細はadb Shell dumpsys activity providers
出力の「Granted Uri Permissions」セクションにあります。
Uriの許可付与は、他のすべてのマルチユーザープラットフォーム機能と同様に、ユーザーごとに分離されます。つまり、2人の異なるユーザーの下で実行されている同じアプリには、重複または共有されたUriアクセス許可が付与されません。
バッキングDocumentProviderは、クラウドベースのドキュメントが削除されたときなど、いつでも許可を取り消すことができます。これらの取り消された許可を発見する最も一般的な方法は、上記のContentResolver.getPersistedUriPermissions()
からそれらが消えるときです。
許可に関係するいずれかのアプリのアプリデータが消去されるたびに、権限も取り消されます。
はい、ACTION_OPEN_DOCUMENT_TREE
インテントは、既存および新規に作成されたファイルとディレクトリの両方に再帰的にアクセスできます。
はい、複数選択はKitKatからサポートされており、EXTRA_ALLOW_MULTIPLE
インテントを開始するときにACTION_OPEN_DOCUMENT
を設定することで許可できます。 Intent.setType()
またはEXTRA_MIME_TYPES
を使用して、選択できるファイルの種類を絞り込むことができます。
http://developer.Android.com/reference/Android/content/Intent.html#ACTION_OPEN_DOCUMENT
はい、エミュレータでも、プライマリ共有ストレージデバイスがピッカーに表示されます。アプリが共有ストレージへのアクセスにStorage Access Frameworkのみを使用する場合、READ/WRITE_EXTERNAL_STORAGE
権限はまったく必要ありません。それらを削除するか、Android:maxSdkVersion
機能を使用できます。古いプラットフォームバージョンでのみリクエストする。
物理メディアが関係する場合、基礎となるメディアのUUID(FATシリアル番号など)は常に返されたUriに焼き付けられます。ユーザーが複数のスロット間でメディアを交換した場合でも、システムはこれを使用してユーザーが最初に選択したメディアに接続します。
ユーザーが2枚目のカードを交換した場合、新しいカードへのアクセスを求めるプロンプトを表示する必要があります。システムはUUID単位で許可を記憶しているため、ユーザーが後で再挿入した場合、元のカードへの以前に許可されたアクセス権を引き続き保持します。
以下にリンクされているGithubのAndroidプロジェクトでは、Android 5.のextSdCardに書き込むことができる作業コードを見つけることができます。これは、ユーザーがSDカード全体とこのカードのどこにでも書くことができます。 (単一のファイルのみにアクセスしたい場合は、物事が簡単になります。)
ストレージアクセスフレームワークのトリガー:
@TargetApi(Build.VERSION_CODES.Lollipop)
private void triggerStorageAccessFramework() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}
Storage Access Frameworkからの応答の処理:
@TargetApi(Build.VERSION_CODES.Lollipop)
@Override
public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS) {
Uri treeUri = null;
if (resultCode == Activity.RESULT_OK) {
// Get Uri from Storage Access Framework.
treeUri = resultData.getData();
// Persist URI in shared preference so that you can use it later.
// Use your own framework here instead of PreferenceUtil.
PreferenceUtil.setSharedPreferenceUri(R.string.key_internal_uri_extsdcard, treeUri);
// Persist access permissions.
final int takeFlags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getActivity().getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
}
}
}
Storage Access Frameworkを介してファイルのoutputStreamを取得する(保存されたURLを使用します。これが外部SDカードのルートフォルダーのURLであると仮定します)
DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
getContentResolver().openOutputStream(targetDocument.getUri());
これは、次のヘルパーメソッドを使用します。
public static DocumentFile getDocumentFile(final File file, final boolean isDirectory) {
String baseFolder = getExtSdCardFolder(file);
if (baseFolder == null) {
return null;
}
String relativePath = null;
try {
String fullPath = file.getCanonicalPath();
relativePath = fullPath.substring(baseFolder.length() + 1);
}
catch (IOException e) {
return null;
}
Uri treeUri = PreferenceUtil.getSharedPreferenceUri(R.string.key_internal_uri_extsdcard);
if (treeUri == null) {
return null;
}
// start with root of SD card and then parse through document tree.
DocumentFile document = DocumentFile.fromTreeUri(Application.getAppContext(), treeUri);
String[] parts = relativePath.split("\\/");
for (int i = 0; i < parts.length; i++) {
DocumentFile nextDocument = document.findFile(parts[i]);
if (nextDocument == null) {
if ((i < parts.length - 1) || isDirectory) {
nextDocument = document.createDirectory(parts[i]);
}
else {
nextDocument = document.createFile("image", parts[i]);
}
}
document = nextDocument;
}
return document;
}
public static String getExtSdCardFolder(final File file) {
String[] extSdPaths = getExtSdCardPaths();
try {
for (int i = 0; i < extSdPaths.length; i++) {
if (file.getCanonicalPath().startsWith(extSdPaths[i])) {
return extSdPaths[i];
}
}
}
catch (IOException e) {
return null;
}
return null;
}
/**
* Get a list of external SD card paths. (KitKat or higher.)
*
* @return A list of external SD card paths.
*/
@TargetApi(Build.VERSION_CODES.KitKat)
private static String[] getExtSdCardPaths() {
List<String> paths = new ArrayList<>();
for (File file : Application.getAppContext().getExternalFilesDirs("external")) {
if (file != null && !file.equals(Application.getAppContext().getExternalFilesDir("external"))) {
int index = file.getAbsolutePath().lastIndexOf("/Android/data");
if (index < 0) {
Log.w(Application.TAG, "Unexpected external file dir: " + file.getAbsolutePath());
}
else {
String path = file.getAbsolutePath().substring(0, index);
try {
path = new File(path).getCanonicalPath();
}
catch (IOException e) {
// Keep non-canonical path.
}
paths.add(path);
}
}
}
return paths.toArray(new String[paths.size()]);
}
/**
* Retrieve the application context.
*
* @return The (statically stored) application context
*/
public static Context getAppContext() {
return Application.mApplication.getApplicationContext();
}
そして
それは単なる補完的な答えです。
新しいファイルを作成した後、その場所をデータベースに保存して明日読む必要があるかもしれません。次のメソッドを使用して、それを再度読み取ることができます。
/**
* Get {@link DocumentFile} object from SD card.
* @param directory SD card ID followed by directory name, for example {@code 6881-2249:Download/Archive},
* where ID for SD card is {@code 6881-2249}
* @param fileName for example {@code intel_haxm.Zip}
* @return <code>null</code> if does not exist
*/
public static DocumentFile getExternalFile(Context context, String directory, String fileName){
Uri uri = Uri.parse("content://com.Android.externalstorage.documents/tree/" + directory);
DocumentFile parent = DocumentFile.fromTreeUri(context, uri);
return parent != null ? parent.findFile(fileName) : null;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS && resultCode == RESULT_OK) {
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(data.getData(), takeFlags);
String sdcard = data.getDataString().replace("content://com.Android.externalstorage.documents/tree/", "");
try {
sdcard = URLDecoder.decode(sdcard, "ISO-8859-1");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// for example, sdcardId results "6312-2234"
String sdcardId = sdcard.substring(0, sdcard.indexOf(':'));
// save to preferences if you want to use it later
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
preferences.edit().putString("sdcard", sdcardId).apply();
}
}