Fondo

Hasta ahora, había una manera fácil de instalar un archivo APK, con esta intención:

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

Pero, si su aplicación apunta a la API de Android 24 y superior (Nougat - 7.0), y ejecuta este código en ella o una versión más reciente, obtendrá una excepción, como se muestra aquí , por ejemplo:

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

El problema

Así que hice lo que me dijeron: use la clase FileProvider de la biblioteca de soporte, 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);

manifiesto :

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

Pero ahora solo funciona en Android Nougat. En Android 5.0, lanza una excepción: ActivityNotFoundException.

Lo que he intentado

Puedo simplemente agregar una verificación para la versión del sistema operativo Android y usar cualquiera de los métodos, pero como he leído, debería haber un solo método para usar: FileProvider.

Entonces, lo que intenté es usar mi propio ContentProvider que actúa como FileProvider, pero obtuve la misma excepción que el FileProvider de la biblioteca de soporte.

Aquí está mi código para ello:

    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
    }

}

manifiesto

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

Las preguntas

  1. ¿Por qué ocurre?

  2. ¿Hay algún problema con el proveedor personalizado que he creado? ¿Se necesita la bandera? ¿Está bien la creación de URI? ¿Debo agregarle el nombre del paquete de la aplicación actual?

  3. ¿Debo agregar una marca de verificación si es la API de Android 24 y superior y, de ser así, usar el proveedor y, si no es así, usar una llamada Uri.fromFile normal? Si uso esto, la biblioteca de soporte realmente pierde su propósito, porque se usará para versiones más nuevas de Android ...

  4. ¿Será suficiente la biblioteca de soporte FileProvider para todos los casos de uso (dado que tengo permiso de almacenamiento externo, por supuesto)?

6
android developer 15 dic. 2016 a las 12:15

2 respuestas

La mejor respuesta

Puedo simplemente agregar una verificación para la versión del sistema operativo Android y usar cualquiera de los métodos, pero como he leído, debería haber un solo método para usar: FileProvider.

Bueno, como dice el refrán, "se necesitan dos para bailar el tango".

Para utilizar cualquier esquema en particular (file, content, http, etc.), no solo debe proporcionar los datos en ese esquema, sino que el destinatario debe poder admitir aceptando los datos en ese esquema.

En el caso del instalador del paquete, la compatibilidad con content como esquema solo se agregó en Android 7.0 (y luego, quizás solo porque señalé el problema).

¿Por qué ocurre?

Porque Google (consulte esto y esto).

¿Hay algún problema con el proveedor personalizado que he creado?

Probablemente no.

¿Debo agregar un cheque si es Android API 24 y superior, y si es así, usar el proveedor y, si no es así, usar una llamada Uri.fromFile normal?

Si. O, si lo prefiere, capture el ActivityNotFoundException y reaccione a eso, o use PackageManager y resolveActivity() para ver con anticipación si un Intent dado (por ejemplo, uno con un content Uri) funcionará correctamente.

Si uso esto, la biblioteca de soporte realmente pierde su propósito, porque se usará para versiones más nuevas de Android

La "biblioteca de soporte" tiene poco que ver con las versiones de Android más nuevas y las más antiguas. Solo un pequeño porcentaje de las clases en los diversos artefactos de soporte de Android son backports o shims de compatibilidad. Grandes cantidades de él (FileProvider, ViewPager, ConstraintLayout, etc.) son simplemente clases que Google quería proporcionar y apoyar, pero quería que estuvieran disponibles fuera del firmware.

¿Será suficiente la biblioteca de soporte FileProvider para todos los casos de uso?

Solo en Android 7.0+. Nuevamente, el instalador de paquetes de Android estándar no es compatible con los esquemas content anteriores a Android 7.0.

5
CommonsWare 15 dic. 2016 a las 13:02

Solo para aquellos que se preguntan cómo finalmente instalar un APK correctamente, aquí:

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

manifiesto

<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>
0
android developer 13 abr. 2019 a las 18:59