やりたいこと
Androidアプリが作成した固有フォルダ(\Android\data¥パッケージ名下)に作成したファイルをユーザがアクセスできるダウンロードフォルダにコピーします。
現代のAndroid開発では、従来の java.io.File
を使った移動は難しくなっています。特に Android 10 (API 29) 以降で導入されたScoped Storage (スコープ付きストレージ) の影響で、外部ストレージへのアクセス権限が厳しくなりました。
この記事では、Androidアプリのプライベート領域(/data/data/...
や /Android/data/.../files
)から、WRITE_EXTERNAL_STORAGE
権限をほとんど必要とせず、安全にファイルを共有領域(Download
)へ移動させる方法を解説します。
背景
従来のAndroidでは、アプリは一度 WRITE_EXTERNAL_STORAGE
権限を取得すれば、外部ストレージのほぼどこにでもファイルを書き込むことができました。
しかし、Scoped Storageの導入により、各アプリは自身のプライベート領域か、MediaStore
が管理する特定の共有コレクション(Pictures
, Download
など)へのアクセスのみに制限されました。
つまり、アプリが他アプリのフォルダや、共有ストレージの任意の場所に勝手にファイルを置くことはできなくなったのです。
これが、プライベート領域からユーザーの共有領域へファイルを移動させる際に、単純な File.renameTo()
や File.copy()
が使えない主な理由です。
何に使う?
処理中はPrivateフォルダで処理を行い完了した後でユーザが操作できるフォルダに移動(ダウンロードなど)
バックアップなどのためにPrivateフォルダのファイルを(アーカイブして)ユーザが操作できるフォルダに移動
実装
以下のように実装します。
(Activityのメソッドとして実装しています。)
import android.provider.MediaStore;
import android.content.ContentValues;
import android.os.Environment;
public void moveFileToDownloads( Path privatePath) {
File privateFile = privatePath.toFile();
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, privateFile.getName());
values.put(MediaStore.MediaColumns.MIME_TYPE, "application/zip");
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + File.separator + APPNAME);
}
ContentResolver resolver = this.getContentResolver();
Uri uri = null;
try {
uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
if (uri == null) {
throw new RuntimeException("MediaStore insert failed.");
}
try (OutputStream os = resolver.openOutputStream(uri);
FileInputStream is = new FileInputStream(privateFile)) {
if (os == null) {
throw new RuntimeException("Failed to open output stream.");
}
byte[] buffer = new byte[4096];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
privateFile.delete();
}
} catch (Exception e) {
e.printStackTrace();
if (uri != null) {
resolver.delete(uri, null, null);
}
}
}
引数のprivatePathはダウンロードフォルダに移動するファイルのパスです。
ファイルパスをStringで保持している場合は Path path = Paths.get(pathString);で変換できます。
コードの説明
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, privateFile.getName());
values.put(MediaStore.MediaColumns.MIME_TYPE, "application/zip");
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + File.separator + APPNAME);
}
ContentValues (保存ファイルの情報)の作成を行っています。MIME_TYPEは必要に応じて変更してください。
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + File.separator + APPNAME);のAPPNAME部分はDownloadフォルダに作成するフォルダ名です。Downloadフォルダ直下に保存する場合はEnvironment.DIRECTORY_DOWNLOADS で問題ありません。
ContentResolver resolver = this.getContentResolver();
Uri uri = null;
try {
uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
if (uri == null) {
throw new RuntimeException("MediaStore insert failed.");
}
resolverを作成してuri(保存用のパス)を取得します。
byte[] buffer = new byte[4096];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
privateFile.delete();
ファイルの中身をコピーして元ファイルを削除しています。移動ではなくコピーの場合はprivateFile.delete();を呼ばないでください。
} catch (Exception e) {
e.printStackTrace();
if (uri != null) {
resolver.delete(uri, null, null);
}
}
エラー処理です。resolver.delete(uri, null, null);で失敗したDownloadフォルダ下ファイルを削除しています。
コメント