Como abrir um arquivo APK para todas as versões do Android

fundo

Até o momento, havia uma maneira fácil de instalar um arquivo APK, usando esta intenção:

    final Intent intent=new Intent(Intent.ACTION_VIEW)
            .setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");

Porém, se seu aplicativo segmentar a API Android 24 e superior (Nougat - 7.0) e você executar esse código nele ou mais recente, receberá uma exceção, como mostradoaqui , por exemplo:

android.os.FileUriExposedException: file:///storage/emulated/0/sample.apk exposed beyond app through Intent.getData()
O problema

Então, fiz o que me disseram: use a classe FileProvider da biblioteca de suporte, como tal:

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

manifesto:

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

Mas agora funciona apenas no Android Nougat. No Android 5.0, ele lança uma exceção: ActivityNotFoundException.

O que eu tentei

Posso apenas adicionar uma verificação da versão do sistema operacional Android e usar os dois métodos, mas, como eu li, deve haver um único método para usar: FileProvider.

Então, o que tentei é usar meu próprio ContentProvider que atua como FileProvider, mas recebi a mesma exceção do FileProvider da biblioteca de suporte.

Aqui está o meu código:

    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
    }

}

manifesto

    <provider
        android:name=".utils.apps_utils.OpenFileProvider"
        android:authorities="open_file_provider"
        android:exported="true"
        android:grantUriPermissions="true"
        android:multiprocess="true"/>
As questões

Por que isso ocorre?

Há algo de errado com o provedor personalizado que eu criei? A bandeira é necessária? A criação do URI está correta? Devo adicionar o nome do pacote do aplicativo atual?

Devo apenas adicionar uma verificação se é a API Android 24 e superior e, em caso afirmativo, use o provedor e, caso contrário, use um método normalUri.fromFile ligar ? Se eu usar isso, a biblioteca de suporte realmente perde seu objetivo, porque será usada para versões mais recentes do Android ...

A biblioteca de suporte FileProvider será suficiente para todos os casos de uso (desde que eu tenha permissão de armazenamento externo, é claro)?

questionAnswers(1)

yourAnswerToTheQuestion