Как открыть файл 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"/>
Вопросы
Почему это происходит?
Что-то не так с созданным пользователем провайдером? Нужен ли флаг? В порядке ли создание URI? Должен ли я добавить имя пакета текущего приложения к нему?
Стоит ли просто добавить проверку, если это Android API 24 и выше, и если да, то использовать провайдера, а если нет, использовать обычный вызов Uri.fromFile? Если я использую это, библиотека поддержки фактически теряет свое предназначение, потому что она будет использоваться для более новых версий Android...
Достаточно ли будет поддержки библиотеки FileProvider для всех случаев использования (если, конечно, у меня есть разрешение на внешнее хранилище)?
1 ответ
Я могу просто добавить проверку версии ОС Android и использовать любой из этих методов, но, как я прочитал, должен использоваться один метод: FileProvider.
Ну, как говорится, "для танго нужны двое".
Использовать какую-то конкретную схему (file
, content
, http
и т. д.), вы должны не только предоставить данные в этой схеме, но и получатель должен иметь возможность поддержать прием данных в этой схеме.
В случае установщика пакета, поддержка content
так как схема была добавлена только в Android 7.0 (и то, возможно, только потому, что я указал на проблему).
Почему это происходит?
Потому что Google (см. Это и это).
Что-то не так с созданным пользователем провайдером?
Возможно нет.
Стоит ли просто добавить проверку, если это Android API 24 и выше, и если да, то использовать провайдера, а если нет, использовать обычный вызов Uri.fromFile?
Да. Или, если вы предпочитаете, поймать ActivityNotFoundException
и реагировать на это, или использовать PackageManager
а также resolveActivity()
видеть заранее, если данное Intent
(например, один с content
Uri
) будет работать правильно.
Если я использую это, библиотека поддержки фактически теряет свое назначение, потому что она будет использоваться для более новых версий 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>