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 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.
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.
Trying to use adb (after a fresh factory reset): Simply unavailable. USB debug is a developer feature and requires manual activation after the setup wizard, which would present the same issues from the first point.
Trying to use Google Cloud's projects with Android Management API policy: Possibly the most modular solution, but impossible for me. The policy is only able to download and set policies over apps available on play Store, and my app will not set foot on that platform. It is still not clear for me if play Store apps can actually be made 'private' or 'unlisted', which is necessary for me because I'm doing this project for a small industry, and normal play Store users should not be able to access it, but only the manually approved devices. Either way, if there actually is such a feature, the Android Management API integration probably requires a Google Play Developer account for Organisations (not a personal one), which requires a D-U-N-S number which my company does not have (Of course, correct me if i'm wrong and if i can actually simply use a personal account).
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>
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>
PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION: This takes the URL towards the android package you want to fetch and set as device owner. As I said, it must be a publicly available link, with no inline HTTP authentication.
PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM:
This takes the checksum of your package (not your .jks key). Make sure your package is properly signed with apksigner.bat (Usually located in C:/Users/User/Local/Android/Sdk/build-tools/<version>/apksigner.bat). Then, on Windows, you can use this batch script to generate a reliable checksum:
@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)