Android IoT, Automotive, & Smart TV Customizations

Dissecting Android’s Update Engine: Customizing OTA for Embedded IoT Devices

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The Criticality of OTA for Embedded Android IoT

For Android-powered IoT, automotive, and smart TV devices, Over-The-Air (OTA) updates are not merely a convenience; they are a fundamental requirement for security, feature enhancements, and device longevity. Unlike consumer smartphones, embedded devices often operate in headless environments or with limited user interaction, making robust and reliable remote updates paramount. Android’s Update Engine, a core component of the AOSP ecosystem, provides the foundation for A/B (seamless) updates. This deep dive will dissect its mechanisms and guide you through customizing it to meet the unique demands of specialized embedded IoT distributions.

Understanding Android’s Update Engine and A/B Updates

The Android Update Engine (`update_engine`) is responsible for applying system updates without requiring the user to wait during the update process. It leverages the A/B partitioning scheme, where two sets of partitions (A and B) exist for the system, boot, and vendor images. While one set is active and running the device, the Update Engine downloads and applies the update to the inactive set. Upon the next reboot, the device switches to the newly updated inactive partition. If the new partition fails to boot or encounters issues, the device can revert to the previous working partition, ensuring a robust rollback mechanism.

Key Components and Workflow

  • update_engine_daemon: The core daemon that runs in the background. It communicates with an update server, downloads update payloads, verifies their integrity, and applies them to the inactive A/B slot.
  • update_engine_client: A command-line utility used to interact with the update_engine_daemon. It can initiate updates, query update status, and cancel pending updates.
  • boot_control HAL: The Hardware Abstraction Layer that `update_engine` uses to interact with the bootloader to switch active slots.
  • Update Payloads: These are `.zip` or `.brillo_update_payload` files containing the new system images or delta updates. They include metadata, file system images, and operations to apply changes.

The typical workflow involves:

  1. An update server notifies the device or the device polls the server for updates.
  2. update_engine_daemon receives an update URL and payload properties.
  3. The daemon downloads the payload in chunks.
  4. It verifies the payload’s cryptographic signature.
  5. It applies the update operations to the inactive A/B slot.
  6. Upon successful application, it instructs the boot_control HAL to switch the active slot for the next reboot.
  7. The device reboots into the new system.

Generating Custom Update Payloads

For custom Android IoT distributions, you’ll need to generate your own update payloads. The `brillo_update_payload` tool (part of AOSP) is essential for this. It takes two system images (source and target) and generates a delta update, or a single target image for a full update.

Example: Generating a Delta Payload

Assuming you have built AOSP and have two full system images (`system.img`, `vendor.img`, etc.) from different builds:

$ brillo_update_payload generate_update 
  --output update.zip 
  --old_image_dir /path/to/old/aosp/out/target/product/device_name 
  --new_image_dir /path/to/new/aosp/out/target/product/device_name 
  --partition_names system vendor boot 
  --payload_properties_file payload_properties.txt

The `payload_properties.txt` file contains key-value pairs defining the update. These properties are critical for `update_engine` to understand the payload:

FILE_HASH=SHA256(payload_file)
FILE_SIZE=payload_size_in_bytes
METADATA_HASH=SHA256(metadata_blob)
METADATA_SIZE=metadata_blob_size
PAYLOAD_TYPE=delta (or full)

These properties are usually generated automatically by `brillo_update_payload`. For production, payloads must be cryptographically signed. The `brillo_update_payload` tool also supports signing with a private key:

$ brillo_update_payload generate_update 
  ... (other options) 
  --private_key /path/to/your/ota_signing_key.pem

Client-Side Customization for IoT

Customizing the Update Engine for IoT often involves two main areas: modifying the AOSP source for specific device behaviors and developing a robust client-side update orchestration service.

1. Modifying AOSP Source

While `update_engine` is robust, embedded devices might require specific conditions for updates (e.g., only update when docked, specific power levels, or during off-peak hours). You can integrate these checks into a custom service that interacts with `update_engine_client`.

For deeper integration, you might consider modifying `update_engine_daemon` itself, though this is generally discouraged due to maintenance overhead. A better approach is to implement a custom policy client or wrapper that controls when `update_engine_client` is invoked.

2. Custom Update Orchestration Service

