Skip to content

Commit 30dbf7f

Browse files
oschwaldclaude
andcommitted
feat: Add codec enumeration and system features collection
Collect media codec support via MediaCodecList: - Audio codecs (name, supported types, encoder/decoder) - Video codecs (name, supported types, encoder/decoder) Collect system features via PackageManager: - Hardware features (camera, bluetooth, wifi, etc.) - Software features (live wallpaper, etc.) Both provide device fingerprinting signals. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 03f6ef6 commit 30dbf7f

File tree

5 files changed

+223
-0
lines changed

5 files changed

+223
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.maxmind.device.collector
2+
3+
import android.media.MediaCodecInfo
4+
import android.media.MediaCodecList
5+
import com.maxmind.device.model.CodecDetail
6+
import com.maxmind.device.model.CodecInfo
7+
8+
/**
9+
* Collects information about available media codecs.
10+
*
11+
* Enumerates all audio and video codecs available on the device
12+
* using MediaCodecList.
13+
*/
14+
internal class CodecCollector {
15+
/**
16+
* Collects information about all available codecs.
17+
*
18+
* @return [CodecInfo] containing audio and video codec lists
19+
*/
20+
fun collect(): CodecInfo {
21+
return try {
22+
val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)
23+
val codecInfos = codecList.codecInfos
24+
25+
val audioCodecs = mutableListOf<CodecDetail>()
26+
val videoCodecs = mutableListOf<CodecDetail>()
27+
28+
for (codecInfo in codecInfos) {
29+
val detail =
30+
CodecDetail(
31+
name = codecInfo.name,
32+
supportedTypes = codecInfo.supportedTypes.toList(),
33+
isEncoder = codecInfo.isEncoder,
34+
)
35+
36+
if (isAudioCodec(codecInfo)) {
37+
audioCodecs.add(detail)
38+
} else if (isVideoCodec(codecInfo)) {
39+
videoCodecs.add(detail)
40+
}
41+
}
42+
43+
CodecInfo(
44+
audio = audioCodecs,
45+
video = videoCodecs,
46+
)
47+
} catch (
48+
@Suppress("TooGenericExceptionCaught", "SwallowedException")
49+
e: Exception,
50+
) {
51+
// MediaCodecList may fail on some devices
52+
CodecInfo()
53+
}
54+
}
55+
56+
private fun isAudioCodec(codecInfo: MediaCodecInfo): Boolean {
57+
return codecInfo.supportedTypes.any { it.startsWith("audio/") }
58+
}
59+
60+
private fun isVideoCodec(codecInfo: MediaCodecInfo): Boolean {
61+
return codecInfo.supportedTypes.any { it.startsWith("video/") }
62+
}
63+
}

