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:
-
In the Project tree view of your Android Studio project, open the
build.gradle.ktsfile. -
In the
dependenciessection, add thedevice-profilemodule 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:
-
Open the project’s manifest file.
For example, app > manifests > AndroidManifest.xml.
-
Add the relevant properties as a child of the
<manifest>element:-
For
NetworkCollector, add theACCESS_NETWORK_STATEpermission:<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> -
For
LocationCollector, optionally add theACCESS_FINE_LOCATIONandACCESS_COARSE_LOCATIONpermissions:<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>The
LocationCollectorautomatically 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:
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:
| Collector | Attributes collected |
|---|---|
|
Gathers platform and device identification information, such as the brand and model. Example data returned by the
PlatformCollector
|
|
Gathers information about the hardware, such as the CPU and display. Example data returned by the
HardwareCollector
|
|
Whether the device has any network connectivity. Example data returned by the
NetworkCollector
|
|
Collects information about the carriers the device uses, such as the carrier name and the country code. Example data returned by the
TelephonyCollector
|
|
Collects latitude and longitude coordinates from the device.
Example data returned by the
LocationCollector
|
|
Whether the device has any bluetooth support. Example data returned by the
BluetoothCollector
|
|
Collects the user-agent string of the default browser on the device. Example data returned by the
BrowserCollector
|
Adding collectors conditionally
You can add collectors by checking that the user has granted the necessary permissions:
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
val BatteryCollector = DeviceCollector<Map<String, String>>("battery") {
mapOf(
"level" to "100",
"isCharging" to "true",
)
}
@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,
)
}
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.
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:
{
"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.
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.