Mobile Device Management System

In this post, we provide an overview of mobile device management technology including its typical architecture, tips for implementing components, and code examples for Android MDM systems. You will also learn how to make your own mobile device management system.

Written by: Mykhayl Tserr, Junior Developer in the Enterprise Solution Development Team.

Contents:

Technology Overview

MDM Architecture: Server Side

  Main Events Processing Loop

  SMS Gateway

  Administrator Console

MDM Architecture: Server Side

   Device Policy Controller

  Device Owner

Conclusion

Smartphones and tablets are no longer only devices for home and personal use. Many companies have realized the benefits of allowing their employees to use personal mobile devices for work, contributing to what is called the BYOD (Bring Your Own Device) trend. However, this approach also leads to a range of data security challenges. People usually associate the term “data breach” with desktop computers and laptops, but mobile devices are also vulnerable. That’s why mobile device management is necessary.

Technology Overview

Mobile device management (MDM) is a set of technologies that ensures security and control over mobile devices in the workplace, such as smartphones, tablets, and various terminals including point-of-sale. MDM systems are aimed at increasing data security for devices and monitoring their state.

An MDM system forms an additional layer of security and provides capabilities for monitoring user activities on mobile devices. An MDM system also can provide device-specific and platform-specific functions related to data encryption, access management, SD card encryption, and geolocation monitoring. Altogether, these functions form the MDM suite.

Most MDM solutions are based on a client-server model. A server sends management commands to mobile devices, which receive and execute these commands. Both client and server components can function independently, and you can develop them separately taking into account the component’s interface flexibility and data exchange protocol.

An MDM software platform should detect devices on a network and send them settings, commands, and configurations for immediate use. This process is fully automated to ensure continuous device usage. Saving the history of sent commands and monitoring device statuses allows an MDM system to send necessary settings to all devices, to a particular group of devices, or to one specific device. Devices can be identified by monitoring International Mobile Equipment Identity (IMEI) and International Mobile Subscriber Identity (IMSI) dyads. An MDM system uses the IMEI to identify a particular device, while the IMSI determines a SIM card that’s associated with a particular end user.

The Difference between Mobile Device Management and Enterprise Mobility Management
Let’s consider the difference between mobile device management and enterprise mobility management (EMM), as these terms are often used interchangeably. MDM is often considered part of EMM. The emergence of the term EMM is connected to the development of more complex solutions that provide mobile app management, mobile content management, and mobile email management in addition to mobile device management. Developers often use cloud-based solutions in EMM systems and thus do not implement data security and encryption locally on devices.

MDM systems are often used to support a Bring Your Own Device (BYOD) approach where employees use personal devices at work. The prevalence of tablets and smartphones in everyday life has led to companies allowing employees to bring their own devices in order to increase corporate productivity and decrease expenses. Previously, this idea was not successful because of numerous security problems. Now, however, the BYOD approach is widespread.

After covering the basic terms and ideas of MDM and EMM, we’ll discuss the design and implementation of an MDM system for Android-based devices.

MDM Architecture: Server Side

When you create a mobile device management system, you should keep in mind that it has a three-level architecture: handset software, middle-tier server, and backend server.

MDM architecture: server-side

Middle-tier and backend (e.g. Google Cloud Messaging or Firebase Cloud Messaging) servers communicate via HTTPS using an API that provides commands for device management tasks such as the following:

  • locking mechanism activation
  • locking mechanism deactivation
  • activation status checking
  • device locking
  • device unlocking
  • sending a push notification
  • asynchronous request/retrieval of information about a device’s lock status

Identification of a particular device in the database is based on its phone number, IMSI, or IMEI in case the device is associated with a specific SIM card.

Client devices communicate with the middle-tier server via SMS. A client-side certificate ensures the security of messages. The system stores a public key locally on each device in a standard encrypted container for the Android KeyChain API, while SMSs from the server are signed with a private key.

The phone number through which the server sends and the client device receives and sends messages is also stored on the device in a secure container. You can update this container via SMS with a command sent from a valid phone number. Handset software checks the validity of the phone number and verifies the SMS signature directly on the device.

In order to fully cover the principle of how the backend server works, we’ll consider how it interacts with its components.

