Orchestration SDKs

Collecting device profiles in Android

PingOne Advanced Identity Cloud PingAM Android

The Device Profile module helps you to collect various attributes from an Android device.

It includes preconfigured collectors to collect attributes, and allows you to create your own collectors to suit your requirements.

Step 1. Installing modules

For obtaining a device profile, you need this module:

  • device-profile

To install the module into your Android app:

  1. In the Project tree view of your Android Studio project, open the build.gradle.kts file.

  2. In the dependencies section, add the device-profile module as a dependency:

    dependencies {
        implementation("com.pingidentity.device:device-profile:2.0.0")
    }

Step 2. Declaring permissions

The Device Profile module respects the Android permissions model, and certain collectors require that you declare the permissions needed.

To declare the permissions the app might request from the user:

  1. Open the project’s manifest file.

    For example, app > manifests > AndroidManifest.xml.

  2. Add the relevant properties as a child of the <manifest> element:

    1. For NetworkCollector, add the ACCESS_NETWORK_STATE permission:

        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    2. For LocationCollector, optionally add the ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions:

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

      The LocationCollector automatically requests the relevant permission when gathering the data if needed, so adding the permissions to the manifest is optional.

Step 3. Collecting device profiles

To generate a device profile, create a list of the collectors you want to use, or specify DefaultDeviceCollector to use the default list, and then call the collect() method to create the device profile JSON:

Obtaining a device profile on an Android device
import com.pingidentity.device.profile.collector.DefaultDeviceCollector
import com.pingidentity.device.profile.collector.collect

suspend fun collectDeviceProfile() {

    // Specify which collectors to use
    val collectors = mutableListOf<DeviceCollector<*>>().apply {
        clear()
        add(PlatformCollector())
        add(HardwareCollector())
        add(NetworkCollector())
        add(TelephonyCollector())
        add(LocationCollector()) // Handles permissions automatically
    }

    // Or use the default list of collectors
    val collectors = mutableListOf<DeviceCollector<*>>().apply(DefaultDeviceCollector())

    // Collect device information
    val deviceProfile = collectors.collect()

    // Use the collected profile (JsonObject)
    println(deviceProfile.toString())
}

The device-profile module provides the following collectors:

Included device profile collectors
Collector Attributes collected

PlatformCollector

Gathers platform and device identification information, such as the brand and model.

Example data returned by the PlatformCollector
{
  "platform": {
    platform = "android",
    version = 31,
    device = "pixel6",
    deviceName = "Pixel 6",
    model = "Pixel 6",
    brand = "google",
    locale = "en_US",
    timeZone = "America/New_York",
    jailBreakScore = 0.0
  }
}

HardwareCollector

Gathers information about the hardware, such as the CPU and display.

Example data returned by the HardwareCollector
{
  "hardware": {
    "hardware": "flame",
    "manufacturer": "Google",
    "storage": 64,
    "memory": 6144,
    "cpu": 8,
    "display": {
      "width": 1080,
      "height": 2280,
      "orientation": 0
    },
    "camera": {
      "numberOfCameras": 2
    }
  }
}

NetworkCollector

Whether the device has any network connectivity.

Example data returned by the NetworkCollector
{
   "network": {
      "connected": true
   }
}

TelephonyCollector

Collects information about the carriers the device uses, such as the carrier name and the country code.

Example data returned by the TelephonyCollector
{
   "telephony": {
      "networkCountryIso": "US",
      "carrierName": "Verizon"
   }
}

LocationCollector

Collects latitude and longitude coordinates from the device.

  • Automatically requests permissions when needed

  • Uses a transparent activity for seamless permission flow

  • Gracefully handles permission denials

  • Includes a 30-second protection against timeouts

  • Returns null if a location is unavailable or denied

Example data returned by the LocationCollector
{
   "location": {
      "latitude": 37.2431,
      "longitude": 115.7930
   }
}

BluetoothCollector

Whether the device has any bluetooth support.

Example data returned by the BluetoothCollector
{
    "bluetooth": {
        "supported": true
    }
}

BrowserCollector

Collects the user-agent string of the default browser on the device.

Example data returned by the BrowserCollector
{
    "browser": {
        "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36"
    }
}

Adding collectors conditionally

You can add collectors by checking that the user has granted the necessary permissions:

Checking permissions before adding collectors
fun createCollectors(context: Context): List<DeviceCollector<*>> {
    return mutableListOf<DeviceCollector<*>>().apply {
        // Always include basic collectors
        add(PlatformCollector())
        add(HardwareCollector())

        // Conditionally add collectors based on permissions
        if (ContextCompat.checkSelfPermission(context, ACCESS_NETWORK_STATE) == PERMISSION_GRANTED) {
            add(NetworkCollector())
        }

        // LocationCollector handles its own permissions
        add(LocationCollector())

        // Add based on device capabilities
        if (packageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
            add(TelephonyCollector)
        }
    }
}

Creating custom collectors

You can create custom device collectors to gather specific attributes depending on your requirements, or the hardware you will support.

Use the DeviceCollector class to create your custom collector, using one of the following patterns:

  • Factory function

  • Data class

  • Implement the interface

Using a factory function to create a custom collector
val BatteryCollector = DeviceCollector<Map<String, String>>("battery") {
    mapOf(
        "level" to "100",
        "isCharging" to "true",
    )
}
Using a data class to create a custom collector
@Serializable // Ensure the data class is serializable
data class BatteryData(val level: Int, val isCharging: Boolean, val capacity: Int)

val BatteryCollector = DeviceCollector<BatteryData>("battery") {
    BatteryData(
        level = 100,
        isCharging = true,
        capacity = 4000,
    )
}
Implementing the interface to create a custom collector
class SecurityCollector : DeviceCollector<SecurityInfo> {
    override val key = "security"
    override val serializer = SecurityInfo.serializer()

    override suspend fun collect(): SecurityInfo {
        return SecurityInfo(
            isDeviceSecure = checkDeviceSecuritySettings(),
            hasScreenLock = checkScreenLockStatus(),
            biometricsAvailable = checkBiometricCapabilities()
        )
    }
}

@Serializable
data class SecurityInfo(
    val isDeviceSecure: Boolean,
    val hasScreenLock: Boolean,
    val biometricsAvailable: Boolean
)

Integrating with Advanced Identity Cloud and PingAM journeys

You can use the Device Profile module to collect the data the Device Profile Collector node requires when used as part of a Advanced Identity Cloud or PingAM auth journey.

The Device Profile module provides the DeviceProfileCallback class. The class includes a collect() method, which collects the data and formats it ready for return to the server.

Preparing a device profile for the Device Profile Collector node
import com.pingidentity.device.profile.DeviceProfileCallback
import kotlinx.coroutines.runBlocking

// Collect with default settings - use DefaultDeviceCollector
val result = deviceProfileCallback.collect {
    collectors.apply(DefaultDeviceCollector())
}

result.onSuccess { profile ->
    // Submit to AIC service
    node = node.next()
    // Note: Some collectors may return null values if data is unavailable
}.onFailure { e ->
    // Handle collection errors
    Log.e("DeviceProfile", "Failed to collect profile", e)
}

The module formats the resulting device profile for consumption by the device profile collector node, and includes a device identifier:

Example device profile for the Device Profile Collector node
{
  "identifier": "unique-device-id",
  "platform": {
    "platform": "android",
    "device": "flame",
    "deviceName": "Pixel 4"
  },
  "hardware": {
    "manufacturer": "Google",
    "storage": 64,
    "memory": 6144
  },
  "network": {
    "connected": true
  },
  "location": {
    "latitude": 37.2431,
    "longitude": 115.7930
  }
}

You can customize which collectors to use when integrating with the device profile collector node, and also the device identifier the profile includes.

The Device Profile module automatically uses the Device ID module by default to generate this identifier, but you can customize how it’s generated. Learn about customizing the device identifier in Customizing device identifiers on Android.

Customizing a device profile for the Device Profile Collector node
val profile = deviceProfileCallback.collect {
   // Set a custom device identifier if needed
   deviceIdentifier = object : DeviceIdentifier {
       override val id: suspend () -> String = { "your-custom-device-id" }
   }

   // Adds collectors in a metadata block
   collectors {
      // Clear default collectors
      clear()
      // Add specific collectors
      add(PlatformCollector())
      add(HardwareCollector())
      add(NetworkCollector())
      add(LocationCollector()) // Will automatically handle permissions
   }
}

// The profile is now ready for submission to AIC services

The deviceProfileCallback class automatically formats the device profile for use with the Device Profile Collector node.