The purpose
Copy a file created by an Android app in its dedicated folder (under \Android\data\package_name) to the user-accessible Downloads folder.
Moving files with the traditional java.io.File class is challenging in modern Android development. Access permissions for external storage have become stricter, particularly as a result of Scoped Storage, which was introduced in Android 10 (API 29) and later.
This article explains how to safely move files from an Android app’s private storage (e.g., /data/data/... or /Android/data/.../files) to the shared storage (Download directory) with minimal need for the WRITE_EXTERNAL_STORAGE permission.
Background
In traditional Android, once an app obtained the WRITE_EXTERNAL_STORAGE permission, it could write files almost anywhere on the external storage.
However, with the introduction of Scoped Storage, each app is restricted to accessing only its own private area or specific shared collections managed by MediaStore (such as Pictures, Download, etc.).
In other words, apps can no longer freely place files in other apps’ folders or in arbitrary locations within shared storage.
This is the main reason why simple File.renameTo() or File.copy() methods cannot be used when moving files from an app’s private storage to the user’s shared storage.
What is this for?
Processing occurs in a Private folder and, upon completion, the files are moved (e.g., downloads) to a folder accessible to the user.
For purposes such as backup, files from the Private folder are archived and then moved to a folder accessible to the user.
Implementation
Implement it as follows.
(Implemented within the Activity class.)
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);
}
}
}
The argument privatePath is the path to the file that will be moved to the download folder.
If you have the file path stored as a String, you can convert it using Path path = Paths.get(pathString);.
Code explanation
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 (which holds the information for the file to be saved) is being created. Please change the MIME_TYPE as necessary.
The APPNAME part in values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + File.separator + APPNAME); is the name of the folder to be created within the Download folder. If you wish to save directly under the Download folder, using Environment.DIRECTORY_DOWNLOADS is sufficient.
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.");
}Create a resolver and get the URI (path for saving).
byte[] buffer = new byte[4096];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
privateFile.delete();It is copying the contents of a file and then deleting the original file. If it is a copy operation and not a move operation, please do not call privateFile.delete();
} catch (Exception e) {
e.printStackTrace();
if (uri != null) {
resolver.delete(uri, null, null);
}
}Error handling. Deleting the file under the Download folder that failed with resolver.delete(uri, null, null);


comment