MDM architecture server-side

Main Events Processing Loop

The main events processing loop (MEPL) receives requests and processes them by calling other components in order to create data, send it within the network, and update the database. Therefore, the MEPL is a link that manages other components.

The MEPL creates database requests for sent and received commands in the form of binary large objects (BLOBs). The system stores BLOBs in a database such as PostgreSQL. It’s important to store server-device command history in order to monitor possible errors and collect user activity statistics.

SMS Gateway

An SMS gateway allows the system to send and receive SMS messages without a without an active phone number. Furthermore, the gateway can transform SMS into email or HTTP requests and vice versa. A message sent via such a gateway is free for the sender. However, there can be some technical restrictions, such as a limit to the number of messages that can be sent from one computer per day. It’s worth noting that in our example system the gateway can work with the main server via HTTP or HTTPS. Once the MEPL receives an HTTP request from the gateway, it processes the message data, generates SMPP messages, and sends them back.

Administrator Console

The administrator console is an interface that allows an administrator to manage the system’s work. In our example, this console is in the form of a desktop application and its communicates with the server via a secure channel based on a proxy server. In the console, an admin can perform various actions including:

  • analyzing work done on corporate devices
  • monitoring the activity of corporate devices
  • manually sending commands to mobile devices
  • detecting device usage violations
  • blocking particular functionality

The system sends an admin’s commands to the MEPL, then to devices and the database via the SMS gateway.

The connection to Google Cloud Messaging (GCM) or other similar services as well as the other backend infrastructure that plays no significant role in device management relies on an internet connection.

MDM Architecture: Client Side

In our sample MDM system, an Android app executes server commands, regularly reports its status, protects corporate data, and limits device functionality up to full device locking. Security is the main priority. If complete device and data security isn’t possible, it’s necessary to at least ensure that hacking one device doesn’t make it easier to hack others.

Device Policy Controller

A device policy controller (DPC) is an Android app that includes all the functions mentioned above and manages access rights for system and client applications. A DPC needs to be a system app in order to get all necessary privileges from the operating system to manage other apps and their permissions. However, there’s an issue here: system apps have to be preinstalled on a mobile device – that is, they need to be baked into the system image (Android ROM) that’s installed on a device. For non-rooted devices (we won’t consider rooted devices, as our goal is to avoid that situation), the /system folder with system apps (/system/app and /system/priv-app) have read-only access. You can update system applications, but these updates won’t become part of the ROM, and reseting the system to its default settings will delete these apps.

To install system apps, you have to set a permission that is usually provided by a system for system apps only (pre-installed or signed with the same certificate as the ROM) in the AndroidManifest file:

 
<permission
...
android:protectionLevel = signatureOrSystem
...
/>

An application with such permission obviously cannot be uploaded to the Play Store because each ROM has its own signature.

Device Owner

A device owner is the first user created during initial device configuration that happens when a device is first set up.

Unprovisioned State

The unprovisioned state is a device’s state before the initial configuration.

Provisioned State

The provisioned state is a device’s state after being configured. For a corporate device with a preinstalled DPC app, the device owner will be the application itself. Therefore, the DPC app will get access to advanced device management capabilities, including:

  • hiding installed apps (not only icons in the app list, but also in the settings)
  • disabling the status bar
  • disabling notifications
  • disabling services such as Google Now and Google Assistant
  • preventing the display from switching off
  • silent installing and uninstalling apps
  • retrieving all necessary DPC runtime permissions
  • blocking debugging mode (when connecting through USB, adb doesn’t connect to the device)

Most of these features can be implemented with the Device Administration API. DeviceAdminReceiver is a basic class for implementing a device administration component. This class ensures the interpretation of raw intent actions sent by the system. Therefore, the class enhances BroadcastReceiver. You should declare DeviceAdminReceiver in the AndroidManifest file along with setting the following intent filters:

<receiver
 
    android:name=".DeviceAdminReceiver"
    android:description="@string/app_name"
    android:label="@string/app_name"
    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"/>
        <action android:name="android.app.action.PROFILE_PROVISIONING_COMPLETE"/>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>