device-sdk/src/main/java/com/maxmind/device/collector/DeviceDataCollector.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ internal class DeviceDataCollector(private val context: Context) {
3030
private val audioCollector = AudioCollector(context)
3131
private val sensorCollector = SensorCollector(context)
3232
private val cameraCollector = CameraCollector(context)
33+
private val codecCollector = CodecCollector()
34+
private val systemFeaturesCollector = SystemFeaturesCollector(context)
3335

3436
/**
3537
* Collects current device data.
@@ -46,6 +48,8 @@ internal class DeviceDataCollector(private val context: Context) {
4648
audio = audioCollector.collect(),
4749
sensors = sensorCollector.collect(),
4850
cameras = cameraCollector.collect(),
51+
codecs = codecCollector.collect(),
52+
systemFeatures = systemFeaturesCollector.collect(),
4953
installation = collectInstallationInfo(),
5054
locale = collectLocaleInfo(),
5155
// Timezone offset in minutes
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.maxmind.device.collector
2+
3+
import android.content.Context
4+
5+
/**
6+
* Collects system feature declarations from PackageManager.
7+
*
8+
* Retrieves the list of hardware and software features declared
9+
* by the device manufacturer.
10+
*/
11+
internal class SystemFeaturesCollector(
12+
private val context: Context,
13+
) {
14+
/**
15+
* Collects the list of system features.
16+
*
17+
* @return List of feature names available on the device
18+
*/
19+
fun collect(): List<String> =
20+
try {
21+
context.packageManager.systemAvailableFeatures
22+
.mapNotNull { it.name }
23+
.sorted()
24+
} catch (
25+
@Suppress("TooGenericExceptionCaught", "SwallowedException")
26+
e: Exception,
27+
) {
28+
// PackageManager may throw on some devices
29+
emptyList()
30+
}
31+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.maxmind.device.collector
2+
3+
import org.junit.jupiter.api.Assertions.assertNotNull
4+
import org.junit.jupiter.api.Test
5+
6+
/**
7+
* Tests for CodecCollector.
8+
*
9+
* Note: MediaCodecList requires Android runtime and will not work in unit tests.
10+
* These tests verify graceful degradation. Full codec enumeration is tested
11+
* via instrumented tests on real devices.
12+
*/
13+
internal class CodecCollectorTest {
14+
@Test
15+
internal fun `collect returns CodecInfo when MediaCodecList unavailable`() {
16+
// In unit tests, MediaCodecList is not available
17+
// The collector should gracefully return empty codec info
18+
val collector = CodecCollector()
19+
val result = collector.collect()
20+
21+
assertNotNull(result)
22+
// Without Android runtime, we get empty lists but audio/video should be non-null
23+
assertNotNull(result.audio)
24+
assertNotNull(result.video)
25+
}
26+
27+
@Test
28+
internal fun `collect returns non-null CodecInfo object`() {
29+
val collector = CodecCollector()
30+
val result = collector.collect()
31+
32+
assertNotNull(result)
33+
assertNotNull(result.audio)
34+
assertNotNull(result.video)
35+
}
36+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.maxmind.device.collector
2+
3+
import android.content.Context
4+
import android.content.pm.FeatureInfo
5+
import android.content.pm.PackageManager
6+
import io.mockk.every
7+
import io.mockk.mockk
8+
import org.junit.jupiter.api.Assertions.assertEquals
9+
import org.junit.jupiter.api.Assertions.assertNotNull
10+
import org.junit.jupiter.api.Assertions.assertTrue
11+
import org.junit.jupiter.api.BeforeEach
12+
import org.junit.jupiter.api.Test
13+
14+
internal class SystemFeaturesCollectorTest {
15+
private lateinit var mockContext: Context
16+
private lateinit var mockPackageManager: PackageManager
17+
private lateinit var collector: SystemFeaturesCollector
18+
19+
@BeforeEach
20+
internal fun setUp() {
21+
mockContext = mockk(relaxed = true)
22+
mockPackageManager = mockk(relaxed = true)
23+
every { mockContext.packageManager } returns mockPackageManager
24+
collector = SystemFeaturesCollector(mockContext)
25+
}
26+
27+
@Test
28+
internal fun `collect returns sorted list of feature names`() {
29+
val features =
30+
arrayOf(
31+
createFeatureInfo("android.hardware.wifi"),
32+
createFeatureInfo("android.hardware.bluetooth"),
33+
createFeatureInfo("android.hardware.camera"),
34+
)
35+
every { mockPackageManager.systemAvailableFeatures } returns features
36+
37+
val result = collector.collect()
38+
39+
assertNotNull(result)
40+
assertEquals(3, result.size)
41+
// Should be sorted alphabetically
42+
assertEquals("android.hardware.bluetooth", result[0])
43+
assertEquals("android.hardware.camera", result[1])
44+
assertEquals("android.hardware.wifi", result[2])
45+
}
46+
47+
@Test
48+
internal fun `collect filters out null feature names`() {
49+
val features =
50+
arrayOf(
51+
createFeatureInfo("android.hardware.wifi"),
52+
createFeatureInfo(null),
53+
createFeatureInfo("android.hardware.camera"),
54+
)
55+
every { mockPackageManager.systemAvailableFeatures } returns features
56+
57+
val result = collector.collect()
58+
59+
assertNotNull(result)
60+
assertEquals(2, result.size)
61+
assertTrue(result.contains("android.hardware.wifi"))
62+
assertTrue(result.contains("android.hardware.camera"))
63+
}
64+
65+
@Test
66+
internal fun `collect returns empty list when no features available`() {
67+
every { mockPackageManager.systemAvailableFeatures } returns arrayOf()
68+
69+
val result = collector.collect()
70+
71+
assertNotNull(result)
72+
assertTrue(result.isEmpty())
73+
}
74+
75+
@Test
76+
internal fun `collect handles exception gracefully`() {
77+
every { mockPackageManager.systemAvailableFeatures } throws RuntimeException("Test error")
78+
79+
val result = collector.collect()
80+
81+
assertNotNull(result)
82+
assertTrue(result.isEmpty())
83+
}
84+
85+
private fun createFeatureInfo(name: String?): FeatureInfo =
86+
FeatureInfo().apply {
87+
this.name = name
88+
}
89+
}

0 commit comments

Comments
 (0)