6

Background

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()

The problem

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.

What I've tried

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"/>

The questions

  1. Why does it occur?

  2. 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?

  3. 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...

  4. Will the support library FileProvider be enough for all use cases (given that I do have external storage permission, of course) ?

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • You sure you shouldn't be using `ACTION_INSTALL_PACKAGE` instead of `ACTION_VIEW`? It seems like that would explain the `ActivityNotFoundException`. – Mike M. Dec 15 '16 at 09:21
  • @MikeM. The same exception is thrown for this action too (even after adding the permission REQUEST_INSTALL_PACKAGES) , and ACTION_VIEW is ok to be used, as it was used on old Android versions too, and can work here too. – android developer Dec 15 '16 at 09:27
  • Yep, seems they're both valid. Guess I've never tried to programmatically install an app before. – Mike M. Dec 15 '16 at 09:32
  • The easiest solution would be point 3) – Murat Karagöz Dec 15 '16 at 09:43
  • @MuratK. I know it's possible, but shouldn't be an official, standard way for this? What's the point of having a support library, if it will work here only for API 24 and above... – android developer Dec 15 '16 at 09:59
  • Can corfirm the issue. Does not work for Android 6 too. If you install the app 'Intent intercept' this app comes up showing content scheme, mimetype and flags. I do see nothing uncommon. Please try. I had choosen for point 3 already. – greenapps Dec 15 '16 at 10:37
  • `Will the support library FileProvider be enough`. No never. Not for Nougat even as FileProvider can only serve files from certain paths. You better use your OpenFileContentProvider as with it you can serve files from all paths. – greenapps Dec 15 '16 at 10:45

2 Answers2

5

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.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • So, I do have to just check the version, and use my own class (or of support library) if it's from API 24 and above, and use File Uri if not? This support library class seem more useless the more I try to use it ... – android developer Dec 15 '16 at 14:58
  • @androiddeveloper: "I do have to just check the version..." -- yes, due to limitations in the package installer. "This support library class seem more useless the more I try to use it" -- this issue has nothing to do with the support library. You would have the same problem with your own `ContentProvider`, or with my `StreamProvider`, or with any other third-party `ContentProvider`. Let me try boldface: **The stock Android package installer does not support `content` schemes prior to Android 7.0**. – CommonsWare Dec 15 '16 at 15:03
  • What about sharing of files (as I've asked before, here: http://stackoverflow.com/q/40941709/878126) ? It seems like ContentProvider was supported forever for this matter, no? It's just weird for me that sharing of a file has different support than opening a file... – android developer Dec 15 '16 at 16:35
  • @androiddeveloper: "It's just weird for me that sharing of a file has different support than opening a file" -- the "different support" is up to the developers of the recipient apps. Some apps support `content`, some do not. More apps *should* support `content`. But, app developers can do what they want. – CommonsWare Dec 15 '16 at 16:54
  • Wait, so it's possible that my code of using FileProvider won't work for some apps, while the old API of using it (meaning "Uri.fromFile") will? – android developer Dec 15 '16 at 17:05
  • @androiddeveloper: Sure. That's no different than some Web sites supporting `https` and others not. When the `Uri` in in the "data" facet of the `Intent`, you can at least determine what activities support `content` versus `file`. `ACTION_SEND`, `ACTION_IMAGE_CAPTURE`, and related `Intent` actions, where the `Uri` goes in an extra, are much more problematic. Likewise for places where a `Uri` winds up in a `Bundle`, such as with `setSound()` on a `Notification.Builder`. For those, `StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().build());` removes the `FileUriExposedException`. – CommonsWare Dec 15 '16 at 17:10
  • So how could I know which to use to share the file on pre-Nougat? And, doesn't it mean that all apps that only support the "Uri.fromFile" call, won't work in any way on Nougat? – android developer Dec 15 '16 at 18:48
  • @androiddeveloper: "So how could I know which to use to share the file on pre-Nougat?" -- I do not understand the question. "And, doesn't it mean that all apps that only support the "Uri.fromFile" call, won't work in any way on Nougat?" -- no. You decided to set your `targetSdkVersion` to 24 or higher. Hence, `FileUriExposedException` is something that you have to deal with. Legacy apps that have their `targetSdkVersion` set to 23 or lower will not raise a `FileUriExposedException`, as they (presumably) were written before that became a hard requirement. – CommonsWare Dec 15 '16 at 18:52
  • I mean: some apps that I can share a file with - have probably supported sharing using "Uri.fromFile" and some with the newer API. How can I know which support which, so that I could know which to use? Also, Starting from API 24, "Uri.fromFile" will probably not work at all for apps that support sharing to them via "Uri.fromFile" . – android developer Dec 15 '16 at 18:54
  • @androiddeveloper: "How can I know which support which, so that I could know which to use?" -- I still do not understand your question. I suggest that you ask a separate Stack Overflow question, rather than continuing this comment chain. – CommonsWare Dec 15 '16 at 18:59
  • OK, here: http://stackoverflow.com/questions/40941709/how-to-share-current-apps-apk-or-of-other-installed-apps-using-the-new-filepr#comment69564037_40941854 – android developer Dec 16 '16 at 10:21
1

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>
android developer
  • 114,585
  • 152
  • 739
  • 1,270