The receiver must have the android.permission.BIND_DEVICE_ADMIN permission to guarantee that the system interacts only with this receiver and not with any other device administration component of another application. In the DeviceAdminReceiver component metadata, you should set an xml file that will contain specific policies for each particular administrative component.

<?xml version="1.0" encoding="utf-8"?>
 
<device-admin>
    <uses-policies>
        <limit-password/>
        <watch-login/>
        <reset-password/>
        <force-lock/>
        <wipe-data/>
        <expire-password/>
        <encrypted-storage/>
        <disable-camera/>
        <disable-keyguard-features/>
     </uses-policies>
</device-admin>

Use the DeviceAdminInfo class to set metadata taken from this xml file.

android.app.action.DEVICE_ADMIN_ENABLED is the main action that DeviceAdminReceiver has to process in order to manage a device. The Receiver will receive events for this intent if you have installed an app as a device admin or device owner. The DeviceAdminReceiver class should be inherited from android.app.admin.DeviceAdminReceiver. Let’s cover the key aspects of implementing DeviceAdminReceiver.

public void onProfileProvisioningComplete(Context context, Intent intent) {
 
        DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
        String packageName = context.getPackageName();
         
        boolean isDeviceOwner = devicePolicyManager.isDeviceOwnerApp(packageName);
        if (!isDeviceOwner) {
            Toast.makeText(context, R.string.admin_receiver_fail, Toast.LENGTH_LONG).show();
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            autoGrantRequestedPermissionsToSelf(context);
        }
        addUserRestriction(context);
}

The onProfileProvisioningComplete method is activated after the ACTION_PROFILE_PROVISIONING_COMPLETE action, whose filter is declared in the AndroidManifest file, indicating that the preparation of the managed device has been completed successfully. When an app sends a request to get administrator permissions, its profile limits the capability to intercept this intent. Calling isDeviceOwnerApp(String), you can check whether your app is the real device owner. You can make your app the device owner using adb in the terminal.

adb shell dpm set-device-owner
example.mobile.device.management/.DeviceAdminReceiver

You also can make your app the device owner using an installer app and near-field communication (NFC). With the NFC method, an installer app on another Android device will initiate the DPC app installation from the apk file via NFC and set that app as the device owner. In this case, the DPC app can’t be a system app.

The next step is obtaining runtime permissions for Android M and higher. You should check all permissions needed for the app to work and get runtime permissions (an app usually requests them from a user as components are used).

@TargetApi(Build.VERSION_CODES.M)
 public static void autoGrantRequestedPermissionsToSelf(Context context)  {
     DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
     String packageName = context.getPackageName();
     ComponentName adminComponentName = getComponentName(context);
     List<String> permissions = getRuntimePermissions(context.getPackageManager(), packageName); //retrieve a list of requested runtime permissions
     for (String permission : permissions) {
         boolean success =   devicePolicyManager.setPermissionGrantState(adminComponentName, packageName, permission, PERMISSION_GRANT_STATE_GRANTED);
         if (!success) {
              Log.e("Failed to grant permission: " + permission);
         }
     }
 }
 
 private static List<String> getRuntimePermissions(PackageManager packageManager, String packageName) {      
      List<String> permissions = new ArrayList<>();
      PackageInfo packageInfo;
      try {
          packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS); //get the list of all requested permissions
      } catch (PackageManager.NameNotFoundException e) {
          return permissions;
      }
 
 if (packageInfo != null && packageInfo.requestedPermissions != null) {
     for (String requestedPerm : packageInfo.requestedPermissions) {
         if (isRuntimePermission(packageManager, requestedPerm)) { //keep only runtime permissions
             permissions.add(requestedPerm);
         }
     }
  }
  return permissions;
}
 
 private static boolean isRuntimePermission(PackageManager packageManager, String permission) {
     try {
         PermissionInfo pInfo = packageManager.getPermissionInfo(permission, 0);
         if (pInfo != null) {
             if ((pInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE) == PermissionInfo.PROTECTION_DANGEROUS) {
                 return true;
             }
         }
      } catch (PackageManager.NameNotFoundException ignore) {
          Log.w(permission + " isn't runtime");
      }
      return false;
  }

