I have a system application with all the necessary permissions. previously, i would download apks using the DownloadManager and then use reflection via PackageManager's installPackage() to install said apk.
unfortunately, following N's behaviour changes
http://developer.android.com/preview/behavior-changes.html :
The DownloadManager can no longer share privately stored files by filename. Legacy applications may end up with an unaccessible path when accessing COLUMN_LOCAL_FILENAME. Apps targeting Android N or higher trigger a SecurityException when attempting to access COLUMN_LOCAL_FILENAME. Legacy applications that set the download location to a public location by using setDestinationInExternalFilesDir(Context, String, String) or setDestinationInExternalPublicDir(String, String) can still access the path in COLUMN_LOCAL_FILENAME, however, this method is strongly discouraged. The preferred way of accessing a file exposed by the DownloadManager is using openFileDescriptor(Uri, String).
I am now getting a permission denied error when trying it the old way.
I need a Uri to pass to PM's installPackage(), and so i currently just copy the file to my application's private folder with a (depcrecated) WORLD_READABLE mode, so that the PM can later access it and install. This feels very hacky and i was wondering if anyone knows of a better way (The other option suggested by androiddev is to download the file to a public dir, but this would require me to add the WRITE_EXTERNAL_STORAGE permissions which is not an option.)
Related
I'm downloading a file through android DownloadManager with de function below.
private fun downloadFile() {
val downloadManager: DownloadManager =
getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request =
DownloadManager.Request(Uri.
parse("https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"))
downloadManager.enqueue(request)
}
}
The download occurs normally without any storage permissions.
Does the download manager work without storage permissions?
I can't find the answer in the official documentation.
https://developer.android.com/reference/android/app/DownloadManager
DownloadManager delegates the downloading work to a separate system-supplied app. That app has rights to write to a few locations, even if your app does not.
If u use setDestinationInExternalPublicDir
For applications targeting Build.VERSION_CODES.Q or above, WRITE_EXTERNAL_STORAGE permission is not needed and the dirType must be one of the known public directories like
Environment#DIRECTORY_DOWNLOADS, Environment#DIRECTORY_PICTURES, Environment#DIRECTORY_MOVIES, etc.
And for setDestinationUri
For applications targeting Build.VERSION_CODES.Q or above, WRITE EXTERNAL_STORAGE permission is not needed and the uri must refer to a path within the directories owned by the application (e.g. Context#getExternalFilesDir(String)) or a path within the top-level Downloads directory (as returned by Environment#getExternalStoragePublicDirectory(String) with Environment#DIRECTORY_DOWNLOADS).
Since Android Q came out there are some privacy changes in the permissions about read from external storage. I have a chat application that the user can choose a photo from Downloads etc... and send it. So i need to have access to that files. The way i did this is by using contentProvider.
context.contentResolver.openInputStream(uri)?.use { inputStream ->
while (true) {
numBytesRead = inputStream.read(buffer)
// .. do stuff
}
}
The uri that is available at that time is -> file:///storage/emulated/0/Download/myFile.pdf
However i get a FileNotFoundException but the file trully exists.
I have set all the permissions in the manifest and granted them on the launch of the app. From Android <= 9 it works properly.
So what do i have to do...?
I have a chat application that the user can choose a photo from Downloads etc... and send it
That will not be possible on Android 10 and higher. You need to switch to something else. For example, you could use ACTION_OPEN_DOCUMENT to allow the user to choose content from anywhere the user wants. Then, use the resulting Uri to upload it to your chat server, akin to how you are using the Uri in your code snippet. Or, better yet, don't read it all into memory — use something like this OkHttp InputStreamRequestBody implementation.
For Android 10, you can add android:requestLegacyExternalStorage="true" to your <application> element in the manifest. This opts you into the legacy storage model, and your existing external storage code will work. That will not work on Android R and higher, though, so this is only a short-term fix.
As Android is very inconsistent between different major Versions regarding File access, I feel a bit lost.
I try to describe the problem as easy as possible:
My Company uses a commercial native DRM to protect other native library's we provide. We have a Licensing App, which invoked some Voodoo to end up with Licensing files in say /sdcard/companyname/LicenseContainer/. Other protected Apps looked at this directory in native code, checking if the user has a valid License.
The Android 10 update however, invalidated this workflow completely as it only provides scoped storage access. We could do a workaround using Storage Manager to grant access, which is unfortunately also deprecated now.
So my Question is now:
How can one App save files to a location on /sdcard/FOLDER which are
not deleted on App deletion
Accessible in native code by other apps
I'm a bit overwhelmed with all the possible solutions (SAF, FileProvider, etc), which invoke often that one app grants permissions to the other. But the files should be accessible without an installed first app who put it there.
I know there must be a solution, as recent FileManagers (i.e. Files by Google) get access to the whole /sdcard/ directory.
Whats the easiest, future-proof route to go here without invoking "hacks" like android:requestLegacyExternalStorage="true"
You may ask the user to give you access to any file or directory, including the root of internal storage or external SD card. You can make this access permanent for your app, be able to read/write files anywhere with the Scoped Storage API afterwards, until the app is uninstalled or reset.
Then, if you need to read or write a file in native C/C++ code, you may get Linux file descriptor (int number) of the file and pass it to native code to use with fdopen() call for example.
Here is a Java code snippet to get a file descriptor form a single file Uri (which in string form is like content://...)
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(uri, "r"); // gets FileNotFoundException here, if file we used to have was deleted
int fd = parcelFileDescriptor.getFd(); // detachFd() if we want to close in native code
If you have source code for your native libraries, or can call them with C FILE* - it will work fine. The only problem is when you don't have the source code and they expect a file path/name. * UPDATE *: it is still possible to use the path/file name strings to pass to C/C++ functions that expect a file name. Simply instead of the "real path/file name", create a name to symbolic link like this:
// fd is file descriptor obtained in Java code above
char fileName[32];
sprintf(fileName, "/proc/self/fd/%d", fd);
// The above fileName can be passed to C/C++ functions that expect a file name.
// They can read or write to it, depending on permissions to fd given in Java,
// but I guess C/C++ code can not create a new file. Someone correct me,
// if I'm mistaken here.
However, at this time I'm not sure that when you create a file in a directory beyond the app "sandbox" in that way, if the system will delete this file too after uninstall... Would need to write a quick test on Android 10 to find out, and then we still won't know if Google won't change this behavior in future.
If you want to save files in shared storage (where it can be accessed by users & other apps) you need to use
for Media Files (Images, Videos, Audio, Downloads) use MediaStore
for Documents and Other Files use Storage Access Framework (this is simply a system file picker)
For instance you can use the following snippet to save a pdf file using Storage Access Framework
const val CREATE_FILE = 1
private fun createFile(pickerInitialUri: Uri) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
putExtra(Intent.EXTRA_TITLE, "invoice.pdf")
// Optionally, specify a URI for the directory that should be opened in
// the system file picker before your app creates the document.
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, CREATE_FILE)
}
After the user has picked a directory we still need to handle the result Uri in on onActivityResult method.
override fun onActivityResult(
requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == CREATE_FILE && resultCode == Activity.RESULT_OK) {
// The result data contains a URI for directory that
// the user selected.
resultData?.data?.also { uri ->
// save your data using the `uri` path
}
}
}
You can read about this in more detail on the following blogpost
https://androidexplained.github.io/android/android11/scoped-storage/2020/09/29/file-saving-android-11.html
Background
Up until Android Q, if we wanted to get information about an APK file, we could use WRITE_EXTERNAL_STORAGE and READ_EXTERNAL_STORAGE to get access to the storage, and then use PackageManager.getPackageArchiveInfo function on the file-path.
Similar cases exist, such as using ZipFile class on a compressed file, and probably countless framework APIs and third party libraries.
The problem
Google announced a huge amount of restrictions recently on Android Q.
One of them is called Scoped Storage, which ruins storage permission when it comes to accessing all files the device has. It lets you either handle media files, or use the very restricted Storage-Access-Framework (SAF) which can't allow apps to reach and use files using File API and file-paths.
When Android Q Beta 2 was published, it broke a lot of apps because of it, including of Google. The reason was that it was turned on by default, affecting all apps, whether they target Android Q or not.
The reason is that many apps, SDKs and Android framework itself - all use File API quite often. On many cases, they also don't support InputStream or SAF-related solutions. An example for this is exactly the APK parsing example I wrote about (PackageManager.getPackageArchiveInfo).
On Q beta 3, however, things changed a bit, so that app that target Q will have the scoped storage, and there is a flag to disable it and still use the normal storage permissions and File API as usual. Sadly the flag is only temporary (read here), so it's delaying the inevitable .
What I've tried
I've tried and found the next things:
Using the storage permission indeed didn't let me read any file that's not media file (I wanted to find APK files). It's as if the files don't exist.
Using SAF, I could find the APK file, and with some workaround to find its real path (link here), I've noticed that File API can tell me that indeed the file exist, but it couldn't get its size, and the framework failed to use its path using getPackageArchiveInfo . Wrote about this here
I tried to make a symlink to the file (link here), and then read from the symlink. It didn't help.
For the case of parsing APK files, I tried to search for alternative solutions. I've found 2 github repositories that handle the APK using a File class (here and here), and one that uses InputStream instead ( here). Sadly the one that uses InputStream is very old, missing various features (such as getting the app's name and icon) and isn't going to be updated anytime soon. Besides, having a library requires maintenance to keep up with future versions of Android, otherwise it might have issues in the future, or even crash.
The questions
Generally, is there a way to still use File API when using SAF ? I'm not talking about root solutions or just copying the file to somewhere else. I'm talking about a more solid solution.
For the case of APK parsing, is there a way to overcome this issue that the framework only provides file-path as a parameter? Any workaround or a way to use InputStream perhaps?
How to handle SAF when I can only handle File or file-path? It is possible, even if you can send only a Java File object, or path string to a library function which you cannot modify:
First, obtain a Uri to a file you need to handle (in String form it would be like "content://..."), then:
try {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(uri, "r"); // may get FileNotFoundException here
// Obtain file descriptor:
int fd = parcelFileDescriptor.getFd(); // or detachFd() if we want to close file in native code
String linkFileName = "/proc/self/fd/" + fd;
// Call library function with path/file string:
someFunc(/*file name*/ linkFileName);
// or with File parameter
otherFunc(new File(linkFileName));
// Finally, if you did not call detachFd() to obtain the file descriptor, call:
parcelFileDescriptor.close();
// Otherwise your library function should close file/stream...
} catch (FileNotFoundException fnf) {
fnf.printStackTrace(); // or whatever
}
Posting another answer just to have more room and let me insert code snipes. Given the file descriptor as explained in my previous answer, I tried using net.dongliu:apk-parser package mentioned by #androiddeveloper in the original question, as follows (Lt.d is my shorthand to using Log.d(SOME_TAG, string...)):
String linkFileName = "/proc/self/fd/" + fd;
try (ApkFile apkFile = new ApkFile(new File(linkFileName))) {
ApkMeta apkMeta = apkFile.getApkMeta();
Lt.d("ApkFile Label: ", apkMeta.getLabel());
Lt.d("ApkFile pkg name: ", apkMeta.getPackageName());
Lt.d("ApkFile version code: ", apkMeta.getVersionCode());
String iconStr = apkMeta.getIcon();
Lt.d("ApkFile icon str: ", iconStr);
for (UseFeature feature : apkMeta.getUsesFeatures()) {
Lt.d(feature.getName());
}
}
catch (Exception ex) {
Lt.e("Exception in ApkFile code: ", ex);
ex.printStackTrace();
}
}
It gives me the correct app label, for the icon it gives me only a string to the resource directory (like "res/drawable-mdpi-v4/fex.png"), so again raw ZIP reading functions would have to be applied to read the actual icon bits. Specifically I was testing ES File Explorer Pro APK (bought this product and saved APK for my own backup, got the following output:
I/StorageTest: ApkFile Label: ES File Explorer Pro
I/StorageTest: ApkFile pkg name: com.estrongs.android.pop.pro
I/StorageTest: ApkFile version code: 1010
I/StorageTest: ApkFile icon str: res/drawable-mdpi-v4/fex.png
I/StorageTest: android.hardware.bluetooth
I/StorageTest: android.hardware.touchscreen
I/StorageTest: android.hardware.wifi
I/StorageTest: android.software.leanback
I/StorageTest: android.hardware.screen.portrait
I'm on an Android 5.0 device where a usb mass storage is mounted at "/storage/usbotg" via USB OTG cable.
Unfortunately from my app I only have read access to that folder. Please note that I set the write external storage permission as I'm able to write to device storage.
I post the following code as reference:
string Root = "/storage/usbotg";
string[] Dirs = Directory.GetDirectories(Root);
string NewFolder = Path.Combine(Root, "NewFolder");
Directory.CreateDirectory(NewFolder);
This gives me an exception on the last line (but I'm able to list subdirectories in Dirs)
Exception:
System.UnauthorizedAccessException
Exception Message:
Access to the path "/storage/usbotg/NewFolder" is denied.
If I use:
string Root = "/storage/emulated/0";
everything is working fine and the "NewFolder" is created.
What I'm missing? How can I write to that folder?
I'm using Xamarin.Forms 2.5.0
Thanks for your help
Accessing data outside your application's private storage using the file system is more restricted with each Android version.
The recommended options are :
Use getExternalFilesDirs(), getExternalCacheDirs, ... : this gives you one or more directories specific to you application (usually directories named after the package name). This does not work for removable media, unless they're adopted.
Use the Storage Access Framework : ask the user (using ACTION_OPEN_DOCUMENT_TREE) to choose the storage root. You can then manage the content of the picked directory through the SAF API. You can persist the permission, so you only need to ask the user once. It seems that it is what ES File explore does to get write permission.
A (much) more detailed explanation by Mark Murphy.