Skip to content

prevent using jvm initializer in android and address violation of retrieving unsettable device id #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ on:
workflow_dispatch:

jobs:
core-test:
cancel_previous:

runs-on: ubuntu-latest
steps:
- uses: styfle/cancel-workflow-action@0.9.1
with:
workflow_id: ${{ github.event.workflow.id }}

core-test:
needs: cancel_previous
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
Expand All @@ -33,7 +41,7 @@ jobs:
uses: codecov/codecov-action@v2

android-test:

needs: cancel_previous
runs-on: ubuntu-latest

steps:
Expand All @@ -57,7 +65,7 @@ jobs:
uses: codecov/codecov-action@v2

destination-test:

needs: cancel_previous
runs-on: ubuntu-latest

steps:
Expand All @@ -81,7 +89,7 @@ jobs:
uses: codecov/codecov-action@v2

security:

needs: cancel_previous
runs-on: ubuntu-latest

steps:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ jobs:
run: |
curl \
-X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Authorization: token $RELEASE_TOKEN" \
https://api.github.com/repos/${{github.repository}}/releases \
-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}'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_VERSION: ${{ steps.vars.outputs.tag }}
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ android {
dependencies {
// MAIN DEPS
api project(':core')
api 'com.github.segmentio:sovran-kotlin:1.2.0'
api 'com.segment:sovran-kotlin:1.2.1'
api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import com.segment.analytics.kotlin.core.platform.plugins.logger.*
// A set of functions tailored to the Android implementation of analytics

@Suppress("FunctionName")
// constructor function to build android specific analytics in dsl format
// Usage: Analytics("$writeKey", applicationContext, applicationScope)
/**
* constructor function to build android specific analytics in dsl format
* Usage: Analytics("$writeKey", applicationContext, applicationScope)
*
* NOTE: this method should only be used for Android application. Context is required.
*/
public fun Analytics(
writeKey: String,
context: Context
Expand All @@ -29,12 +33,16 @@ public fun Analytics(
}

@Suppress("FunctionName")
// constructor function to build android specific analytics in dsl format with config options
// Usage: Analytics("$writeKey", applicationContext) {
// this.analyticsScope = applicationScope
// this.collectDeviceId = false
// this.flushAt = 10
// }
/**
* constructor function to build android specific analytics in dsl format with config options
* Usage: Analytics("$writeKey", applicationContext) {
* this.analyticsScope = applicationScope
* this.collectDeviceId = false
* this.flushAt = 10
* }
*
* NOTE: this method should only be used for Android application. Context is required.
*/
public fun Analytics(
writeKey: String,
context: Context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.os.Build
import android.provider.Settings.Secure
import android.telephony.TelephonyManager
import com.segment.analytics.kotlin.core.Analytics
import com.segment.analytics.kotlin.core.BaseEvent
import com.segment.analytics.kotlin.core.Storage
Expand All @@ -25,6 +23,10 @@ import java.util.Locale
import java.util.TimeZone
import java.util.UUID
import java.lang.System as JavaSystem
import android.media.MediaDrm
import java.lang.Exception
import java.security.MessageDigest


// Plugin that applies context related changes. Auto-added to system on build
class AndroidContextPlugin : Plugin {
Expand Down Expand Up @@ -122,53 +124,23 @@ class AndroidContextPlugin : Plugin {
}

device = buildJsonObject {
put(DEVICE_ID_KEY, getDeviceId(collectDeviceId, context))
put(DEVICE_ID_KEY, getDeviceId(collectDeviceId))
put(DEVICE_MANUFACTURER_KEY, Build.MANUFACTURER)
put(DEVICE_MODEL_KEY, Build.MODEL)
put(DEVICE_NAME_KEY, Build.DEVICE)
put(DEVICE_TYPE_KEY, "android")
}
}

@SuppressLint("HardwareIds", "MissingPermission")
internal fun getDeviceId(collectDeviceId: Boolean, context: Context): String {
internal fun getDeviceId(collectDeviceId: Boolean): String {
if (!collectDeviceId) {
return storage.read(Storage.Constants.AnonymousId) ?: ""
}
val androidId = Secure.getString(context.contentResolver, Secure.ANDROID_ID)
if (!androidId.isNullOrEmpty() && "unknown" != androidId) {
return androidId
}

// Serial number, guaranteed to be on all non phones in 2.3+.
val buildNumber = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Build.getSerial()
} else {
@Suppress("DEPRECATION")
Build.SERIAL
}

if (!buildNumber.isNullOrEmpty()) {
return buildNumber
}

// Telephony ID, guaranteed to be on all phones, requires READ_PHONE_STATE permission
if (hasPermission(context, permission.READ_PHONE_STATE)
&& hasFeature(context, PackageManager.FEATURE_TELEPHONY)
) {
val telephonyManager =
getSystemService<TelephonyManager>(
context,
Context.TELEPHONY_SERVICE
)
val telephonyId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
telephonyManager.imei
} else @Suppress("DEPRECATION") {
telephonyManager.deviceId
}
if (!telephonyId.isNullOrEmpty()) {
return telephonyId
}
// unique id generated from DRM API
val uniqueId = getUniqueID()
if (!uniqueId.isNullOrEmpty()) {
return uniqueId
}
// If this still fails, generate random identifier that does not persist across
// installations
Expand Down Expand Up @@ -270,4 +242,35 @@ fun hasPermission(context: Context, permission: String): Boolean {
/** Returns true if the application has the given feature. */
fun hasFeature(context: Context, feature: String): Boolean {
return context.packageManager.hasSystemFeature(feature)
}
}


/**
* Workaround for not able to get device id on Android 10 or above using DRM API
* {@see https://stackoverflow.com/questions/58103580/android-10-imei-no-longer-available-on-api-29-looking-for-alternatives}
* {@see https://developer.android.com/training/articles/user-data-ids}
*/
fun getUniqueID(): String? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given that we support api 16+ we should have some fallback logic here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. the getDeviceId method does a null check and returns random uuid as fallback.

return null

val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L)
var wvDrm: MediaDrm? = null
try {
wvDrm = MediaDrm(WIDEVINE_UUID)
val wideVineId = wvDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID)
val md = MessageDigest.getInstance("SHA-256")
md.update(wideVineId)
return md.digest().toHexString()
} catch (e: Exception) {
return null
} finally {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
wvDrm?.close()
} else {
wvDrm?.release()
}
}
}

fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.segment.analytics.kotlin.android

import org.junit.jupiter.api.Assertions.*

import org.junit.jupiter.api.Test

internal class AndroidAnalyticsKtTest {
@Test
fun `jvm initializer in android platform should failed`() {
try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the new junit has a way to expect exceptions instead of doing this pattern. if u wanna try that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me try that

com.segment.analytics.kotlin.core.Analytics("123") {
application = "Test"
}
fail()
}
catch(e: Exception){
assertEquals(e.message?.contains("Android"), true)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import android.content.SharedPreferences
import androidx.test.platform.app.InstrumentationRegistry
import com.segment.analytics.kotlin.core.*
import com.segment.analytics.kotlin.android.plugins.AndroidContextPlugin
import com.segment.analytics.kotlin.android.plugins.getUniqueID
import com.segment.analytics.kotlin.android.utils.MemorySharedPreferences
import com.segment.analytics.kotlin.core.platform.plugins.logger.LogFilterKind
import com.segment.analytics.kotlin.core.platform.plugins.logger.log
import com.segment.analytics.kotlin.core.platform.plugins.logger.segmentLog
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.spyk
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.*
import org.junit.Assert.*
Expand All @@ -28,6 +34,8 @@ class AndroidContextCollectorTests {
appContext = spyk(InstrumentationRegistry.getInstrumentation().targetContext)
val sharedPreferences: SharedPreferences = MemorySharedPreferences()
every { appContext.getSharedPreferences(any(), any()) } returns sharedPreferences
mockkStatic("com.segment.analytics.kotlin.android.plugins.AndroidContextPluginKt")
every { getUniqueID() } returns "unknown"

analytics = Analytics(
Configuration(
Expand Down Expand Up @@ -100,7 +108,8 @@ class AndroidContextCollectorTests {
analytics.storage.write(Storage.Constants.AnonymousId, "anonId")
val contextCollector = AndroidContextPlugin()
contextCollector.setup(analytics)
val deviceId = contextCollector.getDeviceId(false, appContext)
val deviceId = contextCollector.getDeviceId(false)
Analytics.segmentLog(deviceId, LogFilterKind.DEBUG)
assertEquals(deviceId, "anonId")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ class AndroidLifecyclePluginTests {
// Simulate activity startup
lifecyclePlugin.onActivityCreated(mockActivity, mockBundle)

verify (timeout = 2000){ mockPlugin.updateState(true) }
verify (timeout = 4000){ mockPlugin.updateState(true) }
val track = slot<TrackEvent>()
verify { mockPlugin.track(capture(track)) }
with(track.captured) {
Expand Down
14 changes: 11 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
maven { url "https://kotlin.bintray.com/kotlinx" }
maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
freeCompilerArgs = ['-Xjvm-default=enable'] //enable or compatibility
jvmTarget = "1.8"
}
}
group GROUP
version VERSION_NAME
version getVersionName()
}

snyk {
Expand All @@ -47,5 +51,9 @@ task clean(type: Delete) {
delete rootProject.buildDir
}

def getVersionName() { // If not release build add SNAPSHOT suffix
return hasProperty('release') ? VERSION_NAME : VERSION_NAME+"-SNAPSHOT"
}

apply from: rootProject.file('gradle/promote.gradle')
apply from: rootProject.file('gradle/codecov.gradle')
2 changes: 1 addition & 1 deletion core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test {

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

Expand Down
34 changes: 27 additions & 7 deletions core/src/main/java/com/segment/analytics/kotlin/core/Analytics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -513,15 +513,35 @@ class Analytics internal constructor(
}
}

// constructor function to build analytics in dsl format with config options
// Usage: Analytics("123") {
// this.application = "n/a"
// this.analyticsScope = MainScope()
// this.collectDeviceId = false
// this.flushAt = 10
// }

/**
* constructor function to build analytics in dsl format with config options
* Usage: Analytics("123") {
* this.application = "n/a"
* this.collectDeviceId = false
* this.flushAt = 10
* }
*
* NOTE: this method should only be used for JVM application. for Android, there is
* another set of extension functions that requires a context as the second parameter:
* * Analytics(writeKey: String, context: Context)
* * Analytics(writeKey: String, context: Context, configs: Configuration.() -> Unit)
*/
fun Analytics(writeKey: String, configs: Configuration.() -> Unit): Analytics {
if (isAndroid()) {
error("Using JVM Analytics initializer in Android platform. Context is required in constructor!")
}

val config = Configuration(writeKey)
configs.invoke(config)
return Analytics(config)
}

internal fun isAndroid(): Boolean {
return try {
Class.forName("com.segment.analytics.kotlin.android.AndroidStorage")
true
} catch (e: ClassNotFoundException) {
false
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.segment.analytics.kotlin.core

object Constants {
const val LIBRARY_VERSION = "1.4.1"
const val LIBRARY_VERSION = "1.4.2"
const val DEFAULT_API_HOST = "api.segment.io/v1"
const val DEFAULT_CDN_HOST = "cdn-settings.segment.com/v1"
}
Loading