如何打开所有 Android 版本的 APK 文件
How to open an APK file for all Android versions
背景
到目前为止,有一种安装 APK 文件的简单方法,使用此 intent:
final Intent intent=new Intent(Intent.ACTION_VIEW)
.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
但是,如果您的应用程序针对 Android API 24 及更高版本 (Nougat - 7.0),并且您 运行 此代码或更高版本,您将得到一个例外,如图所示 here ,例如:
android.os.FileUriExposedException: file:///storage/emulated/0/sample.apk exposed beyond app through Intent.getData()
问题
所以我照我说的做了:使用支持库的 FileProvider class,例如:
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 牛轧糖。在 Android 5.0 上,它抛出异常:ActivityNotFoundException。
我试过的
我可以只检查 Android OS 的版本,并使用任何一种方法,但正如我所读,应该只有一个方法可以使用: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 创建可以吗?我应该在其中添加当前应用程序的包名称吗?
如果是AndroidAPI24以上是不是就加个check,是的话就用provider,不是就用普通的Uri.fromFile 打电话?如果我使用它,支持库实际上失去了它的用途,因为它将用于更新的 Android 版本...
支持库 FileProvider 是否足以满足所有用例(当然,假设我有外部存储权限)?
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.
嗯,俗话说,"it takes two to tango"。
要使用任何特定方案(file
、content
、http
等),不仅您必须提供该方案中的数据,而且接收方还需要能够支持接受该方案中的数据。
对于软件包安装程序,仅在 Android 7.0 中添加了对 content
作为方案的支持(然后,可能只是因为 I pointed out the problem)。
Why does it occur?
Is there anything wrong with the custom provider I've created?
可能不会。
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 ?
是的。或者,如果您愿意,可以捕捉 ActivityNotFoundException
并对此做出反应,或者使用 PackageManager
和 resolveActivity()
提前查看给定的 Intent
(例如,带有content
Uri
) 将正常工作。
If I use this, the support library actually loses its purpose, because it will be used for newer Android versions
"support library" 与较新与较旧的 Android 版本几乎没有关系。在各种 Android 支持工件中,只有一小部分 类 是向后移植或兼容性垫片。大量的它——FileProvider
、ViewPager
、ConstraintLayout
等——只是 类 Google 想要提供和支持但想让它们可用在固件之外。
Will the support library FileProvider be enough for all use cases
仅适用于 Android 7.0+。同样,库存 Android 软件包安装程序不支持 Android 7.0.
之前的 content
方案
对于那些想知道如何最终正确安装 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>
背景
到目前为止,有一种安装 APK 文件的简单方法,使用此 intent:
final Intent intent=new Intent(Intent.ACTION_VIEW)
.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
但是,如果您的应用程序针对 Android API 24 及更高版本 (Nougat - 7.0),并且您 运行 此代码或更高版本,您将得到一个例外,如图所示 here ,例如:
android.os.FileUriExposedException: file:///storage/emulated/0/sample.apk exposed beyond app through Intent.getData()
问题
所以我照我说的做了:使用支持库的 FileProvider class,例如:
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 牛轧糖。在 Android 5.0 上,它抛出异常:ActivityNotFoundException。
我试过的
我可以只检查 Android OS 的版本,并使用任何一种方法,但正如我所读,应该只有一个方法可以使用: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 创建可以吗?我应该在其中添加当前应用程序的包名称吗?
如果是AndroidAPI24以上是不是就加个check,是的话就用provider,不是就用普通的Uri.fromFile 打电话?如果我使用它,支持库实际上失去了它的用途,因为它将用于更新的 Android 版本...
支持库 FileProvider 是否足以满足所有用例(当然,假设我有外部存储权限)?
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.
嗯,俗话说,"it takes two to tango"。
要使用任何特定方案(file
、content
、http
等),不仅您必须提供该方案中的数据,而且接收方还需要能够支持接受该方案中的数据。
对于软件包安装程序,仅在 Android 7.0 中添加了对 content
作为方案的支持(然后,可能只是因为 I pointed out the problem)。
Why does it occur?
Is there anything wrong with the custom provider I've created?
可能不会。
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 ?
是的。或者,如果您愿意,可以捕捉 ActivityNotFoundException
并对此做出反应,或者使用 PackageManager
和 resolveActivity()
提前查看给定的 Intent
(例如,带有content
Uri
) 将正常工作。
If I use this, the support library actually loses its purpose, because it will be used for newer Android versions
"support library" 与较新与较旧的 Android 版本几乎没有关系。在各种 Android 支持工件中,只有一小部分 类 是向后移植或兼容性垫片。大量的它——FileProvider
、ViewPager
、ConstraintLayout
等——只是 类 Google 想要提供和支持但想让它们可用在固件之外。
Will the support library FileProvider be enough for all use cases
仅适用于 Android 7.0+。同样,库存 Android 软件包安装程序不支持 Android 7.0.
之前的content
方案
对于那些想知道如何最终正确安装 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>