'How to programmatically install private .apk?

I have managed to download a .apk file I uploaded to an AWS S3 bucket using DownloadManager, but I can't install the package with out user confirmation. I am working in a private dedicated device app that manages the device owner-level stuff. In this case, I have provisioned my device with adb shell set-device-owner. I have been looking all over the place and I have figured out some parts of how to achieve the installation.

I'm working with Android 8.1 and our devices won't be upgraded to a newer Android version.

My Manifest file has the provider

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths" />
</provider>

as well as the permission <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES>

My provider_paths.xml file is

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

and the code that executes the installation is in a separate class InstallationUtils.java with a public static method installPackage that gets called from a button in the main activity.

public static boolean installPackage(Context context) {
    File toInstall = new File(context.getFilesDir(), appName+".apk");
    
    if (!toInstall.exists()) {
        Log.e("installPackage", "File does not exist!");
        // Handle case
        return false;
    }

    if (!toInstall.canRead()) {
        Log.e("installPackage", "File can't be read!");
        // Handle case
        return fasle;
    }


    Uri apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", toInstall);
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    context.startActivity(intent);

Some parts of the code like the directories are hard-coded because I am trying to make it work first before using best practices. I have also tried using <external-path> and <external-files-path> as well as having a copy of my APK on all these directories (or at least what I believe are the correct directories, I am fairly new to Android development).

The closest I have gotten to installing the package was being able to open the installer activity, but it says "Unknown" in the action bar, and "Staging app..." in the middle of the Activity. This activity immediately stops with a dialog that says: "There was a problem parsing the package" with only the option to press "OK", which goes back to the main activity of the DPC app.

I also saw other alternatives using PackageInstaller, but I don't quite understand them.


Code for downloading APK from S3 bucket

The way I downloaded my .apk file was getting a pre-signed URL from AWS S3.

public static long downloadApkFromS3( Context context, String fileName, String downloadUrl, Handler updateHandler) {

    DownloadManager downloadManager = (DownloadManager) context
            .getSystemService(Context.DOWNLOAD_SERVICE);

    Uri uri = Uri.parse(downloadUrl);
    DownloadManager.Request request = new DownloadManager.Request(uri);

    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
    request.setDestinationInExternalPublicDir(getFilesDir().toString(), fileName + ".apk");
    request.setMimeType("application/vnd.android.package-archive");

    long downloadId = downloadManager.enqueue(request);

    // Run task in background thread to check download progress later
    executor.execute(new Runnable() {
        @Override
        public void run() {
            int progress = 0;
            boolean isDownloadFinished = false;
            while (!isDownloadFinished) {
                try {
                    Log.i("Thread", "Sleeping");
                    Thread.sleep(1000); // Check every second.
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
               
                Cursor cursor = downloadManager
                        .query(new DownloadManager.Query().setFilterById(downloadId));
                if (cursor.moveToFirst()) {
                    int downloadStatus = cursor
                            .getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
                   
                    switch (downloadStatus) {
                        case DownloadManager.STATUS_RUNNING:
                            long totalBytes = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                            if (totalBytes > 0) {
                                long downloadedBytes = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                                progress = (int) (downloadedBytes * 100 / totalBytes);
                            }
                            break;

                        case DownloadManager.STATUS_SUCCESSFUL:
                            progress = 100;
                            isDownloadFinished = true;

                            // Release executor
                            executor.shutdown();

                            // Release handler
                            updateHandler.removeCallbacksAndMessages(null);
                           

                            break;

                        case DownloadManager.STATUS_PAUSED:
                            break;
                        case DownloadManager.STATUS_PENDING:
                            break;
                        case DownloadManager.STATUS_FAILED:
                            isDownloadFinished = true;
                            break;
                    }
                    Message message = Message.obtain();
                    message.what = UPDATE_DOWNLOAD_PROGRESS;
                    message.arg1 = progress;

                    updateHandler.sendMessage(message);
                }
            }
        }
    });
    return downloadId;
}

This code gets called from the main activity with with the lines

Handler updateHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
    @Override
    public boolean handleMessage(@NonNull Message message) {
        if (message.what == UPDATE_DOWNLOAD_PROGRESS) {
            int downloadProgress = message.arg1;                           
            // Update progress bar
            mProgressBar.setProgress(downloadProgress);
            mProgressPercentage.setText(Integer.toString(downloadProgress) + " %");
        }
        return true;
        }
    });
InstallationUtils.downloadApkFromUrl(this, "testDownloadFile", downloadUrl, updateHandler);

where downloadUrl is the presigned URL from the S3 bucket and updateHandler is just for updating the UI when the download is running.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source