The addUserRestriction and clearUserRestriction methods are responsible for enabling and disabling features such as installing apps from unknown sources (not from the Play Store), resetting to default settings, and debugging. The latter two features are useful in development, which is why you should disable them only for the release product.

public static void addUserRestriction(final Context context) {
    DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
    ComponentName admin = getComponentName(context);
    addUserRestriction(admin, devicePolicyManager);
 
    devicePolicyManager.addUserRestriction(admin, DISALLOW_INSTALL_UNKNOWN_SOURCES);
    if (!BuildConfig.DEBUG) {
        devicePolicyManager.addUserRestriction(admin, DISALLOW_FACTORY_RESET);
        devicePolicyManager.addUserRestriction(admin, DISALLOW_DEBUGGING_FEATURES);
    }
    devicePolicyManager.setAutoTimeRequired(admin, true);
}
 
public static void clearUserRestriction(final Context context) {
    DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
    ComponentName admin = getComponentName(context);
    devicePolicyManager.clearUserRestriction(admin, DISALLOW_INSTALL_UNKNOWN_SOURCES);
    devicePolicyManager.clearUserRestriction(admin, DISALLOW_FACTORY_RESET);
    devicePolicyManager.clearUserRestriction(admin, DISALLOW_DEBUGGING_FEATURES);
    devicePolicyManager.setAutoTimeRequired(admin, false);
}

Now that we’ve described how a DPC app gets the capability to manage a mobile device, let’s proceed to the actual device management.

As mentioned above in our MDM example, an administrator should be capable of blocking different device functions and preventing a device from being used for non-corporate purposes using a web-based interface. Furthermore, a device should regularly report its status. To accomplish this, we should use the LockTask API that’s available only for the device owner in Android M and higher.

The more precise task is limiting device capabilities to one single activity and making it impossible to do anything else. This means that both the back and home system buttons have to return users to this activity. To partially do this, we can make our app the default launcher. Therefore, in the lock mode the home screen will be the above-mentioned activity.

<activity
        android:name="example.mobile.device.management.MyLockActivity"
        android:label="@string/app_name"
        android:uiOptions="splitActionBarWhenNarrow"
        android:screenOrientation="portrait"
        android:enabled="false">
      <intent-filter>
	    <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.HOME"/>
      </intent-filter>
</activity>

An administrator can enable and disable this mode (at other times, the device works as usual) on each particular device, identifying the device via its IMEI/IMSI. The device will automatically lock in case an unauthorized action is attempted or under unauthorized conditions – such as inserting an invalid SIM card or removing a SIM card, the impossibility to perform provisioning, the device leaving monitored territory, or in the case of theft or a device crash. If any of these conditions is met, the device will inform the server about it as soon as it gets the chance.

Let’s consider activity methods that enable lock mode.

//check status of lock task
  private boolean isLocked(ActivityManager activityManager)
  {
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      int lockTaskMode = activityManager.getLockTaskModeState();
      return lockTaskMode != ActivityManager.LOCK_TASK_MODE_NONE ? true : false;
    }
    else if(Build.VERSION.SDK_INT >=Build.VERSION_CODES.LOLLIPOP &&
            Build.VERSION.SDK_INT< Build.VERSION_CODES.M) {
      return activityManager.isInLockTaskMode();
    }
    else {
      return false;
    }
  }
 
  //start/restart lock task
  private void restartLock(){
    final ActivityManager activityManager = (ActivityManager) getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
    if (!isLocked(activityManager) && isLocked.get()) {
      Handler lockHandler = new Handler(Looper.getMainLooper());
      lockHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
          try {
            activityManager.moveTaskToFront(getTaskId(), 0); //display lock task activity over any other apps
            startLockTask();
            isLocked.set(true);
          }catch (Exception exception) {
            Log.v("startLockTask - Invalid task, not in foreground");
          }
        }
      }, LOCK_TASK_DELAY);
    }
  }
 
 //reset lock task
  public void unlockTask() {
    startIntent = null;
    try {
      stopLockTask();
    } catch (Exception e) {
      Log.w("stopLockTask - Invalid task, not in foreground", e);
    }
    isLocked.set(false);
    finish();
  }

What should you do if your device must have access to some standard functions in lock mode? For example, if emergency calls should be still available, meaning that when the device is in lock mode the screen will have an emergency call button. Let’s implement this using a standard Android component called EmergencyDialer.

