""

Extracting battery information from external peripherals with OEMInfo

Daniel Neamtu -
6 MIN READ
247
2

Overview

 

If you’re not familiar with OEMInfo, this is a service that has been introduced by Zebra a couple of years ago when Google decided with Android 10 to limit how applications can obtain sensitive information from a device, such as the serial number, the MAC Address and many others.

It provides a secure mechanism for applications to use this kind of information through a unified content provider framework, where such information can be obtained via different APIs.

OEMInfo not only provides access to sensitive information as mentioned in the previous examples, but it can also be used to get data such as battery details from external peripherals connected to a Zebra device via Bluetooth, like DS and RS scanners, or even the HS3100 Bluetooth headsets.

Since these APIs are protected, consuming data through OEMInfo requires the application to first obtain authorization from the ZDM (Zebra Device Manager) which is a one-time operation needed when the application attempts to use any of the available APIs.

At the moment OEM Info is bundled with the OS on all of our devices starting with Android 10 onwards but It can also be installed through a LG (Lifeguard Patch) on devices still running Android 8.

 

Setting up OEM Info in an Android Project

Setting up OEM Info in a project is very easy, we need to provide at first in the Android Manifest a specific permission:

 

<uses-permission android:name=”com.zebra.provider.READ”>

 

Then, still in the Android Manifest provide also package visibility to the Zebra Content Provider which will later be used to get the data (This from Android 11 onwards):

 

<queries>
    <package android:name="com.zebra.zebracontentprovider" />
</queries>

 

Authorising the app to acquire the battery information

As we mentioned, our app needs to be authorised before using any of the available APIs and this is required for any customer-developed apps or services, meaning any app not developed by Zebra.

For this, there are 2 viable ways on how we can do this:

  • Via StageNow
  • Via EMM with an XML which will be provisioned on the device

If you've never done this before, make sure you get yourself familiar at first with the AccessMgr CSP and how to deploy a profile via StageNow by setting up these params:

  • Operation Mode: "Single User without Whitelist"
  • Service Access Action: "Allow Caller to Call Service"
  • Service Identifier: "Delegation scope of the API category"
  • Specify Caller Package Name: "Enter app package name, e.g.: com.company.appname"
  • Caller Signature: "Select signature file that contains the app certificate"

 

The Caller Signature refers to the signature file (for example, Keystore) used to sign your application. This needs to be exported in either BASE64 or CERT format. If you haven't done this yet, we have a tool to assist you. You can follow this guide on how to use it.

If you plan to use an EMM to grant the app on multiple devices using an XML, the profile will look like this:

 

<!-- Note. CallerSignature should be changed with the CERT of the signature which you intend to use to compile your application -->
<wap-provisioningdoc>
  <characteristic type="ProfileInfo">
      <parm name="created_wizard_version" value="11.0.1" />
  </characteristic>
  <characteristic type="Profile">
      <parm name="ProfileName" value="Name of your profile" />
      <parm name="ModifiedDate" value="2022-08-17 10:20:36" />
      <parm name="TargetSystemVersion" value="10.4" />
      <characteristic type="AccessMgr" version="10.4">
          <parm name="emdk_name" value="" />
          <parm name="ServiceAccessAction" value="4" />
          <parm name="ServiceIdentifier" value="content://com.symbol.devicecentral.provider/peripheral_info/battery_info" />
          <parm name="CallerPackageName" value="com.your.package" />
          <parm name="CallerSignature" value="" />
      </characteristic>
  </characteristic>
</wap-provisioningdoc>

 

The Service Identifier (URI) in this case is:

content://com.symbol.devicecentral.provider/peripheral_info/battery_info

And It can be broken down like this:

  • content:// is the scheme, which tells Android that the URI points to a content provider.
  • com.symbol.devicecentral.provider is the authority name of the content provider.
  • peripheral_info is the content provider name unique within a given authority, usually a package name.
  • battery_info is the API name unique within a given package name.

     

Acquiring the battery information

Now, if you’ve done everything correctly, your app should have access to acquire the available battery info that can be obtained through OEM Info which are the following ones:

  • accessory_name
  • battery_level
  • battery_health_status
  • battery_serial_number
  • battery_model_number

 

Note, before acquiring any of this information, make sure that:

  • Device Central is installed on the Zebra device, otherwise you can download it for free from our support site here
  • The external peripheral connected to the device you're trying to get battery info from is one of the supported ones:
    • DS2278
    • DS3678
    • DS8178
    • LI3678
    • RS507
    • RS5100
    • RS6000
    • RS6100
    • HS3100
  • For RS-series scanners, the Zebra device:
    • Running Android 13 must have the Jul. 2023 LifeGuard update (or later)
    • Running Android 11 must have the Oct. 2023 LifeGuard update (or later)

Reading data through OEM Info implies to set up a Content Provider inside the code and use a Cursor object to navigate through the available info which we’ll get from the URI.

