可以从自定义Android应用程序中以编程方式安装动态下载的apk。


是的,这是可能的。但要做到这一点,你需要在手机上安装未经验证的消息源。例如,slideMe就是这样做的。我认为你能做的最好的事情是检查应用程序是否存在,并向Android市场发送一个意图。你应该使用android市场的url方案。

market://details?id=package.name

我不知道如何启动这个活动但如果你用那种url启动一个活动。它应该打开android市场,给你安装应用程序的选择。


您可以轻松启动市场链接或安装提示:

Intent promptInstall = new Intent(Intent.ACTION_VIEW)
    .setDataAndType(Uri.parse("file:///path/to/your.apk"), 
                    "application/vnd.android.package-archive");
startActivity(promptInstall); 

Intent goToMarket = new Intent(Intent.ACTION_VIEW)
    .setData(Uri.parse("market://details?id=com.package.name"));
startActivity(goToMarket);

但是,没有用户的明确许可,你不能安装。apks;除非设备和您的程序已经扎根。


好吧,我深入挖掘,并从Android源代码中找到了PackageInstaller应用程序的源代码。

https://github.com/android/platform_packages_apps_packageinstaller

从manifest中我发现它需要许可:

    <uses-permission android:name="android.permission.INSTALL_PACKAGES" />

而实际安装过程发生在确认之后

Intent newIntent = new Intent();
newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mPkgInfo.applicationInfo);
newIntent.setData(mPackageURI);
newIntent.setClass(this, InstallAppProgress.class);
String installerPackageName = getIntent().getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
if (installerPackageName != null) {
   newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, installerPackageName);
}
startActivity(newIntent);

File file = new File(dir, "App.apk");
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
startActivity(intent);

我也遇到过同样的问题,在尝试了几次之后,结果是这样的。我不知道为什么,但是分别设置数据和类型破坏了我的意图。


只是一个扩展,如果有人需要一个库,那么这可能会有所帮助。感谢Raghav


UpdateNode provides an API for Android to install APK packages from inside another App. You can just define your Update online and integrate the API into your App - that's it. Currently the API is in Beta state, but you can already do some tests yourself. Beside that, UpdateNode offers also displaying messages though the system - pretty useful if you want to tell something important to your users. I am part of the client dev team and am using at least the message functionality for my own Android App.

这里介绍了如何集成API


It's worth noting that if you use the DownloadManager to kick off your download, be sure to save it to an external location e.g. setDestinationInExternalFilesDir(c, null, "<your name here>).apk";. The intent with a package-archive type doesn't appear to like the content: scheme used with downloads to an internal location, but does like file:. (Trying to wrap the internal path into a File object and then getting the path doesn't work either, even though it results in a file: url, as the app won't parse the apk; looks like it must be external.)

例子:

int uriIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
String downloadedPackageUriString = cursor.getString(uriIndex);
File mFile = new File(Uri.parse(downloadedPackageUriString).getPath());
Intent promptInstall = new Intent(Intent.ACTION_VIEW)
        .setDataAndType(Uri.fromFile(mFile), "application/vnd.android.package-archive")
        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
appContext.startActivity(promptInstall);

我只是想分享一个事实,我的apk文件保存到我的应用程序“数据”目录,我需要更改apk文件的权限,使其成为世界可读的,以便允许它以这种方式安装,否则系统会抛出“解析错误:解析包存在问题”;所以使用@Horaceman的解决方案,使:

File file = new File(dir, "App.apk");
file.setReadable(true, false);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
startActivity(intent);

另一种解决方案不需要硬编码接收应用程序,因此更安全:

Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
intent.setData( Uri.fromFile(new File(pathToApk)) );
startActivity(intent);

试试这个

String filePath = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
String title = filePath.substring( filePath.lastIndexOf('/')+1, filePath.length() );
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(filePath)), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // without this flag android returned a intent error!
MainActivity.this.startActivity(intent);

试试这个 -在舱单上填写:

uses-permission android:name="android.permission.INSTALL_PACKAGES"
        tools:ignore="ProtectedPermissions"

编写代码:

File sdCard = Environment.getExternalStorageDirectory();
String fileStr = sdCard.getAbsolutePath() + "/Download";// + "app-release.apk";
File file = new File(fileStr, "app-release.apk");
Intent promptInstall = new Intent(Intent.ACTION_VIEW).setDataAndType(Uri.fromFile(file),
                        "application/vnd.android.package-archive");

startActivity(promptInstall);

为这个问题提供的解决方案都适用于23及以下的targetSdkVersion。然而,对于Android N,即API级别24及以上,它们不能工作并崩溃如下异常:

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

这是因为从Android 24开始,用于寻址下载文件的Uri已经改变。例如,一个名为appName.apk的安装文件存储在应用程序的主要外部文件系统上,包名为com.example.test

file:///storage/emulated/0/Android/data/com.example.test/files/appName.apk

适用于API 23及以下,而类似的

content://com.example.test.authorityStr/pathName/Android/data/com.example.test/files/appName.apk

适用于API 24或以上。

关于这方面的更多细节可以在这里找到,我不打算详细介绍。

要回答24及以上版本的targetSdkVersion的问题,必须遵循以下步骤: 将以下内容添加到AndroidManifest.xml:

<application
        android:allowBackup="true"
        android:label="@string/app_name">
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.authorityStr"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/paths"/>
        </provider>
</application>

2. 将下面的paths.xml文件添加到src, main中的res的xml文件夹中:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="pathName"
        path="pathValue"/>
</paths>

pathName是上面示例内容uri示例中显示的,pathValue是系统上的实际路径。 放个"。"是个好主意如果您不想添加任何额外的子目录,则为上面的pathValue(不带引号)。

Write the following code to install the apk with the name appName.apk on the primary external filesystem: File directory = context.getExternalFilesDir(null); File file = new File(directory, fileName); Uri fileUri = Uri.fromFile(file); if (Build.VERSION.SDK_INT >= 24) { fileUri = FileProvider.getUriForFile(context, context.getPackageName(), file); } Intent intent = new Intent(Intent.ACTION_VIEW, fileUri); intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); intent.setDataAndType(fileUri, "application/vnd.android" + ".package-archive"); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); context.startActivity(intent); activity.finish();

在外部文件系统上写入自己应用程序的私有目录时,也不需要任何权限。

我在这里写了一个AutoUpdate库,我使用了上面的方法。


首先在AndroidManifest.xml中添加以下行:

<uses-permission android:name="android.permission.INSTALL_PACKAGES"
    tools:ignore="ProtectedPermissions" />

然后使用以下代码安装apk:

File sdCard = Environment.getExternalStorageDirectory();
            String fileStr = sdCard.getAbsolutePath() + "/MyApp";// + "app-release.apk";
            File file = new File(fileStr, "TaghvimShamsi.apk");
            Intent promptInstall = new Intent(Intent.ACTION_VIEW).setDataAndType(Uri.fromFile(file),
                    "application/vnd.android.package-archive");
            startActivity(promptInstall);

这可以帮助别人很多!

第一:

private static final String APP_DIR = Environment.getExternalStorageDirectory().getAbsolutePath() + "/MyAppFolderInStorage/";

private void install() {
    File file = new File(APP_DIR + fileName);

    if (file.exists()) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        String type = "application/vnd.android.package-archive";

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Uri downloadedApk = FileProvider.getUriForFile(getContext(), "ir.greencode", file);
            intent.setDataAndType(downloadedApk, type);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        } else {
            intent.setDataAndType(Uri.fromFile(file), type);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }

        getContext().startActivity(intent);
    } else {
        Toast.makeText(getContext(), "ّFile not found!", Toast.LENGTH_SHORT).show();
    }
}

第二:对于android 7及以上,你应该在manifest中定义一个提供商,如下所示!

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="ir.greencode"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/paths" />
    </provider>

第三:在res/xml文件夹中定义path.xml,如下所示! 我使用这个路径内部存储,如果你想改变它的东西有一些方法!你可以进入这个链接: FileProvider

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="your_folder_name" path="MyAppFolderInStorage/"/>
</paths>

第四:您应该在manifest中添加此权限:

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

允许应用程序请求安装包。针对api大于25的应用程序必须拥有此权限才能使用Intent.ACTION_INSTALL_PACKAGE。 请确保提供者权限相同!



