javaandroidandroid-12device-ownerandroid-device-owner

Issues setting up an Android Device Owner app with QR provisioning


I'm having some serious headaches over trying to make QR provisioning work on a mobile device that is supposed to run an app in kiosk mode.

Here's some initial disclaimers in case you're asking:

The problem:

The issue i'm having happens in the setup wizard as i try to make a qr provisioning of my app to set it as the device owner and enforce DPM policies, which simply throws back a generic "Can't set up device. Contact your IT admin for help".

The way I'm currently trying to do it is:

Everything listed in the section below gave the same exact outcome.

What I tried:

Initially, I tried some other ways to get my custom app to run as the device owner.

set-device-owner

and

set-profile-owner

are denied, returning a "Calling Identity is not authorized" error. I've concluded that this likely happens because there already is an existing profile and/or device owner which is protecting the device.

What I did not (and will not) try:

My current setup:

This is the current JSON payload that I'm using for the QR code provisioning:

{
    "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME": "com.mycompany.marcatempo/com.mycompany.marcatempo.MyDeviceAdminReceiver",
    "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_LOCATION": "https://path-to-my.apk",
    "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM": "some-valid-checksum",
    "android.app.extra.PROVISIONING_SKIP_ENCRYPTION": true,

    "android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED": true,
    "android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE": {
    }
}

This was very confusing for me, because many templates were actually different on different reference sites:

PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME: I've tried both writing it like this

com.mycompany.marcatempo/com.mycompany.marcatempo.MyDeviceAdminReceiver

and like this

com.mycompany.marcatempo/.MyDeviceAdminReceiver

As some sites showed it written differently.

PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM: I've generated this in different ways, as some sites showed this step differently as well. I tried to generate the checksum using Git Bash:

cat C:/path/to/app-release.apk | "C:/Program Files/OpenSSL-Win64/bin/openssl.exe" dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '='

And using Windows Powershell:

Get-FileHash -Path "C:\path\to\release-app.apk" -Algorithm SHA256

Both gave me some very different results.

PROVISIONING_SKIP_ENCRYPTION: I've attempted to set this as true, false and even removing it completely.

PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED | PROVISIONING_ADMIN_EXTRAS_BUNDLE: These are extra options which seem harmless. I've included them because one of the templates did.

According to this question, on Android 12+ devices these kinds of apps require some extra classes and an extended manifest setup. I already did try to implement those things listed in the solution, but it was no use for me. Below I'll append the relevant files:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.mycompany.marcatempo">

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.NFC" />
    <uses-permission android:name="android.permission.INTERNET" />

    <uses-feature android:name="android.hardware.nfc.hce" android:required="true"/>

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

    <application
        android:testOnly="false"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SimpleNFC">

        <service
            android:name="com.mycompany.marcatempo.MyHceService"
            android:exported="true"
            android:permission="android.permission.BIND_NFC_SERVICE">

            <intent-filter>
                <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
            </intent-filter>

            <meta-data
                android:name="android.nfc.cardemulation.host_apdu_service"
                android:resource="@xml/apduservice" />
        </service>

        <activity
            android:name="com.mycompany.marcatempo.MainActivity"
            android:launchMode="singleInstance"
            android:clearTaskOnLaunch="true"
            android:stateNotNeeded="true"
            android:exported="true"
            android:screenOrientation="portrait"
            tools:ignore="DiscouragedApi,LockedOrientationActivity">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
                <category android:name="android.intent.category.HOME" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <activity
            android:name="com.mycompany.marcatempo.AdminPolicyComplianceActivity"
            android:exported="true"
            android:enabled="@bool/provision_mode_compliance_enabled"
            android:theme="@style/Theme.AppCompat"
            android:permission="android.permission.BIND_DEVICE_ADMIN">
            <intent-filter>
                <action android:name="android.app.action.ADMIN_POLICY_COMPLIANCE"/>
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <activity
            android:name="com.mycompany.marcatempo.ProvisioningModeActivity"
            android:exported="true"
            android:enabled="@bool/provision_mode_compliance_enabled"
            android:theme="@style/Theme.AppCompat"
            android:permission="android.permission.BIND_DEVICE_ADMIN">
            <intent-filter>
                <action android:name="android.app.action.GET_PROVISIONING_MODE" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>


        <receiver
            android:name="com.mycompany.marcatempo.MyDeviceAdminReceiver"
            android:exported="true"
            android:permission="android.permission.BIND_DEVICE_ADMIN">
            <meta-data
                android:name="android.app.device_admin"
                android:resource="@xml/device_admin_receiver" />
            <intent-filter>
                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
            </intent-filter>
        </receiver>

        <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/file_paths" />
        </provider>

    </application>

</manifest>

AdminPolicyComplianceActivity.java

import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;



public class AdminPolicyComplianceActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_policy_compliance);

        Intent intent = getIntent();

        setResult(RESULT_OK, intent);
        finish();
    }
}

ProvisioningModeActivity.java

import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE;

import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;

import androidx.appcompat.app.AppCompatActivity;

import java.util.List;

public class ProvisioningModeActivity extends AppCompatActivity {

