Years back, I always wondered how Android-based MDM solutions and device management worked. There wasn’t a lot of online info on enabling this at both the Android OS platform level and the application level for COTS devices. In this post, I wanted to discuss how to configure, enroll, provision, and use Android Device Manager to manage a fleet of large devices at scale.
At GoDaddy, we manage 300k+ Smart Payment Terminals using our in-house Device Management Solution. Here, I wanted to share some lessons from my experience and a few established open-source methods that Android OS offers to accomplish Device Management.
App and Content Control: Push educational apps and block social media or entertainment apps to ensure that the use of the tablet is for learning purposes.
Classroom Mode: The teacher can see the activities going on in the tablet, push content, and lock the device during exams or study hours.
Software Updates: Remotely push OS updates and patches to all devices continuously for security and functionality.
Remote Management: Allow IT administrators to diagnose problems, monitor usage, and address device malfunction from a distance.
For COTS devices, it’s essential to first register the Device Manager app package with the Google Android Enterprise Management portal. This will establish the Device Manager App we are trying to build as the MDM provider.
Create an enrollment token
POST https://androidmanagement.googleapis.com/v1/{parent=enterprises/*}/enrollmentTokens
Input:
userAccountIdentifier
: This field allows you to associate a specific user account with the enrolled device. If not specified, the API will generate a unique user account for each device.{
"applications": [
{
"packageName": "com.myawesome.devicemanager",
"installType": "REQUIRED_FOR_SETUP"
}
],
"setupActions": [
{
"title": {
"defaultMessage": "Setup"
},
"launchApp": {
"packageName": "com.myawesome.devicemanager"
}
}
],
"allowPersonalUsage": false
}
Output:
enrollmentToken
Object: Contains the enrollmentTokenId
and a QRCode
link, which is then used by the end user for device provisioning. This token is crucial for enrolling COTS devices under management without manual configuration.Provisioning can be done in multiple ways in this example; however, we will focus on achieving this via QR code, which is the most widely used option.
The other popular ways include,
Refer here to explore other options: https://developers.google.com/android/management/provision-device
Once an enrollment token is created, provisioning can be initiated using the provided QR code. On a factory-reset device or a new device, the user simply taps the screen six times in the same spot, triggering the device to prompt for a QR code. The QR code is then scanned to begin the provisioning process.
Provisioning with QR Code Example,
{
"android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME": "com.myawesome.devicemanager/.receiver.DeviceAdminReceiver",
"android.app.extra.PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM": "I5YvS0O5hXY46mb01BlRjq4oJJGs2kuUcHvVkAPEXlg",
"android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION": "https://play.google.com/managed/downloadManagingApp?identifier=setup",
"android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE": {
"com.google.android.apps.work.clouddpc.EXTRA_ENROLLMENT_TOKEN": "{enrollment-token}"
}
}
This QR code bundle contains all the information necessary to automatically enroll the device, install required apps, and apply policies such as those specified in the enrollment token.
To configure this at the Android OS level,
/data/system/device_owner_2.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<root>
<device-owner
package="com.myawesome.devicemanager"
name="Device Manager"
component="com.myawesome.devicemanager/.receiver.DeviceAdminReceiver"
userRestrictionsMigrated="true"
canAccessDeviceIds="true" />
<device-owner-context userId="0" />
</root>
/data/system/device_policies.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<policies setup-complete="true" provisioning-state="3">
<admin name="com.myawesome.devicemanager/.receiver.DeviceAdminReceiver">
<policies flags="17" />
<strong-auth-unlock-timeout value="0" />
<user-restrictions no_add_managed_profile="true" />
<default-enabled-user-restrictions>
<restriction value="no_add_managed_profile" />
</default-enabled-user-restrictions>
<cross-profile-calendar-packages />
</admin>
<lock-task-features value="16" />
</policies>
The first step is to setup a DeviceAdminReceiver that will be the component for the Device Owner which can be accessed via DevicePolicyManager. The receiver declaration in manifest will also contain the device policies xml in the meta-data.
<!-- In your AndroidManifest.xml -->
<receiver
android:name=".MyDeviceAdminReceiver"
android:permission="android.permission.BIND_DEVICE_ADMIN">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_policies" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
Here is the device_policies meta data,
<!-- res/xml/device_policies.xml -->
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
<uses-policies>
<force-lock />
<wipe-data />
<reset-password />
<reboot />
<grant-policies />
</uses-policies>
</device-admin>
We will explore the usage of Device Manager, which we configured for common device management tasks like rebooting, generating bug reports, granting permissions, defining policies, and factory reset/wiping on Android.
DevicePolicyManager API reference: https://developer.android.com/reference/android/app/admin/DevicePolicyManager
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
fun rebootDevice(context: Context, adminComponent: ComponentName) {
val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
devicePolicyManager.reboot(adminComponent)
}
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
fun requestBugReport(context: Context, adminComponent: ComponentName) {
val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
devicePolicyManager.requestBugreport(adminComponent)
}
Here’s how to implement the BugReportReceiver
to retrieve the bug report:
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
class BugReportReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Retrieve the file path of the generated bug report zip file
if (intent.action == DevicePolicyManager.ACTION_BUGREPORT_SHARE) {
val bugReportUri = intent.getData()
if (bugReportUri != null) {
Log.d("BugReportReceiver", "Bug report saved at: $bugReportUri")
// Process the bug report file as needed, e.g., upload to a server or save it locally
}
} else {
Log.e("BugReportReceiver", "Bug report generation failed")
}
}
}
Here’s the registration in the manifest,
<receiver android:name=".BugReportReceiver">
<intent-filter>
<action android:name="android.app.action.BUGREPORT_SHARE" />
</intent-filter>
</receiver>
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.os.UserHandle
fun grantPermissionToApp(context: Context, adminComponent: ComponentName, packageName: String, permission: String) {
val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
devicePolicyManager.setPermissionGrantState(adminComponent, packageName, permission, DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED)
}
For example, if you want to grant CAMERA
permission to an app:
grantPermissionToApp(context, adminComponent, "com.app.cameraapp", android.Manifest.permission.CAMERA)
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
fun wipeDeviceData(context: Context, adminComponent: ComponentName) {
val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
devicePolicyManager.wipeData(DevicePolicyManager.WIPE_EXTERNAL_STORAGE or DevicePolicyManager.WIPE_RESET_PROTECTION_DATA)
}
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
fun setCameraDisabled(context: Context, adminComponent: ComponentName, disabled: Boolean) {
val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
devicePolicyManager.setCameraDisabled(adminComponent, disabled)
}
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
fun enforcePasswordQuality(context: Context, adminComponent: ComponentName) {
val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
devicePolicyManager.setPasswordQuality(adminComponent, DevicePolicyManager.PASSWORD_QUALITY_NUMERIC)
}
Hiding an app prevents the user from launching or interacting with it, though the app remains installed on the device.
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
fun hideAppPackage(context: Context, adminComponent: ComponentName, packageName: String, hide: Boolean) {
val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val success = devicePolicyManager.setApplicationHidden(adminComponent, packageName, hide)
if (success) {
if (hide) {
println("Package $packageName has been hidden.")
} else {
println("Package $packageName has been unhidden.")
}
} else {
println("Failed to change the visibility of $packageName.")
}
}
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import java.io.File
fun installApk(context: Context, apkFile: File, adminComponent: ComponentName) {
val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
// Install the APK using the DevicePolicyManager
devicePolicyManager.installPackage(adminComponent, apkFile.toURI(), false)
}
Managing over 300,000 Android-based payment terminals has been a challenging yet enriching experience. Building the GoDaddy Commerce Terminal Management Solution from the ground up has taught me the importance of scalability, security, and streamlined operations. By leveraging the Android Device Policy Manager, we’ve been able to ensure that these terminals remain secure, updated, and capable of supporting our various payment services in compliance with PCI and EMV standards.
Throughout this journey, we’ve encountered and solved numerous device management challenges — provisioning devices at scale, enforcing security policies, configuring devices remotely, and much more. One key takeaway is that creating a robust Terminal Management Solution involves more than just deploying devices; it requires deep insights into the Android OS and application-level management, ensuring that these terminals can handle the rigors of real-world commerce.
I hope the tools and techniques outlined in this post will help others in similar roles, whether managing a few hundred or hundreds of thousands of devices. As Android continues to evolve, so too will device management capabilities, and I look forward to seeing what new features emerge to simplify and secure the management of large fleets of devices.
In the end, it’s all about making sure that every transaction, every tap, and every payment is seamless, secure, and reliable — no matter how large the scale.