不要忘记申请权限:

android.Manifest.permission.WRITE_EXTERNAL_STORAGE 
android.Manifest.permission.READ_EXTERNAL_STORAGE

在AndroidManifest.xml中添加提供商和权限:

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
...
<application>
    ...
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths"/>
    </provider>
</application>

创建XML文件提供程序res/ XML /provider_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="external"
        path="." />
    <external-files-path
        name="external_files"
        path="." />
    <cache-path
        name="cache"
        path="." />
    <external-cache-path
        name="external_cache"
        path="." />
    <files-path
        name="files"
        path="." />
</paths>

使用下面的示例代码:

   public class InstallManagerApk extends AppCompatActivity {

    static final String NAME_APK_FILE = "some.apk";
    public static final int REQUEST_INSTALL = 0;

     @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // required permission:
        // android.Manifest.permission.WRITE_EXTERNAL_STORAGE 
        // android.Manifest.permission.READ_EXTERNAL_STORAGE

        installApk();

    }

    ...

    /**
     * Install APK File
     */
    private void installApk() {

        try {

            File filePath = Environment.getExternalStorageDirectory();// path to file apk
            File file = new File(filePath, LoadManagerApkFile.NAME_APK_FILE);

            Uri uri = getApkUri( file.getPath() ); // get Uri for  each SDK Android

            Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
            intent.setData( uri );
            intent.setFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK );
            intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
            intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
            intent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, getApplicationInfo().packageName);

            if ( getPackageManager().queryIntentActivities(intent, 0 ) != null ) {// checked on start Activity

                startActivityForResult(intent, REQUEST_INSTALL);

            } else {
                throw new Exception("don`t start Activity.");
            }

        } catch ( Exception e ) {

            Log.i(TAG + ":InstallApk", "Failed installl APK file", e);
            Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG)
                .show();

        }

    }

    /**
     * Returns a Uri pointing to the APK to install.
     */
    private Uri getApkUri(String path) {

        // Before N, a MODE_WORLD_READABLE file could be passed via the ACTION_INSTALL_PACKAGE
        // Intent. Since N, MODE_WORLD_READABLE files are forbidden, and a FileProvider is
        // recommended.
        boolean useFileProvider = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;

        String tempFilename = "tmp.apk";
        byte[] buffer = new byte[16384];
        int fileMode = useFileProvider ? Context.MODE_PRIVATE : Context.MODE_WORLD_READABLE;
        try (InputStream is = new FileInputStream(new File(path));
             FileOutputStream fout = openFileOutput(tempFilename, fileMode)) {

            int n;
            while ((n = is.read(buffer)) >= 0) {
                fout.write(buffer, 0, n);
            }

        } catch (IOException e) {
            Log.i(TAG + ":getApkUri", "Failed to write temporary APK file", e);
        }

        if (useFileProvider) {

            File toInstall = new File(this.getFilesDir(), tempFilename);
            return FileProvider.getUriForFile(this,  BuildConfig.APPLICATION_ID, toInstall);

        } else {

            return Uri.fromFile(getFileStreamPath(tempFilename));

        }

    }

    /**
     * Listener event on installation APK file
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if(requestCode == REQUEST_INSTALL) {

            if (resultCode == Activity.RESULT_OK) {
                Toast.makeText(this,"Install succeeded!", Toast.LENGTH_SHORT).show();
            } else if (resultCode == Activity.RESULT_CANCELED) {
                Toast.makeText(this,"Install canceled!", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(this,"Install Failed!", Toast.LENGTH_SHORT).show();
            }

        }

    }

    ...

}

在Android Oreo及以上版本中,我们必须采用不同的方法以编程方式安装apk。

 private void installApkProgramatically() {


    try {
        File path = activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);

        File file = new File(path, filename);

        Uri uri;

        if (file.exists()) {

            Intent unKnownSourceIntent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(Uri.parse(String.format("package:%s", activity.getPackageName())));

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

                if (!activity.getPackageManager().canRequestPackageInstalls()) {
                    startActivityForResult(unKnownSourceIntent, Constant.UNKNOWN_RESOURCE_INTENT_REQUEST_CODE);
                } else {
                    Uri fileUri = FileProvider.getUriForFile(activity.getBaseContext(), activity.getApplicationContext().getPackageName() + ".provider", file);
                    Intent intent = new Intent(Intent.ACTION_VIEW, fileUri);
                    intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
                    intent.setDataAndType(fileUri, "application/vnd.android" + ".package-archive");
                    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    startActivity(intent);
                    alertDialog.dismiss();
                }

            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

                Intent intent1 = new Intent(Intent.ACTION_INSTALL_PACKAGE);
                uri = FileProvider.getUriForFile(activity.getApplicationContext(), BuildConfig.APPLICATION_ID + ".provider", file);
                activity.grantUriPermission("com.abcd.xyz", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
                activity.grantUriPermission("com.abcd.xyz", uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                intent1.setDataAndType(uri,
                        "application/*");
                intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent1.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                intent1.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                startActivity(intent1);

            } else {
                Intent intent = new Intent(Intent.ACTION_VIEW);

                uri = Uri.fromFile(file);

                intent.setDataAndType(uri,
                        "application/vnd.android.package-archive");
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(intent);
            }
        } else {

            Log.i(TAG, " file " + file.getPath() + " does not exist");
        }
    } catch (Exception e) {

        Log.i(TAG, "" + e.getMessage());

    }
}

在Oreo及以上版本中,我们需要未知资源安装权限。所以在活动结果中,你必须检查结果的权限

    @Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {

        case Constant.UNKNOWN_RESOURCE_INTENT_REQUEST_CODE:
            switch (resultCode) {
                case Activity.RESULT_OK:
                    installApkProgramatically();

                    break;
                case Activity.RESULT_CANCELED:
                    //unknown resouce installation cancelled

                    break;
            }
            break;
    }
}

大概两个月前,我发现了这个问题。今天回来了,一点头绪也没有。据我所知,我的设置没有任何改变,所以很明显,无论过去的我想到什么,对现在的我来说都不够强大。我终于设法让一些东西重新工作了,所以在这里记录它,以便将来的我和其他可能从另一次尝试中受益的人。

这一尝试代表了原始Android Java安装APK - Session API示例的直接Xamarin c#翻译。它可能需要一些额外的工作,但这至少是一个开始。我让它运行在Android 9设备上,尽管我的项目针对的是Android 11。

InstallApkSessionApi.cs

namespace LauncherDemo.Droid
{
    using System;
    using System.IO;

    using Android.App;
    using Android.Content;
    using Android.Content.PM;
    using Android.OS;
    using Android.Widget;

    [Activity(Label = "InstallApkSessionApi", LaunchMode = LaunchMode.SingleTop)]
    public class InstallApkSessionApi : Activity
    {
        private static readonly string PACKAGE_INSTALLED_ACTION =
                "com.example.android.apis.content.SESSION_API_PACKAGE_INSTALLED";

        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            this.SetContentView(Resource.Layout.install_apk_session_api);

            // Watch for button clicks.
            Button button = this.FindViewById<Button>(Resource.Id.install);
            button.Click += this.Button_Click;
        }

        private void Button_Click(object sender, EventArgs e)
        {
            PackageInstaller.Session session = null;
            try
            {
                PackageInstaller packageInstaller = this.PackageManager.PackageInstaller;
                PackageInstaller.SessionParams @params = new PackageInstaller.SessionParams(
                        PackageInstallMode.FullInstall);
                int sessionId = packageInstaller.CreateSession(@params);
                session = packageInstaller.OpenSession(sessionId);
                this.AddApkToInstallSession("HelloActivity.apk", session);

                // Create an install status receiver.
                Context context = this;
                Intent intent = new Intent(context, typeof(InstallApkSessionApi));
                intent.SetAction(PACKAGE_INSTALLED_ACTION);
                PendingIntent pendingIntent = PendingIntent.GetActivity(context, 0, intent, 0);
                IntentSender statusReceiver = pendingIntent.IntentSender;

                // Commit the session (this will start the installation workflow).
                session.Commit(statusReceiver);
            }
            catch (IOException ex)
            {
                throw new InvalidOperationException("Couldn't install package", ex);
            }
            catch
            {
                if (session != null)
                {
                    session.Abandon();
                }

                throw;
            }
        }

        
        private void AddApkToInstallSession(string assetName, PackageInstaller.Session session)
        {
            // It's recommended to pass the file size to openWrite(). Otherwise installation may fail
            // if the disk is almost full.
            using Stream packageInSession = session.OpenWrite("package", 0, -1);
            using Stream @is = this.Assets.Open(assetName);
            byte[] buffer = new byte[16384];
            int n;
            while ((n = @is.Read(buffer)) > 0)
            {
                packageInSession.Write(buffer, 0, n);
            }
        }

        // Note: this Activity must run in singleTop launchMode for it to be able to receive the intent
        // in onNewIntent().
        protected override void OnNewIntent(Intent intent)
        {
            Bundle extras = intent.Extras;
            if (PACKAGE_INSTALLED_ACTION.Equals(intent.Action))
            {
                PackageInstallStatus status = (PackageInstallStatus)extras.GetInt(PackageInstaller.ExtraStatus);
                string message = extras.GetString(PackageInstaller.ExtraStatusMessage);
                switch (status)
                {
                    case PackageInstallStatus.PendingUserAction:
                        // This test app isn't privileged, so the user has to confirm the install.
                        Intent confirmIntent = (Intent) extras.Get(Intent.ExtraIntent);
                        this.StartActivity(confirmIntent);
                        break;
                    case PackageInstallStatus.Success:
                        Toast.MakeText(this, "Install succeeded!", ToastLength.Short).Show();
                        break;
                    case PackageInstallStatus.Failure:
                    case PackageInstallStatus.FailureAborted:
                    case PackageInstallStatus.FailureBlocked:
                    case PackageInstallStatus.FailureConflict:
                    case PackageInstallStatus.FailureIncompatible:
                    case PackageInstallStatus.FailureInvalid:
                    case PackageInstallStatus.FailureStorage:
                        Toast.MakeText(this, "Install failed! " + status + ", " + message,
                                ToastLength.Short).Show();
                        break;
                    default:
                        Toast.MakeText(this, "Unrecognized status received from installer: " + status,
                                ToastLength.Short).Show();
                        break;
                }
            }
        }
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.launcherdemo" android:installLocation="auto">
    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
    <application android:label="LauncherDemo.Android" android:theme="@style/MainTheme" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>

What I particularly like about this approach is that it does not require any special work in the manifest - no broadcast receivers, file providers, etc. Granted, this takes as its source some APK in the app's assets, whereas a more useful system will probably use some given APK path. I imagine that will introduce a certain level of additional complexity. In addition, I never ran into any issues here (at least as far as I could tell) with the Xamarin GC closing streams before Android was done with them. Nor did I have any issues with the APK not being parsed. I made sure to use a signed APK (the one generated by Visual Studio when deploying to the device worked just fine), and again I did not run into any file access permission issues simply due to using an APK from the app's assets in this example.

这里的一些其他答案提供的一件事是使侧面加载许可授予更加简化的想法。Yabaze Cool的回答提供了如下特色:

Intent unKnownSourceIntent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES). setdata (Uri.parse(字符串。格式(“包:% s activity.getPackageName ()))); 如果(Build.VERSION。SDK_INT >= Build.VERSION_CODES.O) { if (!activity.getPackageManager(). canrequestpackageinstaller ()) { startActivityForResult (unKnownSourceIntent Constant.UNKNOWN_RESOURCE_INTENT_REQUEST_CODE); ...

当我测试我的翻译时,我卸载了启动器演示程序和它安装的应用程序。没有向canrequestpackageinstalled提供检查,导致我不得不手动按下一个额外的设置按钮,将我带到与上面的ACTION_MANAGE_UNKNOWN_APP_SOURCES意图相同的对话框。因此,添加这个逻辑可以帮助在一定程度上简化用户的安装过程。


基于@ uroovic podkrinik的回答。

通过APK安装应用程序对于不同版本的android可能有所不同(API级别21-30):

private var uri: Uri? = null
private var manager: DownloadManager? = null
private var file: File? = null
private var request: DownloadManager.Request? = null

private val REQUEST_WRITE_PERMISSION = 786
private val REQUEST_INSTALL_PACKAGE = 1234

private var receiver: BroadcastReceiver? = null
private var installIntent: Intent? = null

...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val externalStorageDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
    } else {
        @Suppress("DEPRECATION")
        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
    }
    val destination = "$externalStorageDir/Application.apk"
    uri = Uri.parse("file://$destination")

    file = File(destination)
    file?.let { if (it.exists()) it.delete() }
    
    request = DownloadManager.Request(Uri.parse("https://path_to_file/application.apk"))
    request?.let {
        it.setDescription("Update App")
        it.setTitle("Application")
        it.setDestinationUri(uri)
    }
    manager = context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager

    // for level android api >= 23 needs permission to write to external storage
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (ContextCompat.checkSelfPermission(context!!, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
            // here you can display the loading diagram
            registerReceiver()
        } else {
            // request for permission to write to external storage
            requestPermissions(
                arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                REQUEST_WRITE_PERMISSION
            )
        }
    } else {
        // here you can display the loading diagram
        registerReceiver()
    }
}

创建并注册接收者:

private val onDownloadComplete = object : BroadcastReceiver() {
    // install app when apk is loaded
    override fun onReceive(ctxt: Context, intent: Intent) {
        val mimeType = "application/vnd.android.package-archive"
        receiver = this
        try {
            installIntent = Intent(Intent.ACTION_VIEW)
            installIntent?.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP

            // for android api >= 24 requires FileProvider
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                installIntent?.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

                val fileProviderURI = FileProvider.getUriForFile(
                    context!!,
                    context!!.applicationContext.packageName + ".provider",
                    file!!)

                installIntent?.setDataAndType(fileProviderURI, mimeType)

                // for android api >= 26 requires permission to install from APK in settings
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    if (context!!.applicationContext.packageManager.canRequestPackageInstalls()) {
                        installFromAPK()
                    } else goToSecuritySettings()
                } else installFromAPK()
            } else {
                // for android api < 24 used file:// instead content://
                // (no need to use FileProvider)
                installIntent?.setDataAndType(uri, mimeType)
                installFromAPK()
            }
        } catch (e: Exception) {
            // view error message
        }
    }
}

private fun registerReceiver() {
    manager!!.enqueue(request)
    context?.registerReceiver(
        onDownloadComplete,
        IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
    )
}

private fun installFromAPK() {
    try {
        startActivity(installIntent)
        context?.unregisterReceiver(receiver)
        activity?.finish()
    } catch (e: Exception) {
        // view error message
    }
}

// go to settings for get permission install from APK
private fun goToSecuritySettings() {
    val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(
        Uri.parse(String.format(
            "package:%s",
            context!!.applicationContext.packageName
        ))
    )
    try {
        startActivityForResult(intent, REQUEST_INSTALL_PACKAGE)
    } catch (e: Exception) {
        // view error message
    }
}

拦截权限请求WRITE_EXTERNAL_STORAGE的结果:

override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == REQUEST_WRITE_PERMISSION
            && grantResults.isNotEmpty()
            && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        try {
            // here you can display the loading diagram
            registerReceiver()
        } catch (e: Exception) {
            // view error message
        }
    }
}

在安全设置中拦截用户选择的结果:

@RequiresApi(api = Build.VERSION_CODES.O)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_INSTALL_PACKAGE
            && resultCode == AppCompatActivity.RESULT_OK) {
        if (context!!.applicationContext.packageManager.canRequestPackageInstalls()) {
            installFromAPK()
        }
    } else {
        // view error message
    }
}

添加到您的清单:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application...>
    ...
    <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>
    ...
</application>

将provider_paths.xml文件添加到res/xml:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path name="external_files" path="."/>
</paths>

对于android API级别= 30,从安全设置返回不工作, 所以使用浏览器安装:

try {
    val intent = Intent(Intent.ACTION_VIEW)
    intent.data = Uri.parse("https://path_to_file/application.apk")
    startActivity(intent)
    activity?.finish()
} catch (e: ActivityNotFoundException) { }