Как открыть файл APK для всех версий Android

Фон

До сих пор был простой способ установить файл APK, используя это намерение:

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

Но если ваше приложение предназначено для Android API 24 и выше (Nougat - 7.0), и вы запускаете этот код на нем или новее, вы получите исключение, как показано здесь, например:

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

Эта проблема

Поэтому я сделал то, что мне сказали: используйте класс FileProvider библиотеки поддержки как таковой:

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

манифест:

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

Но теперь он работает только на Android Nougat. На Android 5.0 выдает исключение: ActivityNotFoundException.

Что я пробовал

Я могу просто добавить проверку версии ОС Android и использовать любой из этих методов, но, как я прочитал, должен использоваться один метод: FileProvider.

Итак, я попытался использовать свой собственный ContentProvider, который действует как FileProvider, но я получил то же исключение, что и FileProvider библиотеки поддержки.

Вот мой код для этого:

    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
    }

}

манифест

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

Вопросы

  1. Почему это происходит?

  2. Что-то не так с созданным пользователем провайдером? Нужен ли флаг? В порядке ли создание URI? Должен ли я добавить имя пакета текущего приложения к нему?

  3. Стоит ли просто добавить проверку, если это Android API 24 и выше, и если да, то использовать провайдера, а если нет, использовать обычный вызов Uri.fromFile? Если я использую это, библиотека поддержки фактически теряет свое предназначение, потому что она будет использоваться для более новых версий Android...

  4. Достаточно ли будет поддержки библиотеки FileProvider для всех случаев использования (если, конечно, у меня есть разрешение на внешнее хранилище)?

1 ответ

Решение

Я могу просто добавить проверку версии ОС Android и использовать любой из этих методов, но, как я прочитал, должен использоваться один метод: FileProvider.

Ну, как говорится, "для танго нужны двое".

Использовать какую-то конкретную схему (file, content, httpи т. д.), вы должны не только предоставить данные в этой схеме, но и получатель должен иметь возможность поддержать прием данных в этой схеме.

В случае установщика пакета, поддержка content так как схема была добавлена ​​только в Android 7.0 (и то, возможно, только потому, что я указал на проблему).

Почему это происходит?

Потому что Google (см. Это и это).

Что-то не так с созданным пользователем провайдером?

Возможно нет.

Стоит ли просто добавить проверку, если это Android API 24 и выше, и если да, то использовать провайдера, а если нет, использовать обычный вызов Uri.fromFile?

Да. Или, если вы предпочитаете, поймать ActivityNotFoundException и реагировать на это, или использовать PackageManager а также resolveActivity() видеть заранее, если данное Intent (например, один с contentUri) будет работать правильно.

Если я использую это, библиотека поддержки фактически теряет свое назначение, потому что она будет использоваться для более новых версий Android

"Библиотека поддержки" не имеет ничего общего с более новыми версиями Android. Лишь небольшой процент классов в различных артефактах поддержки Android являются бэкпортами или совместимостью. Огромное количество этого - FileProvider, ViewPager, ConstraintLayoutи т. д. - это просто классы, которые Google хотел предоставлять и поддерживать, но хотел сделать их доступными вне прошивки.

Достаточно ли будет поддержки библиотеки FileProvider для всех случаев использования?

Только на Android 7.0+. Опять же, стандартный установщик пакета Android не поддерживает content схемы до Android 7.0.

Просто для тех, кто интересуется, как правильно установить APK, здесь:

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

манифест

<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>
Другие вопросы по тегам