final Button EmergancyCallButton = (Button) findViewById(R.id.bEmergencyCallButton);
EmergancyCallButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    KeyguardManager kManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
    final KeyguardManager.KeyguardLock keyguardLock = kManager.newKeyguardLock("EmergencyDialer.KeyguardLock");
    keyguardLock.disableKeyguard(); //enabling the keyboard
 
    Intent callIntent = new Intent("com.android.phone.EmergencyDialer.DIAL");
    callIntent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NO_HISTORY);
 
    startActivityForResult(callIntent, EMERGENCY_CALL_ACTIVITY_REQUEST_CODE); //launch EmergencyDialer activity
 
    final ActivityManager activityManager = (ActivityManager) getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
    if (isLocked(activityManager)) {
      Handler lockHandler = new Handler(Looper.getMainLooper());
      lockHandler.postDelayed(new Runnable() {
        @Override public void run() {
          try {
            activityManager.moveTaskToFront(getTaskId(), 0);
            stopLockTask(); //stop lock task to have possibility make a call
            isEmergencyScreen.set(true);             
          } catch (Exception exception) {
            Log.v("stopLockTask - Invalid task, not in foreground", exception);
          }
        }
      }, LOCK_TASK_DELAY);
    }
  }
});
 
//restart lock task after emergency dialer activity was closed
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch(requestCode)
    {
        case EMERGENCY_CALL_ACTIVITY_REQUEST_CODE:
        if (resultCode == RESULT_CANCELED) {
            isEmergencyScreen.set(false);
            screenPinningHelper.setLockTaskPackages();
            restartLock();
        }
        break;
  
        /*...*/
 
    }
}  

You can’t perform an emergency call from the lock task. Therefore, the user will be able to disable lock mode by opening a notification bar or using the recent apps button. Tapping the home or back button, however, will restart the lock task because the activity is a launcher.

There are a couple of solutions to this. You can launch the EmergencyDialer component as a pinned activity. This will display system buttons but they won’t function (available on Android devices with Lollipop or higher). Or you can intercept system button taps. This method is more reliable than the first. To intercept button taps, let’s consider the BroadcastReceiver that will process the ACTION_CLOSE_SYSTEM_DIALOGS event.

public class SysButtonsWatcher {
    private Context mContext;
    private IntentFilter mFilter;
    private OnSysButtonListener mListener;
    private InnerRecevier mRecevier;
 
    public SysButtonsWatcher(Context context) {
        Log.entry();
        mContext = context;
        mFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
    }
 
    public void OnSysButtonPressedListener(OnSysButtonListener listener) {
        Log.entry();
        mListener = listener;
        mRecevier = new InnerRecevier();
    }
 
    public void startWatch() {
        Log.entry();
        if (mRecevier != null) {
            mContext.registerReceiver(mRecevier, mFilter);
        }
    }
 
    public void stopWatch() {
        Log.entry();
        if (mRecevier != null) {
            mContext.unregisterReceiver(mRecevier);
        }
    }
 
    class InnerRecevier extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.entry();
            String action = intent.getAction();
            if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
                String reason = intent.getStringExtra("reason");
                if (reason != null) {                 
                    if (mListener != null) {
                        if (reason.equals("homekey")) { //"home" button
                            mListener.onHomePressed();
                        } else if (reason.equals("recentapps")) { //"recent apps" button
                            mListener.onHomeLongPressed();
                        } else if (reason.equals("voiceinteraction")) { //long tap on "home" button
                            mListener.onVoiceInteraction();
                        }
                    }
                }
            }
        }
    }
}

Conclusion

When you build an MDM system, you should take the following key aspects into consideration:

  • An administrator should be able to manage each particular device by identifying it with its phone number, IMEI, and IMSI.
  • Communication with devices can be implemented with SMS via the SMS gateway API of a corresponding communications provider.
  • It’s important to ensure the connection between devices and a server in order to update information about the status of each device.
  • Android version M and higher provides a wide set of APIs, such as Device Administration and Lock Task, for flexible client software configuration (for a DPC app).

 

Subscribe to updates