So far, there was an easy way to install an APK file, using this intent:
final Intent intent=new Intent(Intent.ACTION_VIEW)
.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
But, if your app targets Android API 24 and above (Nougat - 7.0) , and you run this code on it or newer, you will get an exception, as shown here , for example:
android.os.FileUriExposedException: file:///storage/emulated/0/sample.apk exposed beyond app through Intent.getData()
So I did what I was told: use the support library's FileProvider class, as such:
final Intent intent = new Intent(Intent.ACTION_VIEW)//
.setDataAndType(android.support.v4.content.FileProvider.getUriForFile(context,
context.getPackageName() + ".provider", apkFile),
"application/vnd.android.package-archive").addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
manifest:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
res/xml/provider_paths.xml :
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!--<external-path name="external_files" path="."/>-->
<external-path
name="files_root"
path="Android/data/${applicationId}"/>
<external-path
name="external_storage_root"
path="."/>
</paths>
But, now it works only on Android Nougat. On Android 5.0, it throws an exception: ActivityNotFoundException.
I can just add a check for the version of Android OS, and use either methods, but as I've read, there should be a single method to use: FileProvider.
So, what I tried is to use my own ContentProvider that acts as FileProvider, but I got the same exception as of the support library's FileProvider.
Here's my code for it:
final Intent intent = new Intent(Intent.ACTION_VIEW)
.setDataAndType(OpenFileProvider.prepareSingleFileProviderFile(apkFilePath),
"application/vnd.android.package-archive")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
OpenFileProvider.java
public class OpenFileProvider extends ContentProvider {
private static final String FILE_PROVIDER_AUTHORITY = "open_file_provider";
private static final String[] DEFAULT_PROJECTION = new String[]{MediaColumns.DATA, MediaColumns.DISPLAY_NAME, MediaColumns.SIZE};
public static Uri prepareSingleFileProviderFile(String filePath) {
final String encodedFilePath = new String(Base64.encode(filePath.getBytes(), Base64.URL_SAFE));
final Uri uri = Uri.parse("content://" + FILE_PROVIDER_AUTHORITY + "/" + encodedFilePath);
return uri;
}
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(@NonNull Uri uri) {
String fileName = getFileName(uri);
if (fileName == null)
return null;
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileName);
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
final String fileName = getFileName(uri);
if (fileName == null)
return null;
final File file = new File(fileName);
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final String filePath = getFileName(uri);
if (filePath == null)
return null;
final String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection;
final MatrixCursor ret = new MatrixCursor(columnNames);
final Object[] values = new Object[columnNames.length];
for (int i = 0, count = columnNames.length; i < count; ++i) {
String column = columnNames[i];
switch (column) {
case MediaColumns.DATA:
values[i] = uri.toString();
break;
case MediaColumns.DISPLAY_NAME:
values[i] = extractFileName(uri);
break;
case MediaColumns.SIZE:
File file = new File(filePath);
values[i] = file.length();
break;
}
}
ret.addRow(values);
return ret;
}
private static String getFileName(Uri uri) {
String path = uri.getLastPathSegment();
return path != null ? new String(Base64.decode(path, Base64.URL_SAFE)) : null;
}
private static String extractFileName(Uri uri) {
String path = getFileName(uri);
return path;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0; // not supported
}
@Override
public int delete(@NonNull Uri uri, String arg1, String[] arg2) {
return 0; // not supported
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
return null; // not supported
}
}
manifest
<provider
android:name=".utils.apps_utils.OpenFileProvider"
android:authorities="open_file_provider"
android:exported="true"
android:grantUriPermissions="true"
android:multiprocess="true"/>
Why does it occur?
Is there anything wrong with the custom provider I've created? Is the flag needed? Is the URI creation ok ? Should I add the current app's package name to it?
Should I just add a check if it's Android API 24 and above, and if so, use the provider, and if not, use a normal Uri.fromFile call ? If I use this, the support library actually loses its purpose, because it will be used for newer Android versions...
Will the support library FileProvider be enough for all use cases (given that I do have external storage permission, of course) ?
I can just add a check for the version of Android OS, and use either methods, but as I've read, there should be a single method to use: FileProvider.
Well, as the saying goes, "it takes two to tango".
To use any particular scheme (file
, content
, http
, etc.), not only do you have to provide the data in that scheme, but the recipient needs to be able to support accepting the data in that scheme.
In the case of the package installer, support for content
as a scheme was only added in Android 7.0 (and then, perhaps only because I pointed out the problem).
Why does it occur?
Because Google (see this and this).
Is there anything wrong with the custom provider I've created?
Probably not.
Should I just add a check if it's Android API 24 and above, and if so, use the provider, and if not, use a normal Uri.fromFile call ?
Yes. Or, if you prefer, catch the ActivityNotFoundException
and react to that, or use PackageManager
and resolveActivity()
to see ahead of time if a given Intent
(e.g., one with a content
Uri
) will work properly.
If I use this, the support library actually loses its purpose, because it will be used for newer Android versions
The "support library" has little to do with newer-vs.-older Android versions. Only a small percentage of the classes across the various Android Support artifacts are backports or compatibility shims. Vast quantities of it — FileProvider
, ViewPager
, ConstraintLayout
, etc. — are simply classes that Google wanted to provide and support but wanted to make them available outside of the firmware.
Will the support library FileProvider be enough for all use cases
Only on Android 7.0+. Again, the stock Android package installer does not support content
schemes prior to Android 7.0.
just for those who wonder how to finally install an APK properly, here:
@JvmStatic
fun prepareAppInstallationIntent(context: Context, file: File, requestResult: Boolean): Intent? {
var intent: Intent? = null
try {
intent = Intent(Intent.ACTION_INSTALL_PACKAGE)//
.setDataAndType(
if (VERSION.SDK_INT >= VERSION_CODES.N)
androidx.core.content.FileProvider.getUriForFile(context, context.packageName + ".provider", file)
else
Uri.fromFile(file),
"application/vnd.android.package-archive")
.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
.putExtra(Intent.EXTRA_RETURN_RESULT, requestResult)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (VERSION.SDK_INT < VERSION_CODES.JELLY_BEAN)
intent!!.putExtra(Intent.EXTRA_ALLOW_REPLACE, true)
} catch (e: Throwable) {
}
return intent
}
manifest
<provider
android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/>
</provider>
/res/xml/provider_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!--<external-path name="external_files" path="."/>-->
<external-path
name="files_root" path="Android/data/${applicationId}"/>
<external-path
name="external_storage_root" path="."/>
</paths>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With