Skip to content

Commit 9944986

Browse files
authored
prevent using jvm initializer in android and address violation of retrieving unsettable device id (#67)
* add check on jvm initializer * add unit tests * add note to analytics constructors * fix flasky test * update sovran dependencies and get rid jitpack * fix snapshot artifacts dependency issue * prepare snapshot 1.4.2 * add cancel previous to github action * update release token * address violation of retrieving unsettable device id * remove redundant code * add more info about drm api * get rid of android id and serial number * remove redundant code * fix test case * fix test case * add debug output * use junit assert throw pattern * try to fix flaky test * Revert "try to fix flaky test" This reverts commit de827c5 * try to fix flaky test again
1 parent 65d5419 commit 9944986

File tree

16 files changed

+173
-74
lines changed

16 files changed

+173
-74
lines changed

.github/workflows/build.yml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@ on:
88
workflow_dispatch:
99

1010
jobs:
11-
core-test:
11+
cancel_previous:
1212

1313
runs-on: ubuntu-latest
14+
steps:
15+
- uses: styfle/[email protected]
16+
with:
17+
workflow_id: ${{ github.event.workflow.id }}
18+
19+
core-test:
20+
needs: cancel_previous
21+
runs-on: ubuntu-latest
1422

1523
steps:
1624
- uses: actions/checkout@v2
@@ -33,7 +41,7 @@ jobs:
3341
uses: codecov/codecov-action@v2
3442

3543
android-test:
36-
44+
needs: cancel_previous
3745
runs-on: ubuntu-latest
3846

3947
steps:
@@ -57,7 +65,7 @@ jobs:
5765
uses: codecov/codecov-action@v2
5866

5967
destination-test:
60-
68+
needs: cancel_previous
6169
runs-on: ubuntu-latest
6270

6371
steps:
@@ -81,7 +89,7 @@ jobs:
8189
uses: codecov/codecov-action@v2
8290

8391
security:
84-
92+
needs: cancel_previous
8593
runs-on: ubuntu-latest
8694

8795
steps:

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ jobs:
4646
run: |
4747
curl \
4848
-X POST \
49-
-H "Authorization: token $GITHUB_TOKEN" \
49+
-H "Authorization: token $RELEASE_TOKEN" \
5050
https://api.github.com/repos/${{github.repository}}/releases \
5151
-d '{"tag_name": "${{ env.RELEASE_VERSION }}", "name": "${{ env.RELEASE_VERSION }}", "body": "Release of version ${{ env.RELEASE_VERSION }}", "draft": false, "prerelease": false, "generate_release_notes": true}'
5252
env:
53-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53+
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
5454
RELEASE_VERSION: ${{ steps.vars.outputs.tag }}

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ android {
5252
dependencies {
5353
// MAIN DEPS
5454
api project(':core')
55-
api 'com.github.segmentio:sovran-kotlin:1.2.0'
55+
api 'com.segment:sovran-kotlin:1.2.1'
5656
api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
5757
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
5858
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'

android/src/main/java/com/segment/analytics/kotlin/android/AndroidAnalytics.kt

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ import com.segment.analytics.kotlin.core.platform.plugins.logger.*
1111
// A set of functions tailored to the Android implementation of analytics
1212

1313
@Suppress("FunctionName")
14-
// constructor function to build android specific analytics in dsl format
15-
// Usage: Analytics("$writeKey", applicationContext, applicationScope)
14+
/**
15+
* constructor function to build android specific analytics in dsl format
16+
* Usage: Analytics("$writeKey", applicationContext, applicationScope)
17+
*
18+
* NOTE: this method should only be used for Android application. Context is required.
19+
*/
1620
public fun Analytics(
1721
writeKey: String,
1822
context: Context
@@ -29,12 +33,16 @@ public fun Analytics(
2933
}
3034

3135
@Suppress("FunctionName")
32-
// constructor function to build android specific analytics in dsl format with config options
33-
// Usage: Analytics("$writeKey", applicationContext) {
34-
// this.analyticsScope = applicationScope
35-
// this.collectDeviceId = false
36-
// this.flushAt = 10
37-
// }
36+
/**
37+
* constructor function to build android specific analytics in dsl format with config options
38+
* Usage: Analytics("$writeKey", applicationContext) {
39+
* this.analyticsScope = applicationScope
40+
* this.collectDeviceId = false
41+
* this.flushAt = 10
42+
* }
43+
*
44+
* NOTE: this method should only be used for Android application. Context is required.
45+
*/
3846
public fun Analytics(
3947
writeKey: String,
4048
context: Context,

android/src/main/java/com/segment/analytics/kotlin/android/plugins/AndroidContextPlugin.kt

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
99
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
1010
import android.net.NetworkCapabilities.TRANSPORT_WIFI
1111
import android.os.Build
12-
import android.provider.Settings.Secure
13-
import android.telephony.TelephonyManager
1412
import com.segment.analytics.kotlin.core.Analytics
1513
import com.segment.analytics.kotlin.core.BaseEvent
1614
import com.segment.analytics.kotlin.core.Storage
@@ -25,6 +23,10 @@ import java.util.Locale
2523
import java.util.TimeZone
2624
import java.util.UUID
2725
import java.lang.System as JavaSystem
26+
import android.media.MediaDrm
27+
import java.lang.Exception
28+
import java.security.MessageDigest
29+
2830

2931
// Plugin that applies context related changes. Auto-added to system on build
3032
class AndroidContextPlugin : Plugin {
@@ -122,53 +124,23 @@ class AndroidContextPlugin : Plugin {
122124
}
123125

124126
device = buildJsonObject {
125-
put(DEVICE_ID_KEY, getDeviceId(collectDeviceId, context))
127+
put(DEVICE_ID_KEY, getDeviceId(collectDeviceId))
126128
put(DEVICE_MANUFACTURER_KEY, Build.MANUFACTURER)
127129
put(DEVICE_MODEL_KEY, Build.MODEL)
128130
put(DEVICE_NAME_KEY, Build.DEVICE)
129131
put(DEVICE_TYPE_KEY, "android")
130132
}
131133
}
132134

133-
@SuppressLint("HardwareIds", "MissingPermission")
134-
internal fun getDeviceId(collectDeviceId: Boolean, context: Context): String {
135+
internal fun getDeviceId(collectDeviceId: Boolean): String {
135136
if (!collectDeviceId) {
136137
return storage.read(Storage.Constants.AnonymousId) ?: ""
137138
}
138-
val androidId = Secure.getString(context.contentResolver, Secure.ANDROID_ID)
139-
if (!androidId.isNullOrEmpty() && "unknown" != androidId) {
140-
return androidId
141-
}
142-
143-
// Serial number, guaranteed to be on all non phones in 2.3+.
144-
val buildNumber = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
145-
Build.getSerial()
146-
} else {
147-
@Suppress("DEPRECATION")
148-
Build.SERIAL
149-
}
150139

151-
if (!buildNumber.isNullOrEmpty()) {
152-
return buildNumber
153-
}
154-
155-
// Telephony ID, guaranteed to be on all phones, requires READ_PHONE_STATE permission
156-
if (hasPermission(context, permission.READ_PHONE_STATE)
157-
&& hasFeature(context, PackageManager.FEATURE_TELEPHONY)
158-
) {
159-
val telephonyManager =
160-
getSystemService<TelephonyManager>(
161-
context,
162-
Context.TELEPHONY_SERVICE
163-
)
164-
val telephonyId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
165-
telephonyManager.imei
166-
} else @Suppress("DEPRECATION") {
167-
telephonyManager.deviceId
168-
}
169-
if (!telephonyId.isNullOrEmpty()) {
170-
return telephonyId
171-
}
140+
// unique id generated from DRM API
141+
val uniqueId = getUniqueID()
142+
if (!uniqueId.isNullOrEmpty()) {
143+
return uniqueId
172144
}
173145
// If this still fails, generate random identifier that does not persist across
174146
// installations
@@ -270,4 +242,35 @@ fun hasPermission(context: Context, permission: String): Boolean {
270242
/** Returns true if the application has the given feature. */
271243
fun hasFeature(context: Context, feature: String): Boolean {
272244
return context.packageManager.hasSystemFeature(feature)
273-
}
245+
}
246+
247+
248+
/**
249+
* Workaround for not able to get device id on Android 10 or above using DRM API
250+
* {@see https://stackoverflow.com/questions/58103580/android-10-imei-no-longer-available-on-api-29-looking-for-alternatives}
251+
* {@see https://developer.android.com/training/articles/user-data-ids}
252+
*/
253+
fun getUniqueID(): String? {
254+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2)
255+
return null
256+
257+
val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L)
258+
var wvDrm: MediaDrm? = null
259+
try {
260+
wvDrm = MediaDrm(WIDEVINE_UUID)
261+
val wideVineId = wvDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID)
262+
val md = MessageDigest.getInstance("SHA-256")
263+
md.update(wideVineId)
264+
return md.digest().toHexString()
265+
} catch (e: Exception) {
266+
return null
267+
} finally {
268+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
269+
wvDrm?.close()
270+
} else {
271+
wvDrm?.release()
272+
}
273+
}
274+
}
275+
276+
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.segment.analytics.kotlin.android
2+
3+
import org.junit.jupiter.api.Assertions.*
4+
5+
import org.junit.jupiter.api.Test
6+
import org.junit.jupiter.api.assertThrows
7+
8+
internal class AndroidAnalyticsKtTest {
9+
@Test
10+
fun `jvm initializer in android platform should failed`() {
11+
val exception = assertThrows<Exception> {
12+
com.segment.analytics.kotlin.core.Analytics("123") {
13+
application = "Test"
14+
}
15+
}
16+
17+
assertEquals(exception.message?.contains("Android"), true)
18+
}
19+
}

android/src/test/java/com/segment/analytics/kotlin/android/AndroidContextCollectorTests.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package com.segment.analytics.kotlin.android
22

33
import android.content.Context
44
import android.content.SharedPreferences
5+
import android.util.Log
56
import androidx.test.platform.app.InstrumentationRegistry
67
import com.segment.analytics.kotlin.core.*
78
import com.segment.analytics.kotlin.android.plugins.AndroidContextPlugin
9+
import com.segment.analytics.kotlin.android.plugins.getUniqueID
810
import com.segment.analytics.kotlin.android.utils.MemorySharedPreferences
911
import io.mockk.every
12+
import io.mockk.mockkStatic
1013
import io.mockk.spyk
1114
import kotlinx.coroutines.runBlocking
1215
import kotlinx.serialization.json.*
@@ -28,6 +31,8 @@ class AndroidContextCollectorTests {
2831
appContext = spyk(InstrumentationRegistry.getInstrumentation().targetContext)
2932
val sharedPreferences: SharedPreferences = MemorySharedPreferences()
3033
every { appContext.getSharedPreferences(any(), any()) } returns sharedPreferences
34+
mockkStatic("com.segment.analytics.kotlin.android.plugins.AndroidContextPluginKt")
35+
every { getUniqueID() } returns "unknown"
3136

3237
analytics = Analytics(
3338
Configuration(
@@ -100,7 +105,8 @@ class AndroidContextCollectorTests {
100105
analytics.storage.write(Storage.Constants.AnonymousId, "anonId")
101106
val contextCollector = AndroidContextPlugin()
102107
contextCollector.setup(analytics)
103-
val deviceId = contextCollector.getDeviceId(false, appContext)
108+
val deviceId = contextCollector.getDeviceId(false)
109+
Log.d("debug flaky test", deviceId)
104110
assertEquals(deviceId, "anonId")
105111
}
106112

android/src/test/java/com/segment/analytics/kotlin/android/AndroidLifecyclePluginTests.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ class AndroidLifecyclePluginTests {
316316
// Simulate activity startup
317317
lifecyclePlugin.onActivityCreated(mockActivity, mockBundle)
318318

319-
verify (timeout = 2000){ mockPlugin.updateState(true) }
319+
verify (timeout = 4000){ mockPlugin.updateState(true) }
320320
val track = slot<TrackEvent>()
321321
verify { mockPlugin.track(capture(track)) }
322322
with(track.captured) {

build.gradle

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@ allprojects {
2727
repositories {
2828
google()
2929
mavenCentral()
30-
maven { url 'https://jitpack.io' }
3130
maven { url "https://kotlin.bintray.com/kotlinx" }
3231
maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
3332
}
34-
33+
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
34+
kotlinOptions {
35+
freeCompilerArgs = ['-Xjvm-default=enable'] //enable or compatibility
36+
jvmTarget = "1.8"
37+
}
38+
}
3539
group GROUP
36-
version VERSION_NAME
40+
version getVersionName()
3741
}
3842

3943
snyk {
@@ -47,5 +51,9 @@ task clean(type: Delete) {
4751
delete rootProject.buildDir
4852
}
4953

54+
def getVersionName() { // If not release build add SNAPSHOT suffix
55+
return hasProperty('release') ? VERSION_NAME : VERSION_NAME+"-SNAPSHOT"
56+
}
57+
5058
apply from: rootProject.file('gradle/promote.gradle')
5159
apply from: rootProject.file('gradle/codecov.gradle')

core/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ test {
1515

1616
dependencies {
1717
// MAIN DEPS
18-
api 'com.github.segmentio:sovran-kotlin:1.2.0'
18+
api 'com.segment:sovran-kotlin:1.2.1'
1919
api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
2020
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
2121

0 commit comments

Comments
 (0)