    private String EXTRA_PROVISIONING_ALLOWED_PROVISIONING_MODES = "android.app.extra.PROVISIONING_ALLOWED_PROVISIONING_MODES";
    private int PROVISIONING_MODE_FULLY_MANAGED_DEVICE = 1;
    private int PROVISIONING_MODE_MANAGED_PROFILE = 2;
    private String EXTRA_PROVISIONING_MODE = "android.app.extra.PROVISIONING_MODE";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_get_provisioning_mode);

        Intent intent = getIntent();
        int provisioningMode = PROVISIONING_MODE_FULLY_MANAGED_DEVICE;
        List<Integer> allowedProvisioningModes = intent.getIntegerArrayListExtra(EXTRA_PROVISIONING_ALLOWED_PROVISIONING_MODES);

        if (allowedProvisioningModes != null) {
            if (allowedProvisioningModes.contains(PROVISIONING_MODE_FULLY_MANAGED_DEVICE)) {
                provisioningMode = PROVISIONING_MODE_FULLY_MANAGED_DEVICE;
            } else if (allowedProvisioningModes.contains(PROVISIONING_MODE_MANAGED_PROFILE)) {
                provisioningMode = PROVISIONING_MODE_MANAGED_PROFILE;
            }
        }
        //grab the extras (might contain some needed values from QR code) and pass to AdminPolicyComplianceActivity
        PersistableBundle extras = intent.getParcelableExtra(EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE);
        Intent resultIntent = getIntent();

        if (extras != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                resultIntent.putExtra(EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE, extras);
            }
        }
        resultIntent.putExtra(EXTRA_PROVISIONING_MODE, provisioningMode);

        setResult(RESULT_OK, resultIntent);
        finish();
    }
}

MyDeviceAdminReceiver.java

import android.app.admin.DeviceAdminReceiver;
import android.content.Context;
import android.content.Intent;

public class MyDeviceAdminReceiver extends DeviceAdminReceiver {
    @Override
    public void onEnabled(Context context, Intent intent) {
        super.onEnabled(context, intent);
    }

    @Override
    public void onDisabled(Context context, Intent intent) {
        super.onDisabled(context, intent);
    }
}

xml/device_admin_receiver.xml

<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-policies>
        <limit-password />
        <watch-login />
        <reset-password />
        <force-lock />
        <wipe-data />
    </uses-policies>
</device-admin>

layout/activity_get_provisioning_mode.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ProvisioningModeActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Policy Compliance Screen"/>

</LinearLayout>

layout/activity_policy_compliance.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".AdminPolicyComplianceActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Policy Compliance Screen"/>

</LinearLayout>

values/bool.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="provision_mode_compliance_enabled">false</bool>
</resources>

values-v31/bool.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="provision_mode_compliance_enabled">true</bool>
</resources>

Solution

  • Update

    I actually managed to figure it out for myself, and it was actually something extremely hard to guess from the things I've shared. For that, I apologize. My setup was actually a fully working solution. The real issue that caused the QR provisioning to fail was actually a problem with the download link requiring an http auth. Although this was a pretty special case, I feel the need to let everyone know: Do NOT use links requiring HTTP authentication ("https://username:password@your-domain.com" format). Even if they work on each of your devices, they will not work on a QR provisioning setup

    Since my link seemed to work on many browsers and all my devices, i was sure it would've actually worked on my target device as well. Seems like it was not the case.

    For anyone else struggling with this, here's an actually working setup to make a QR code for Android Provisioning:

    
    {
        "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME": "com.mycompany.myapp/.MyDeviceAdminReceiver",
        "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION": "https://path/to/your/package.apk",
        "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM": "sha256-checksum",
        "android.app.extra.PROVISIONING_SKIP_ENCRYPTION": true,
        "android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED": true
    }
    

    This takes the admin receiver class. Something like this was enough for me:

    AndroidManifext.xml

    <receiver
                android:name="com.mycompany.myapp.MyDeviceAdminReceiver"
                android:exported="true"
                android:permission="android.permission.BIND_DEVICE_ADMIN">
                <meta-data
                    android:name="android.app.device_admin"
                    android:resource="@xml/device_admin_receiver" />
                <intent-filter>
                    <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
                </intent-filter>
            </receiver>
    

    MyDeviceAdminReceiver.java

    import android.app.admin.DeviceAdminReceiver;
    import android.content.Context;
    import android.content.Intent;
    import android.util.Log;
    
    public class MyDeviceAdminReceiver extends DeviceAdminReceiver {
        private static final String TAG = "DeviceAdminReceiver";
    
        @Override
        public void onEnabled(Context context, Intent intent) {
            super.onEnabled(context, intent);
            Log.d(TAG, "Device Admin Enabled");
        }
    
        @Override
        public void onProfileProvisioningComplete(Context context, Intent intent) {
            super.onProfileProvisioningComplete(context, intent);
            Log.d(TAG, "Profile Provisioning Complete");
        }
    }
    

    xml/device_admin_receiver.xml

    <?xml version="1.0" encoding="utf-8"?>
    <device-admin xmlns:android="http://schemas.android.com/apk/res/android">
        <uses-policies>
            <limit-password />
            <watch-login />
            <reset-password />
            <force-lock />
            <wipe-data />
        </uses-policies>
    </device-admin>
    
    @echo off
    powershell -NoExit -Command ^
      "$hash = (Get-FileHash -Path 'C:/path/to/your/app-release.apk' -Algorithm SHA256).Hash; " ^
      "$hexBytes = $hash -replace '..', '0x$0 '; " ^
      "$bytes = $hexBytes.Trim().Split(' ') | ForEach-Object { [Convert]::ToByte($_, 16) }; " ^
      "$base64 = [Convert]::ToBase64String($bytes); " ^
      "$base64 = $base64.Replace('+', '-').Replace('/', '_').Replace('=', ''); " ^
      "Write-Output ($base64); "
    

    Copy everything that was printed and you can paste it in your PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM field of the JSON payload.

    At this point you should be good to go. You can generate the QR code image in several ways. I did it using this little Python script:

    import json
    import qrcode
    
    with open("provisioning.json", "r") as f:
        data = json.load(f)
    
    json_str = json.dumps(data, separators=(',', ':'))
    
    img = qrcode.make(json_str)
    
    qr_image = "provisioning_qr.png"
    
    img.save(qr_image)
    
    print("QR code saved as " + qr_image)