Since the data will come under the form of a JSON Object with everything inside, we cannot select individually which property to get so, we’ll have to pick it manually once we parse the stringified JSON:

 

fun queryIdentifierInfo() {
    viewModelScope.launch(Dispatchers.IO) {
        val results = arrayListOf<Identifier>()
        application.applicationContext.contentResolver.query(
          Uri.parse(BATTERY_IDENTIFIERS_URI),
          null,
          null,
          null
        )?.use { cursor ->
          if (cursor.columnCount == 0) {
              Log.e(TAG, "App is not having permission for this API")
              results.add(
                  Identifier().also {
                      it.value = null
                  }
              )
              processedIdentifiers.postValue(results)
          }
          while (cursor.moveToNext()) {
              // Battery Info
              Log.i(TAG, "Extracting Battery Info related identifiers")
              val columnIndexDisplayName = cursor.getColumnIndex(BATTERY_IDENTIFIERS_COLUMN)
              val mainBatteryObj = JSONObject(cursor.getString(columnIndexDisplayName))
              if (mainBatteryObj.length() == 0) {
                  Log.e( TAG, "Device Central is not installed on the device or no external device is connected")
                  results.add(
                      Identifier().also {
                          it.value = null
                      }
                  )
                  processedIdentifiers.postValue(results)
              }
              val batteryObj =
                  mainBatteryObj.getJSONObject(mainBatteryObj.names()?.get(0).toString())
              batteryObj.keys().forEach { key ->
                  	val formattedName = key.split("_").joinToString(" ") { word ->
                    	word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
              		}
              		results.add(
                  		Identifier(
                      		name = formattedName,
                      		value = batteryObj.getString(key)
                  		)
              		)
          	  }
              processedIdentifiers.postValue(results)
          }
        }
    }
}
companion object {
    const val BATTERY_IDENTIFIERS_URI =
        "content://com.symbol.devicecentral.provider/peripheral_info/battery_info"
    const val BATTERY_IDENTIFIERS_COLUMN =
        "battery_info"
}

 

Considering that we should keep the main thread “lightweight”, the entire logic around the content provider with the cursor has been moved to a ViewModel and the Activity will receive the results through a registered Observer when everything is finished.

So, let’s explain what’s happening in the snippet of code:

  • Assuming we have an object called Identifier, we use it to store the property's name and value. This allows us to later return the array to the activity and display the information to the user.
  • We check if the cursor has no available columns, and if that's the case, it likely means the app doesn't have the right authorisation to query this URI, so you'll need to revisit the previous steps to identify what went wrong.
  • If there is an available column, which in this case will be only one, we'll proceed to parse the stringified JSON with all the available info.
    • In this case, we're also adding some extra checks to ensure that if no external peripheral is connected to the device or the Device Central app is not installed, our application won't crash, so we check if the length of the JSON is not zero.
  • Considering that the main object is a JSONArray, which will include the actual JSONObject from which we’ll get our data, we don’t know exactly what this object will be named:

     

{
    "BLUETOOTH_RING_SCANNER":{
        "accessory_name":"RS6000",
        "battery_level":"",
        "battery_serial_number":"",
        "battery_model_number":"",
        "battery_health_status":""
    }
}

 

So, to avoid crashes or map each one of these names in the code, we'll directly get the first object of the JSONArray:

 

val batteryObj = 
    mainBatteryObj.getJSONObject(mainBatteryObj.names()?.get(0).toString())

 

The last part of the code could also look like this: instead of using a forEach, we manually go through each of these parameters and add them to the array. Alternatively, you could use this method to find the property you're looking for and then return it to the activity:

 

//Accessory Name
results.add(
    Identifier(
        name = "Accessory Name",
        value = batteryObj.getString("accessory_name")
    )
)
//Battery Level
results.add(
    Identifier(
        name = "Battery Level",
        value = batteryObj.getString("battery_level")
    )
)
//Battery Health Status
results.add(
    Identifier(
        name = "Battery Health Status",
        value = batteryObj.getString("battery_health_status")
    )
)
//Battery Serial Number
results.add(
    Identifier(
        name = "Battery Serial Number",
        value = batteryObj.getString("battery_serial_number")
    )
)
//Battery Model Number
results.add(
    Identifier(
        name = "Battery Model Number",
        value = batteryObj.getString("battery_model_number")
    )
)
processedIdentifiers.postValue(results)

 

In any case, regardless of the specific method or approach you intend to use, the final result should be something similar like this:

 

ACCESSORY_NAME=RS5100
BATTERY_LEVEL=95
BATTERY_HEALTH_STATUS=Good
BATTERY_SERIAL_NUMBER=
BATTERY_MODEL_NUMBER=BT-000397-10 Rev E

 

Happy coding!

profile

Daniel Neamtu

Please Register or Login to post a reply

2 Replies

E Elena Gilbert

Is there a way to check for compatibility with other Zebra peripheral models beyond the ones explicitly listed in the article?

D Daniel Neamtu

Hi Elena, at the moment it works only with the listed models.