【问题标题】:How to open an APK file for all Android versions如何打开适用于所有 Android 版本的 APK 文件
【发布时间】:2017-04-30 19:56:48
【问题描述】:

背景

到目前为止,有一种安装 APK 文件的简单方法,使用此意图:

    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 类,如下所示:

    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 是否足以满足所有用例(当然,假设我确实有外部存储权限)?

【问题讨论】:

  • 您确定不应该使用ACTION_INSTALL_PACKAGE 而不是ACTION_VIEW?这似乎可以解释ActivityNotFoundException
  • @MikeM。此操作也会引发相同的异常(即使添加了 REQUEST_INSTALL_PACKAGES 权限),并且可以使用 ACTION_VIEW,因为它也用于旧的 Android 版本,并且也可以在这里工作。
  • 是的,似乎它们都是有效的。猜猜我以前从未尝试过以编程方式安装应用程序。
  • 最简单的解决方案是第 3 点)
  • @MuratK。我知道这是可能的,但不应该是官方的标准方式吗?如果它仅适用于 API 24 及更高版本,那么拥有支持库有什么意义......

标签: android android-intent apk android-7.0-nougat android-fileprovider


【解决方案1】:

我可以只检查 Android 操作系统的版本,并使用任何一种方法,但正如我所读到的,应该有一个方法可以使用:FileProvider。

好吧,俗话说,“探戈需要两个人”。

要使用任何特定的方案(filecontenthttp 等),您不仅必须提供该方案中的数据,而且接收者需要能够支持接受数据在那个方案中。

对于包安装程序,仅在 Android 7.0 中添加了对 content 作为方案的支持(然后,可能只是因为 I pointed out the problem)。

为什么会发生?

因为 Google(请参阅 thisthis)。

我创建的自定义提供程序有什么问题吗?

可能不会。

我是否应该添加检查它是否是 Android API 24 及更高版本,如果是,则使用提供程序,如果不是,则使用普通的 Uri.fromFile 调用?

是的。或者,如果您愿意,抓住ActivityNotFoundException 并对此做出反应,或者使用PackageManagerresolveActivity() 提前查看给定的Intent(例如,带有content Uri 的)将正常工作。

如果我使用它,支持库实际上失去了它的用途,因为它将用于更新的 Android 版本

“支持库”与较新和较旧的 Android 版本关系不大。在各种 Android 支持工件中,只有一小部分类是向后移植或兼容性填充程序。大量的它——FileProviderViewPagerConstraintLayout 等——只是谷歌想要提供和支持但想让它们在固件之外可用的类。

支持库 FileProvider 是否足以满足所有用例

仅适用于 Android 7.0+。同样,Android 7.0 之前的标准 Android 软件包安装程序不支持 content 方案。

【讨论】:

  • 所以,我必须检查版本,如果它来自 API 24 及更高版本,则使用我自己的类(或支持库),如果不是,则使用 File Uri?这个支持库类似乎越用越没用...
  • @androiddeveloper:“我必须检查版本...”——是的,由于包安装程序的限制。 “这个支持库类似乎越用越没用”——这个问题与支持库无关。您自己的ContentProvider、我的StreamProvider 或任何其他第三方ContentProvider 都会遇到同样的问题。让我试试粗体字:现有的 Android 软件包安装程序不支持 Android 7.0 之前的 content 方案
  • 如何共享文件(正如我之前问过的,这里:stackoverflow.com/q/40941709/878126)?似乎 ContentProvider 在这件事上得到了永远的支持,不是吗?对我来说,文件共享与打开文件的支持不同,这让我感到很奇怪......
  • @androiddeveloper:“对我来说,共享文件与打开文件的支持不同,这让我感到很奇怪”——“不同的支持”取决于收件人应用程序的开发人员。有些应用支持content,有些不支持。更多应用应该支持content。但是,应用开发者可以为所欲为。
  • 等等,我使用 FileProvider 的代码可能不适用于某些应用程序,而使用它的旧 API(意思是“Uri.fromFile”)可以?
【解决方案2】:

只为那些想知道如何最终正确安装 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>

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多