This is where most of your IoT-specific logic will reside. An Android service can monitor device state and trigger updates programmatically.

Example: A Basic Update Trigger Service

Let’s imagine a service that only allows updates when the device battery is above 80% and connected to Wi-Fi.

// Example Java (AndroidManifest.xml entries omitted for brevity)
public class CustomUpdateService extends Service {
    private static final String TAG = "CustomUpdateService";
    private Handler handler = new Handler();
    private Runnable checkUpdateRunnable = new Runnable() {
        @Override
        public void run() {
            if (shouldAttemptUpdate()) {
                Log.i(TAG, "Conditions met. Triggering update check.");
                triggerUpdateEngine();
            } else {
                Log.d(TAG, "Conditions not met for update. Retrying later.");
            }
            handler.postDelayed(this, 3600000); // Check hourly
        }
    };

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        handler.post(checkUpdateRunnable);
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        handler.removeCallbacks(checkUpdateRunnable);
        super.onDestroy();
    }

    private boolean shouldAttemptUpdate() {
        // Check battery level
        Intent batteryIntent = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
        int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
        int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
        float batteryPct = level / (float)scale;
        if (batteryPct < 0.80f) {
            Log.d(TAG, "Battery too low: " + (batteryPct * 100) + "%");
            return false;
        }

        // Check network connectivity (e.g., Wi-Fi)
        ConnectivityManager connManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo wifiInfo = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
        if (wifiInfo == null || !wifiInfo.isConnected()) {
            Log.d(TAG, "Not connected to Wi-Fi.");
            return false;
        }

        // Add other custom checks here (e.g., device specific sensors, operational hours, etc.)

        return true;
    }

    private void triggerUpdateEngine() {
        try {
            // This would typically involve communication with your update server
            // to get the payload URL and hash. For demonstration, we use a placeholder.
            String payloadUrl = "http://your.update.server/path/to/update.zip";
            String payloadHash = "SHA256_OF_YOUR_PAYLOAD"; // This must match payload_properties.txt
            long payloadSize = 123456789L; // This must match payload_properties.txt

            // Construct the command for update_engine_client
            // Note: Directly invoking update_engine_client via Runtime.exec might require
            // system app privileges or be done through a system service if possible.
            // For robust solutions, consider AIDL interface to update_engine_daemon if exposed
            // or a privileged system service that can execute shell commands.
            String command = String.format(
                "update_engine_client --payload=%s --update --headers=" 
                + "FILE_HASH=%s;FILE_SIZE=%d;PAYLOAD_TYPE=delta;",
                payloadUrl, payloadHash, payloadSize);

            Log.i(TAG, "Executing update command: " + command);
            Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", command});
            int exitCode = process.waitFor();
            Log.i(TAG, "update_engine_client exited with code: " + exitCode);
            // Handle success/failure based on exitCode and read process output/error streams
        } catch (IOException | InterruptedException e) {
            Log.e(TAG, "Error triggering update: " + e.getMessage());
        }
    }
}

Important Security Note: Directly executing `update_engine_client` via `Runtime.exec` with `su -c` requires a rooted device or elevated permissions, which may not be desirable or available in a production IoT environment. A more secure and robust approach for system applications is to integrate with a custom system service that has appropriate permissions or to expose an AIDL interface to `update_engine_daemon` if AOSP customization allows.

Server-Side and Security Considerations

Your update server plays a crucial role. It needs to host the update payloads and provide a manifest or API endpoint that your client-side service can query. This manifest would contain the latest version information, payload URL, cryptographic hash, and size. Ensure all communication between your device and the update server is secured using HTTPS.

Cryptographic signing of your payloads is non-negotiable. `update_engine` rigorously verifies the signature using public keys embedded in the device’s build. Without proper signing, `update_engine` will reject the update, protecting your devices from unauthorized firmware modifications.

Conclusion

Customizing Android’s Update Engine for embedded IoT devices is a powerful way to ensure reliable, secure, and tailored OTA updates. By understanding its architecture, leveraging tools like `brillo_update_payload`, and building intelligent client-side orchestration, developers can implement sophisticated update policies that cater to the unique operational constraints and requirements of their IoT fleets. This level of control is essential for maintaining device health, deploying new features, and responding promptly to security vulnerabilities in the dynamic landscape of connected devices.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner