Device Admin APK checksum mismatch when provisioning via QR on a fresh Android device
00:05 29 Nov 2025

I'm trying to provision an Android Device Admin app on a completely fresh device using a QR code. Previously, I encountered an error about missing components, but now the app fails with a checksum mismatch error.

Setup:

-I build a release APK using Gradle in GitHub Actions with a release keystore.

-The workflow copies the APK to a server via SCP.

-SHA256 of the APK in GitHub Actions:

78b718df0e56ce5c6f3673c4a2ce277dc83d001544234cf6b00648709828048c

-SHA256 of the APK downloaded from the server:

78b718df0e56ce5c6f3673c4a2ce277dc83d001544234cf6b00648709828048c

Provisioning JSON:

{
  "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME": "com.example.kios_app/.KioskDeviceAdminReceiver",
  "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION": "http://example.com/downloads/app-release.apk",
  "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM": "78b718df0e56ce5c6f3673c4a2ce277dc83d001544234cf6b00648709828048c",
  "android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE": {"server_url": "http://example.com/api"},
  "android.app.extra.PROVISIONING_SKIP_ENCRYPTION": true,
  "android.app.extra.PROVISIONING_WIFI_SSID": "MySSID",
  "android.app.extra.PROVISIONING_WIFI_PASSWORD": "MyPassword",
  "android.app.extra.PROVISIONING_WIFI_SECURITY_TYPE": "WPA"
}

Manifest snippet:




    
    
    
    
    
    
    
    
    
    
    
    

    
    

    

    

        
            
            
                
                
                
                
            
        

        
            
                
                
            
        

        
            
                
            
        

        
            
        

        
            
                
                
                
                
            
        

        
            
        
    

KioskDeviceAdminReceiver class:

package com.example.kios_app

import android.app.admin.DeviceAdminReceiver
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.UserManager
import android.provider.Settings
import android.util.Log

class KioskDeviceAdminReceiver : DeviceAdminReceiver() {

    companion object {
        private const val TAG = "KioskDeviceAdmin"
        const val REQUEST_CODE_ENABLE_ADMIN = 1001

        fun getComponentName(context: Context): ComponentName =
            ComponentName(context.applicationContext, KioskDeviceAdminReceiver::class.java)
    }

    override fun onEnabled(context: Context, intent: Intent) {
        super.onEnabled(context, intent)
        Log.d(TAG, "Device Admin Enabled")

        setupDevicePolicies(context)
    }

    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)
        Log.d(TAG, "onReceive: ${intent.action}")

        when (intent.action) {
            ACTION_DEVICE_ADMIN_ENABLED -> {
                Log.d(TAG, "Device admin enabled")
                setupDevicePolicies(context)
            }
            ACTION_PROFILE_PROVISIONING_COMPLETE -> {
                Log.d(TAG, "Profile provisioning complete")
                completeDeviceOwnerSetup(context)
            }
        }
    }

    override fun onProfileProvisioningComplete(context: Context, intent: Intent) {
        super.onProfileProvisioningComplete(context, intent)
        Log.d(TAG, "🎯 Profile Provisioning Complete - Device Owner mode activated")

        completeDeviceOwnerSetup(context)
    }

    private fun setupDevicePolicies(context: Context) {
        try {
            val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
            val admin = getComponentName(context)
            
            dpm.setLockTaskPackages(admin, arrayOf(context.packageName))
            
            if (dpm.isDeviceOwnerApp(context.packageName)) {
                setupDeviceOwnerPolicies(dpm, admin, context)
                Log.d(TAG, "🚀 Device Owner policies applied")
            } else {
                Log.d(TAG, "ℹ️ Regular Device Admin mode")
            }

        } catch (e: Exception) {
            Log.e(TAG, "Error setting up device policies", e)
        }
    }

    private fun completeDeviceOwnerSetup(context: Context) {
        try {
            val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager

            if (dpm.isDeviceOwnerApp(context.packageName)) {
                Log.d(TAG, "Device Owner confirmed")
                
                val intent = Intent(context, MainActivity::class.java).apply {
                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
                }
                context.startActivity(intent)
            } else {
                Log.w(TAG, "Not Device Owner after provisioning")
            }

        } catch (e: Exception) {
            Log.e(TAG, "Error completing device owner setup", e)
        }
    }

    private fun setupDeviceOwnerPolicies(dpm: DevicePolicyManager, admin: ComponentName, context: Context) {
        try {
            dpm.setPasswordMinimumLength(admin, 0)
            dpm.setPasswordQuality(admin, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED)
            
            dpm.setSecureSetting(admin, Settings.Secure.INSTALL_NON_MARKET_APPS, "1")
            
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                dpm.addUserRestriction(admin, UserManager.DISALLOW_SAFE_BOOT)
                dpm.addUserRestriction(admin, UserManager.DISALLOW_FACTORY_RESET)
                dpm.addUserRestriction(admin, UserManager.DISALLOW_ADD_USER)
                dpm.addUserRestriction(admin, UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA)
            }
            
            try {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    dpm.setStatusBarDisabled(admin, true)
                }
            } catch (e: Exception) {
                Log.w(TAG, "Cannot disable status bar: ${e.message}")
            }
            
            dpm.addUserRestriction(admin, UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS)

            Log.d(TAG, "Device Owner policies setup complete")

        } catch (e: Exception) {
            Log.e(TAG, "Error setting device owner policies", e)
        }
    }

    override fun onDisableRequested(context: Context, intent: Intent): CharSequence {
        return "Disabling device administration will take it out of kiosk mode."
    }

    override fun onDisabled(context: Context, intent: Intent) {
        super.onDisabled(context, intent)
        Log.w(TAG, "Device Admin Disabled")
    }
}

Observations / Attempts:

-APK on the server is identical to the one built locally (SHA256 matches).

-Downloading with curl -L including timestamp to bypass caching still produces the same checksum.

-APK is signed with the release keystore.

-Device fails provisioning with checksum mismatch.

Questions:

  1. Why does the device complain about a checksum mismatch even though SHA256 matches?

  2. Could it be caused by HTTP caching, signing, or QR provisioning metadata?

  3. How can I ensure the device accepts the APK for provisioning without checksum errors?

Environment:

-Android 13/14 device

-Gradle 8.13

-GitHub Actions runner: ubuntu-latest

-Nginx serving the APK

android kotlin device-owner