Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Remote APK Download and Update Functionality in Android Applications

Tech May 12 2

An application reqiures several Manifest permissions to enable downolading and installing APK files from a remote source.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" tools:ignore="ProtectedPermissions" />
<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" />

Within the application tag, configure network security and a FileProvider for Nougat and above compatibility.

<application
    android:networkSecurityConfig="@xml/network_config"
    ... >

    <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>
</application>

Define the provider paths in res/xml/provider_paths.xml.

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

Create res/xml/network_config.xml to allow cleartext traffic for HTTP downloads.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true"/>
</network-security-config>

A custom PackageUpdater class handles the core logic: checking for updates, downloading, and installing.

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

public class PackageUpdater {
    private Context appContext;
    private String remoteApkUrl;
    private String versionCheckUrl;
    private File localApkFile;
    private ProgressBar downloadProgressBar;
    private TextView statusTextView;
    private boolean downloadCancelled = false;
    private Handler uiHandler;

    private static final int MSG_SHOW_UPDATE = 1;
    private static final int MSG_UPDATE_PROGRESS = 2;
    private static final int MSG_DOWNLOAD_COMPLETE = 3;

    public PackageUpdater(Context context, String baseUrl) {
        this.appContext = context;
        this.remoteApkUrl = baseUrl + "/latest.apk";
        this.versionCheckUrl = baseUrl + "/version-info";
        File downloadsDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
        this.localApkFile = new File(downloadsDir, "update_package.apk");
        this.uiHandler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                handleUiMessage(msg);
            }
        };
    }

    public void verifyVersion() {
        new Thread(() -> {
            try {
                String serverVersion = fetchServerVersion();
                String clientVersion = getAppVersionName();
                if (isNewVersionAvailable(clientVersion, serverVersion)) {
                    uiHandler.sendEmptyMessage(MSG_SHOW_UPDATE);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }

    private String fetchServerVersion() throws IOException {
        HttpURLConnection conn = null;
        BufferedReader reader = null;
        try {
            URL url = new URL(versionCheckUrl);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            InputStream is = conn.getInputStream();
            reader = new BufferedReader(new InputStreamReader(is));
            StringBuilder builder = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                builder.append(line);
            }
            return builder.toString();
        } finally {
            if (reader != null) reader.close();
            if (conn != null) conn.disconnect();
        }
    }

    private String getAppVersionName() {
        try {
            return appContext.getPackageManager()
                    .getPackageInfo(appContext.getPackageName(), 0).versionName;
        } catch (Exception e) {
            return "1.0";
        }
    }

    private boolean isNewVersionAvailable(String local, String remote) {
        return !local.equals(remote);
    }

    private void showUpdatePrompt() {
        new AlertDialog.Builder(appContext)
                .setTitle("Update Available")
                .setMessage("A new version is ready to download.")
                .setPositiveButton("Download", (dialog, which) -> startDownload())
                .setCancelable(false)
                .show();
    }

    private void startDownload() {
        AlertDialog progressDialog = createProgressDialog();
        progressDialog.show();
        new Thread(() -> performDownload(progressDialog)).start();
    }

    private AlertDialog createProgressDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(appContext);
        builder.setTitle("Downloading Update");
        builder.setView(R.layout.dialog_download_progress);
        builder.setNegativeButton("Cancel", (dialog, which) -> downloadCancelled = true);
        AlertDialog dialog = builder.create();
        dialog.setCanceledOnTouchOutside(false);
        dialog.setOnShowListener(dialogInterface -> {
            downloadProgressBar = dialog.findViewById(R.id.progress_bar);
            statusTextView = dialog.findViewById(R.id.status_text);
        });
        return dialog;
    }

    private void performDownload(AlertDialog dialog) {
        try {
            URL downloadUrl = new URL(remoteApkUrl);
            HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
            connection.connect();
            int fileSize = connection.getContentLength();
            InputStream input = connection.getInputStream();
            FileOutputStream output = new FileOutputStream(localApkFile);
            byte[] buffer = new byte[4096];
            int totalBytesRead = 0;
            int bytesRead;
            while ((bytesRead = input.read(buffer)) != -1 && !downloadCancelled) {
                output.write(buffer, 0, bytesRead);
                totalBytesRead += bytesRead;
                int percent = (int) ((totalBytesRead * 100.0f) / fileSize);
                Message msg = uiHandler.obtainMessage(MSG_UPDATE_PROGRESS, percent, 0);
                uiHandler.sendMessage(msg);
            }
            output.close();
            input.close();
            connection.disconnect();
            if (!downloadCancelled) {
                uiHandler.sendEmptyMessage(MSG_DOWNLOAD_COMPLETE);
                dialog.dismiss();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void handleUiMessage(Message msg) {
        switch (msg.what) {
            case MSG_SHOW_UPDATE:
                showUpdatePrompt();
                break;
            case MSG_UPDATE_PROGRESS:
                int progress = msg.arg1;
                if (downloadProgressBar != null) downloadProgressBar.setProgress(progress);
                if (statusTextView != null) statusTextView.setText(progress + "%");
                break;
            case MSG_DOWNLOAD_COMPLETE:
                executeInstallation();
                break;
        }
    }

    private void executeInstallation() {
        Intent installIntent = new Intent(Intent.ACTION_VIEW);
        installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

        Uri apkUri;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            String authority = appContext.getPackageName() + ".fileprovider";
            apkUri = FileProvider.getUriForFile(appContext, authority, localApkFile);
        } else {
            apkUri = Uri.fromFile(localApkFile);
        }
        installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        appContext.startActivity(installIntent);
    }
}

Define the progress dialog layout in res/layout/dialog_download_progress.xml.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <ProgressBar
        android:id="@+id/progress_bar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/status_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="0%"
        android:layout_marginTop="8dp" />
</LinearLayout>

In your main activity, request runtime permissions and initialize the updater.

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private PackageUpdater appUpdater;
    private static final int PERMISSION_REQUEST_CODE = 200;
    private Handler periodicCheckHandler = new Handler();
    private static final long CHECK_INTERVAL = 3600000; // 1 hour

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        appUpdater = new PackageUpdater(this, "https://your-server.com/api");
        verifyPermissionsAndCheckUpdate();
        startPeriodicUpdateCheck();
    }

    private void verifyPermissionsAndCheckUpdate() {
        String[] requiredPermissions = {
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.INTERNET
        };
        List<String> pendingPermissions = new ArrayList<>();
        for (String perm : requiredPermissions) {
            if (ContextCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED) {
                pendingPermissions.add(perm);
            }
        }
        if (pendingPermissions.isEmpty()) {
            appUpdater.verifyVersion();
        } else {
            ActivityCompat.requestPermissions(this,
                    pendingPermissions.toArray(new String[0]),
                    PERMISSION_REQUEST_CODE);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           String[] permissions,
                                           int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == PERMISSION_REQUEST_CODE) {
            boolean allGranted = true;
            for (int result : grantResults) {
                if (result != PackageManager.PERMISSION_GRANTED) {
                    allGranted = false;
                    break;
                }
            }
            if (allGranted) {
                appUpdater.verifyVersion();
            }
        }
    }

    private void startPeriodicUpdateCheck() {
        periodicCheckHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                verifyPermissionsAndCheckUpdate();
                periodicCheckHandler.postDelayed(this, CHECK_INTERVAL);
            }
        }, CHECK_INTERVAL);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        periodicCheckHandler.removeCallbacksAndMessages(null);
    }
}

Configure the Gradle build file to generate unique version names and output filenames.

android {
    compileSdkVersion 33
    defaultConfig {
        applicationId "com.example.app"
        minSdkVersion 23
        targetSdkVersion 33
        versionCode 1
        versionName generateVersionName()
    }
    buildTypes {
        release {
            minifyEnabled false
            buildConfigField("String", "BASE_URL", '"https://prod-server.com/api"')
            setProperty("archivesBaseName", "app_${generateVersionName()}")
        }
        debug {
            buildConfigField("String", "BASE_URL", '"https://dev-server.com/api"')
            applicationIdSuffix ".debug"
        }
    }
}

def generateVersionName() {
    return new Date().format('yyyyMMdd.HHmm', TimeZone.getTimeZone('UTC'))
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.