Skip to content

Commit

Permalink
Releasing 4.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Rover Release Bot 🤖 committed Nov 16, 2023
1 parent 867cb03 commit d1ccc40
Show file tree
Hide file tree
Showing 32 changed files with 770 additions and 213 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import io.rover.sdk.core.container.Scope
import io.rover.sdk.core.events.ContextProvider
import io.rover.sdk.core.events.EventQueueServiceInterface
import io.rover.sdk.core.platform.LocalStorage
import io.rover.sdk.core.privacy.PrivacyService
import io.rover.sdk.core.streams.Scheduler

/**
Expand All @@ -40,6 +41,7 @@ class AdvertisingAssembler : Assembler {
) { resolver ->
AdvertisingIdContentProvider(
resolver.resolveSingletonOrFail(Context::class.java),
resolver.resolveSingletonOrFail(PrivacyService::class.java),
resolver.resolveSingletonOrFail(Scheduler::class.java, "io"),
resolver.resolveSingletonOrFail(LocalStorage::class.java)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ import io.rover.sdk.core.data.domain.DeviceContext
import io.rover.sdk.core.events.ContextProvider
import io.rover.sdk.core.logging.log
import io.rover.sdk.core.platform.LocalStorage
import io.rover.sdk.core.privacy.PrivacyService
import io.rover.sdk.core.streams.Publishers
import io.rover.sdk.core.streams.Scheduler
import io.rover.sdk.core.streams.subscribe
import io.rover.sdk.core.streams.subscribeOn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class AdvertisingIdContentProvider(
applicationContext: Context,
ioScheduler: Scheduler,
private val applicationContext: Context,
private val privacyService: PrivacyService,
private val ioScheduler: Scheduler,
localStorage: LocalStorage
) : ContextProvider {
private val keyValueStorage = localStorage.getKeyValueStorageFor(STORAGE_CONTEXT_IDENTIFIER)
Expand All @@ -41,7 +46,7 @@ class AdvertisingIdContentProvider(
field = token
}

init {
private fun acquireAdvertisingId() {
Publishers.defer {
advertisingId = try {
AdvertisingIdClient.getAdvertisingIdInfo(applicationContext).id
Expand All @@ -55,9 +60,26 @@ class AdvertisingIdContentProvider(
}.subscribeOn(ioScheduler).subscribe { }
}

init {
CoroutineScope(Dispatchers.Main).launch {
privacyService.trackingModeFlow.collect { trackingEnabled ->
if (trackingEnabled != PrivacyService.TrackingMode.Default) {
advertisingId = null
log.i("Tracking disabled, advertising id cleared.")
} else {
log.i("Tracking enabled, acquiring advertising id.")
acquireAdvertisingId()
}
}
}
}

override fun captureContext(deviceContext: DeviceContext): DeviceContext {
if (privacyService.trackingMode != PrivacyService.TrackingMode.Default) {
return deviceContext
}
return deviceContext.copy(
advertisingIdentifier = this.advertisingId
advertisingIdentifier = this.advertisingId,
)
}

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.

// The version number for the build SDK modules and testbench app.
val roverSdkVersion by extra("4.3.0")
val roverSdkVersion by extra("4.4.0")

// Definitions of several core shared dependencies:
val kotlinVersion by extra("1.8.20") // NB: when changing this one check the two duplicates of this number below
Expand Down
58 changes: 40 additions & 18 deletions core/src/main/kotlin/io/rover/sdk/core/CoreAssembler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import io.rover.sdk.core.platform.IoMultiplexingExecutor
import io.rover.sdk.core.platform.LocalStorage
import io.rover.sdk.core.platform.SharedPreferencesLocalStorage
import io.rover.sdk.core.platform.whenNotNull
import io.rover.sdk.core.privacy.PrivacyService
import io.rover.sdk.core.routing.LinkOpenInterface
import io.rover.sdk.core.routing.Router
import io.rover.sdk.core.routing.RouterService
Expand Down Expand Up @@ -179,6 +180,12 @@ class CoreAssembler @JvmOverloads constructor(
application
}

container.register(Scope.Singleton, PrivacyService::class.java) { resolver ->
PrivacyService(
resolver.resolveSingletonOrFail(LocalStorage::class.java)
)
}

if (openAppIntent != null || application.packageManager.getLaunchIntentForPackage(application.packageName) != null) {
container.register(Scope.Singleton, Intent::class.java, "openApp") { _ ->
openAppIntent ?: application.packageManager.getLaunchIntentForPackage(application.packageName) ?: Intent()
Expand Down Expand Up @@ -303,8 +310,8 @@ class CoreAssembler @JvmOverloads constructor(
ScreenContextProvider(application.resources)
}

container.register(Scope.Singleton, ContextProvider::class.java, "telephony") { _ ->
TelephonyContextProvider(application)
container.register(Scope.Singleton, ContextProvider::class.java, "telephony") { resolver ->
TelephonyContextProvider(application, resolver.resolveSingletonOrFail(PrivacyService::class.java))
}

container.register(Scope.Singleton, ContextProvider::class.java, "device") { _ ->
Expand All @@ -315,8 +322,11 @@ class CoreAssembler @JvmOverloads constructor(
TimeZoneContextProvider()
}

container.register(Scope.Singleton, ContextProvider::class.java, "locationAuthorization") { _ ->
LocationServicesContextProvider(application)
container.register(Scope.Singleton, ContextProvider::class.java, "locationAuthorization") { resolver ->
LocationServicesContextProvider(
application,
resolver.resolveSingletonOrFail(PrivacyService::class.java)
)
}

container.register(Scope.Singleton, ContextProvider::class.java, "attributes") { resolver ->
Expand Down Expand Up @@ -485,20 +495,21 @@ class CoreAssembler @JvmOverloads constructor(
val eventQueue = resolver.resolveSingletonOrFail(EventQueueServiceInterface::class.java)

listOf(
resolver.resolveSingletonOrFail(ContextProvider::class.java, "device"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "locale"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "darkMode"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "reachability"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "screen"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "telephony"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "timeZone"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "attributes"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "application"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "deviceIdentifier"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "sdkVersion"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "locationAuthorization"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "conversions"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "lastSeen")
resolver.resolveSingletonOrFail(PrivacyService::class.java),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "device"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "locale"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "darkMode"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "reachability"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "screen"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "telephony"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "timeZone"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "attributes"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "application"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "deviceIdentifier"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "sdkVersion"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "locationAuthorization"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "conversions"),
resolver.resolveSingletonOrFail(ContextProvider::class.java, "lastSeen"),
).forEach { eventQueue.addContextProvider(it) }

resolver.resolveSingletonOrFail(VersionTrackerInterface::class.java).trackAppVersion()
Expand Down Expand Up @@ -528,6 +539,8 @@ class CoreAssembler @JvmOverloads constructor(
resolver.resolveSingletonOrFail(ConversionsManager::class.java).apply {
this.migrateLegacyTags()
}

resolver.resolveSingletonOrFail(PrivacyService::class.java).refreshAllListeners()
}
}

Expand Down Expand Up @@ -568,3 +581,12 @@ val Rover.userInfoManager: UserInfoInterface
private fun missingDependencyError(name: String): Throwable {
throw RuntimeException("Dependency not registered: $name. Did you include CoreAssembler() in the assembler list?")
}

val Rover.privacyService: PrivacyService
get() = this.resolve(PrivacyService::class.java) ?: throw missingDependencyError("PrivacyService")

var Rover.trackingMode: PrivacyService.TrackingMode
get() = privacyService.trackingMode
set(value) {
privacyService.trackingMode = value
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import java.util.Date
* A Rover context: describes the device and general situation when an [Event] is generated.
*/
data class DeviceContext(
val trackingMode: String?,
val appBuild: String?,
val appIdentifier: String?,
val appVersion: String?,
Expand Down Expand Up @@ -118,7 +119,7 @@ data class DeviceContext(
null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null,
null, null, null, null, null, null, false, null, hashMapOf(), null, listOf(), null
null, null, null, null, null, null, false, null, null, hashMapOf(), null, listOf(), null
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import org.json.JSONObject
internal fun DeviceContext.asJson(dateFormatting: DateFormattingInterface): JSONObject {
return JSONObject().apply {
val props = listOf(
DeviceContext::trackingMode,
DeviceContext::appBuild,
DeviceContext::appIdentifier,
DeviceContext::deviceIdentifier,
Expand Down Expand Up @@ -87,6 +88,7 @@ internal fun DeviceContext.asJson(dateFormatting: DateFormattingInterface): JSON
*/
internal fun DeviceContext.Companion.decodeJson(json: JSONObject, dateFormatting: DateFormattingInterface): DeviceContext {
return DeviceContext(
trackingMode = json.safeOptString("trackingMode"),
appBuild = json.safeOptString("appBuild"),
appIdentifier = json.safeOptString("appIdentifier"),
appVersion = json.safeOptString("appVersion"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ import android.provider.Settings.Secure.LOCATION_MODE_OFF
import androidx.core.content.ContextCompat
import io.rover.sdk.core.data.domain.DeviceContext
import io.rover.sdk.core.events.ContextProvider
import io.rover.sdk.core.privacy.PrivacyService

class LocationServicesContextProvider(val applicationContext: android.content.Context) : ContextProvider {
class LocationServicesContextProvider(
private val applicationContext: android.content.Context,
private val privacyService: PrivacyService
) : ContextProvider {
companion object {
private const val BACKGROUND_LOCATION_PERMISSION_CODE = "android.permission.ACCESS_BACKGROUND_LOCATION"
private const val Q_VERSION_CODE = 29
Expand All @@ -38,6 +42,10 @@ class LocationServicesContextProvider(val applicationContext: android.content.Co
}

override fun captureContext(deviceContext: DeviceContext): DeviceContext {
if (privacyService.trackingMode != PrivacyService.TrackingMode.Default) {
return deviceContext.copy(locationAuthorization = DENIED, isLocationServicesEnabled = false)
}

val mode = Settings.Secure.getInt(applicationContext.contentResolver, LOCATION_MODE, LOCATION_MODE_OFF)
val locationServicesEnabled = mode != LOCATION_MODE_OFF

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ import android.telephony.TelephonyManager
import androidx.core.app.ActivityCompat
import io.rover.sdk.core.data.domain.DeviceContext
import io.rover.sdk.core.events.ContextProvider
import io.rover.sdk.core.privacy.PrivacyService

/**
* Captures and adds the mobile carrier and data connection details to a [DeviceContext].
*/
class TelephonyContextProvider(
private val applicationContext: android.content.Context
private val applicationContext: android.content.Context,
private val privacyService: PrivacyService
) : ContextProvider {
private val telephonyManager = applicationContext.applicationContext.getSystemService(android.content.Context.TELEPHONY_SERVICE) as TelephonyManager

Expand Down Expand Up @@ -62,6 +64,10 @@ class TelephonyContextProvider(

@SuppressLint("MissingPermission")
override fun captureContext(deviceContext: DeviceContext): DeviceContext {
if (privacyService.trackingMode != PrivacyService.TrackingMode.Default) {
return deviceContext
}

val targetSdkVersion = applicationContext.applicationInfo.targetSdkVersion

val networkTypeName = if (ActivityCompat.checkSelfPermission(applicationContext, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED || targetSdkVersion < 30) {
Expand Down
83 changes: 83 additions & 0 deletions core/src/main/kotlin/io/rover/sdk/core/privacy/PrivacyService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2023, Rover Labs, Inc. All rights reserved.
* You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
* copy, modify, and distribute this software in source code or binary form for use
* in connection with the web services and APIs provided by Rover.
*
* This copyright notice shall be included in all copies or substantial portions of
* the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package io.rover.sdk.core.privacy

import io.rover.sdk.core.data.domain.DeviceContext
import io.rover.sdk.core.events.ContextProvider
import io.rover.sdk.core.logging.log
import io.rover.sdk.core.platform.LocalStorage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow

/**
* This object is responsible for the global privacy settings of the Rover SDK.
*/
class PrivacyService(
private val localStorage: LocalStorage,

) : ContextProvider {

private val _trackingEnabledFlow: MutableStateFlow<TrackingMode> = MutableStateFlow(
localStorage.getKeyValueStorageFor("PrivacyService")["trackingMode"]?.let { stringValue ->
TrackingMode.values().singleOrNull() { it.wireFormat == stringValue }
} ?: TrackingMode.Default,
)

var trackingModeFlow: SharedFlow<TrackingMode> = _trackingEnabledFlow.asSharedFlow()

enum class TrackingMode(
val wireFormat: String,
) {
Default("default"),
Anonymized("anonymized"),
}

var trackingMode: TrackingMode
get() {
return _trackingEnabledFlow.value
}
set(value) {
localStorage.getKeyValueStorageFor("PrivacyService")["trackingMode"] =
value.wireFormat
_trackingEnabledFlow.value = value
log.i("Tracking set to ${value.wireFormat}.")
listeners.forEach { it.onTrackingModeChanged(value) }
}

private var listeners = mutableListOf<TrackingEnabledChangedListener>()

fun registerTrackingEnabledChangedListener(listener: TrackingEnabledChangedListener) {
listeners.add(listener)
listener.onTrackingModeChanged(trackingMode)
}

interface TrackingEnabledChangedListener {
fun onTrackingModeChanged(trackingMode: TrackingMode)
}

fun refreshAllListeners() {
listeners.forEach { it.onTrackingModeChanged(trackingMode) }
}

override fun captureContext(deviceContext: DeviceContext): DeviceContext {
return deviceContext.copy(
trackingMode = trackingMode.wireFormat,
)
}
}
19 changes: 17 additions & 2 deletions core/src/main/kotlin/io/rover/sdk/core/streams/Publishers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ object Publishers {
fun <T1, T2, R> combineLatest(
source1: Publisher<T1>,
source2: Publisher<T2>,
combiner: (T1, T2) -> R
combiner: (T1, T2) -> R,
): Publisher<R> {
@Suppress("UNCHECKED_CAST") // Suppression due to erasure/variance issues.
return combineLatest(listOf(source1, source2) as List<Publisher<Any>>) { list: List<Any> ->
Expand All @@ -320,7 +320,7 @@ object Publishers {
source1: Publisher<T1>,
source2: Publisher<T2>,
source3: Publisher<T3>,
combiner: (T1, T2, T3) -> R
combiner: (T1, T2, T3) -> R,
): Publisher<R> {
@Suppress("UNCHECKED_CAST") // Suppression due to erasure/variance issues.
return combineLatest(listOf(source1, source2, source3) as List<Publisher<Any>>) { list: List<Any> ->
Expand All @@ -329,4 +329,19 @@ object Publishers {
combiner(list[0] as T1, list[1] as T2, list[2] as T3)
}
}

fun <T1, T2, T3, T4, R> combineLatest(
source1: Publisher<T1>,
source2: Publisher<T2>,
source3: Publisher<T3>,
source4: Publisher<T4>,
combiner: (T1, T2, T3, T4) -> R,
): Publisher<R> {
@Suppress("UNCHECKED_CAST") // Suppression due to erasure/variance issues.
return combineLatest(listOf(source1, source2, source3, source4) as List<Publisher<Any>>) { list: List<Any> ->
// Suppression due to erasure.
@Suppress("UNCHECKED_CAST")
combiner(list[0] as T1, list[1] as T2, list[2] as T3, list[3] as T4)
}
}
}
Loading

0 comments on commit d1ccc40

Please sign in to comment.