Signed-off-by: Haoyi <haoyi.zhang@castbox.fm>
v3.3.1
Haoyi 2024-09-19 17:45:53 +08:00
parent d5ec200617
commit d55b525069
21 changed files with 517 additions and 441 deletions

View File

@ -179,7 +179,7 @@ class MainActivity : AppCompatActivity() {
} }
findViewById<TextView>(R.id.tvOpenTestProcessActivity).setOnClickListener { findViewById<TextView>(R.id.tvOpenTestProcessActivity).setOnClickListener {
// TestProcessActivity.startActivity(this) // TestProcessActivity.startActivity(this)
GuruAnalytics.Builder(this) GuruAnalytics.Builder(this, "v3.0.0")
.setBatchLimit(25) .setBatchLimit(25)
.setUploadPeriodInSeconds(60) .setUploadPeriodInSeconds(60)
.setStartUploadDelayInSecond(3) .setStartUploadDelayInSecond(3)

View File

@ -21,7 +21,7 @@ class TestProcessActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_process) setContentView(R.layout.activity_test_process)
GuruAnalytics.Builder(this) GuruAnalytics.Builder(this, "v3.0.0")
.setBatchLimit(25) .setBatchLimit(25)
.setUploadPeriodInSeconds(60) .setUploadPeriodInSeconds(60)
.setStartUploadDelayInSecond(3) .setStartUploadDelayInSecond(3)

View File

@ -20,4 +20,6 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the # Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
guruAnalyticsSdkVersion=1.1.2

View File

@ -30,6 +30,7 @@ android {
versionName "1.0" versionName "1.0"
buildConfigField "String", "buildTs", buildTs() buildConfigField "String", "buildTs", buildTs()
buildConfigField "String", "guruAnalyticsSdkVersion", "\"v$guruAnalyticsSdkVersion\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@ -18,7 +18,8 @@ publishing { // Repositories *to* which Gradle can publish artifacts
maven(MavenPublication) { maven(MavenPublication) {
groupId 'guru.core.analytics' groupId 'guru.core.analytics'
artifactId 'guru_analytics' artifactId 'guru_analytics'
version '1.1.0' // Your package version // version '1.1.1' // Your package version
version = project.findProperty("guruAnalyticsSdkVersion") ?: "unknown"
// artifact publishArtifact //Example: *./target/myJavaClasses.jar* // artifact publishArtifact //Example: *./target/myJavaClasses.jar*
// artifact "build/outputs/aar/aar-test-release.aar"//aar // artifact "build/outputs/aar/aar-test-release.aar"//aar
afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) } afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) }

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="guru.core.analytics"> package="guru.core.analytics">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

View File

@ -12,14 +12,18 @@ object Constants {
const val EVENT = "event" const val EVENT = "event"
const val PARAM = "param" const val PARAM = "param"
const val SCREEN = "screen_name" const val SCREEN = "screen_name"
const val SESSION_ID = "session_id"
const val ITEM_CATEGORY = "item_category" const val ITEM_CATEGORY = "item_category"
const val ITEM_NAME = "item_name" const val ITEM_NAME = "item_name"
const val VALUE = "value" const val VALUE = "value"
const val FG = "fg" const val FG = "guru_engagement"
const val DURATION = "duration" const val DURATION = "duration"
const val FIRST_OPEN = "first_open" const val FIRST_OPEN = "first_open"
const val ERROR_PROCESS = "error_process" const val ERROR_PROCESS = "error_process"
const val PROCESS = "process" const val PROCESS = "process"
const val SESSION_START = "session_start"
const val SDK_INIT = "guru_sdk_init"
const val SDK_INIT_COMPLETE = "guru_sdk_init_complete"
} }
object Ids { object Ids {
@ -43,7 +47,9 @@ object Constants {
const val SCREEN_W = "screenW" const val SCREEN_W = "screenW"
const val OS_VERSION = "osVersion" const val OS_VERSION = "osVersion"
const val LANGUAGE = "language" const val LANGUAGE = "language"
const val SDK_INFO = "sdkInfo" const val SDK_BUILD_ID = "guruAnalyticsBuildId"
const val SDK_VERSION = "guruAnalyticsVersion"
const val GURU_SDK_VERSION = "gurusdkVersion"
} }
object Properties { object Properties {

View File

@ -30,7 +30,8 @@ abstract class GuruAnalytics {
mainProcess: String? = null, mainProcess: String? = null,
isEnableCronet: Boolean? = null, isEnableCronet: Boolean? = null,
uploadIpAddress: List<String>? = null, uploadIpAddress: List<String>? = null,
dnsMode: Int? = null dnsMode: Int? = null,
guruSdkVersion: String = ""
) )
abstract fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String) abstract fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String)
@ -95,7 +96,7 @@ abstract class GuruAnalytics {
} }
} }
class Builder(val context: Context) { class Builder(val context: Context, private val guruSdkVersion: String) {
private val analyticsInfo = AnalyticsInfo() private val analyticsInfo = AnalyticsInfo()
fun setBatchLimit(batchLimit: Int?) = apply { analyticsInfo.batchLimit = batchLimit } fun setBatchLimit(batchLimit: Int?) = apply { analyticsInfo.batchLimit = batchLimit }
@ -162,7 +163,8 @@ abstract class GuruAnalytics {
mainProcess, mainProcess,
isEnableCronet, isEnableCronet,
uploadIpAddress, uploadIpAddress,
dnsMode dnsMode,
guruSdkVersion
) )
} }
return INSTANCE return INSTANCE

View File

@ -60,5 +60,7 @@ class PreferencesManager private constructor(
var deviceId: String? by bind("device_id", "") var deviceId: String? by bind("device_id", "")
var adId: String? by bind("ad_id", "") var adId: String? by bind("ad_id", "")
var sessionDate: String? by bind("session_date", "")
var sessionCount: Int? by bind("session_count", 0)
} }

View File

@ -31,7 +31,7 @@ object GuruAnalyticsAudit {
var enabledCronet: Boolean = false var enabledCronet: Boolean = false
var eventDispatcherStarted: Boolean = false var eventDispatcherStarted: Boolean = false
var fgHelperInitialized: Boolean = false var fgHelperInitialized: Boolean = false
var connectionState: Boolean = false var networkAvailable: Boolean = false
// 整体的 // 整体的
var total: Int = 0 var total: Int = 0
@ -54,7 +54,7 @@ object GuruAnalyticsAudit {
dnsMode = dnsMode, dnsMode = dnsMode,
eventDispatcherStarted = eventDispatcherStarted, eventDispatcherStarted = eventDispatcherStarted,
fgHelperInitialized = fgHelperInitialized, fgHelperInitialized = fgHelperInitialized,
connectionState = connectionState, connectionState = networkAvailable,
total = total, total = total,
deleted = deleted, deleted = deleted,
uploaded = uploaded, uploaded = uploaded,

View File

@ -14,7 +14,7 @@ import java.util.*
object DeviceInfoStore { object DeviceInfoStore {
const val SDK_VERSION = "v1.1.0" var GURU_SDK_VERSION = ""
private val deviceInfoSubject: BehaviorSubject<Map<String, Any>> = private val deviceInfoSubject: BehaviorSubject<Map<String, Any>> =
BehaviorSubject.createDefault( BehaviorSubject.createDefault(
@ -41,7 +41,9 @@ object DeviceInfoStore {
map[Constants.DeviceInfo.SCREEN_W] = AndroidUtils.getWindowWidth(context) ?: 0 map[Constants.DeviceInfo.SCREEN_W] = AndroidUtils.getWindowWidth(context) ?: 0
map[Constants.DeviceInfo.OS_VERSION] = Build.VERSION.RELEASE map[Constants.DeviceInfo.OS_VERSION] = Build.VERSION.RELEASE
map[Constants.DeviceInfo.LANGUAGE] = Locale.getDefault().language map[Constants.DeviceInfo.LANGUAGE] = Locale.getDefault().language
map[Constants.DeviceInfo.SDK_INFO] = "${SDK_VERSION}-${BuildConfig.buildTs}" map[Constants.DeviceInfo.SDK_VERSION] = BuildConfig.guruAnalyticsSdkVersion
map[Constants.DeviceInfo.SDK_BUILD_ID] = BuildConfig.buildTs
map[Constants.DeviceInfo.GURU_SDK_VERSION] = GURU_SDK_VERSION
deviceInfoSubject.onNext(map) deviceInfoSubject.onNext(map)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_DEVICE_INFO, map) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_DEVICE_INFO, map)
Timber.tag("DeviceInfoStore").i("DeviceInfo: $map") Timber.tag("DeviceInfoStore").i("DeviceInfo: $map")

View File

@ -38,6 +38,7 @@ object EventInfoStore {
restoreIds(this) restoreIds(this)
GuruAnalyticsAudit.total = this.eventCountAll ?: 0 GuruAnalyticsAudit.total = this.eventCountAll ?: 0
GuruAnalyticsAudit.uploaded = this.eventCountUploaded ?: 0 GuruAnalyticsAudit.uploaded = this.eventCountUploaded ?: 0
GuruAnalyticsAudit.deleted = this.eventCountDeleted ?: 0
} }
} }
} }
@ -163,7 +164,10 @@ object EventInfoStore {
private val supplementEventParamsSubject: BehaviorSubject<Map<String, Any>> = private val supplementEventParamsSubject: BehaviorSubject<Map<String, Any>> =
BehaviorSubject.createDefault( BehaviorSubject.createDefault(
hashMapOf(Constants.Event.SCREEN to "main") hashMapOf(
Constants.Event.SCREEN to "main",
Constants.Event.SESSION_ID to SESSION.hashCode()
)
) )
private val idsSubject: BehaviorSubject<Map<String, String>> = private val idsSubject: BehaviorSubject<Map<String, String>> =
@ -184,7 +188,10 @@ object EventInfoStore {
} }
private var supplementEventParams: Map<String, Any> private var supplementEventParams: Map<String, Any>
get() = supplementEventParamsSubject.value ?: hashMapOf(Constants.Event.SCREEN to "main") get() = supplementEventParamsSubject.value ?: hashMapOf(
Constants.Event.SCREEN to "main",
Constants.Event.SESSION_ID to SESSION.hashCode()
)
set(value) { set(value) {
supplementEventParamsSubject.onNext(value) supplementEventParamsSubject.onNext(value)
} }

View File

@ -37,11 +37,13 @@ internal class AppLifecycleMonitor internal constructor(context: Context) {
when (event) { when (event) {
Lifecycle.Event.ON_START -> { Lifecycle.Event.ON_START -> {
Timber.d("${TAG}_ON_START") Timber.d("${TAG}_ON_START")
fgHelper.start()
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.LIFECYCLE_START) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.LIFECYCLE_START)
} }
Lifecycle.Event.ON_RESUME -> Timber.d("${TAG}_ON_RESUME") Lifecycle.Event.ON_RESUME -> {
Timber.d("${TAG}_ON_RESUME")
fgHelper.start()
}
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
Timber.d("${TAG}_ON_PAUSE") Timber.d("${TAG}_ON_PAUSE")
fgHelper.stop() fgHelper.stop()

View File

@ -1,69 +0,0 @@
package guru.core.analytics.impl
import android.os.SystemClock
import guru.core.analytics.data.db.GuruAnalyticsDatabase
import guru.core.analytics.data.model.AnalyticsOptions
import guru.core.analytics.data.model.EventItem
import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.data.store.EventInfoStore
import timber.log.Timber
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
class PendingEvent(
val item: EventItem,
val options: AnalyticsOptions
) {
val at = SystemClock.elapsedRealtime()
}
data object EventDispatcher : EventDeliver {
private val pendingEvents = ConcurrentLinkedQueue<PendingEvent>()
private val started = AtomicBoolean(false)
private fun dispatchPendingEvent() {
Timber.d("EventDispatcher dispatchPendingEvent ${pendingEvents.size}!")
if (pendingEvents.isNotEmpty()) {
while (true) {
val pendingEvent = pendingEvents.poll() ?: return
val event = EventInfoStore.deriveEvent(
pendingEvent.item,
priority = pendingEvent.options.priority,
elapsed = SystemClock.elapsedRealtime() - pendingEvent.at
)
GuruAnalyticsDatabase.getInstance().eventDao().addEvent(event)
}
}
}
fun start() {
if (started.compareAndSet(false, true)) {
Timber.d("EventDispatcher started!")
dispatchPendingEvent()
GuruAnalyticsAudit.eventDispatcherStarted = true
}
}
override fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
if (started.get()) {
Timber.d("EventDispatcher deliverEvent!")
dispatchPendingEvent()
val event = EventInfoStore.deriveEvent(item, priority = options.priority)
GuruAnalyticsDatabase.getInstance().eventDao().addEvent(event)
} else {
Timber.d("EventDispatcher deliverEvent pending!")
pendingEvents.offer(PendingEvent(item, options))
}
}
override fun deliverProperty(name: String, value: String) {
EventInfoStore.setUserProperty(name, value)
}
override fun removeProperties(keys: Set<String>) {
EventInfoStore.removeUserProperties(keys)
}
}

View File

@ -1,20 +1,15 @@
package guru.core.analytics.impl package guru.core.analytics.impl
import android.content.Context import android.content.Context
import android.net.Uri
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.SystemClock
import androidx.work.WorkManager import androidx.work.WorkManager
import guru.core.analytics.Constants import guru.core.analytics.Constants
import guru.core.analytics.GuruAnalytics import guru.core.analytics.GuruAnalytics
import guru.core.analytics.data.api.GuruRepository import guru.core.analytics.data.api.GuruRepository
import guru.core.analytics.data.api.ServiceLocator
import guru.core.analytics.data.db.GuruAnalyticsDatabase import guru.core.analytics.data.db.GuruAnalyticsDatabase
import guru.core.analytics.data.db.model.EventEntity import guru.core.analytics.data.db.model.EventEntity
import guru.core.analytics.data.db.model.EventPriority import guru.core.analytics.data.db.model.EventPriority
@ -44,6 +39,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max
internal class EventEngine internal constructor( internal class EventEngine internal constructor(
private val context: Context, private val context: Context,
@ -51,21 +47,21 @@ internal class EventEngine internal constructor(
private val uploadPeriodInSeconds: Long = DEFAULT_UPLOAD_PERIOD_IN_SECONDS, private val uploadPeriodInSeconds: Long = DEFAULT_UPLOAD_PERIOD_IN_SECONDS,
private val eventExpiredInDays: Int = DEFAULT_EVENT_EXPIRED_IN_DAYS, private val eventExpiredInDays: Int = DEFAULT_EVENT_EXPIRED_IN_DAYS,
private val guruRepository: GuruRepository private val guruRepository: GuruRepository
) : EventDeliver { ) {
private val preferencesManager by lazy { private val preferencesManager: PreferencesManager by lazy {
PreferencesManager.getInstance(context) PreferencesManager.getInstance(context)
} }
private val connectivity: Connectivity by lazy { private val connectivity: Connectivity by lazy {
Connectivity(context) Connectivity.of(context)
} }
private val lifecycleMonitor = AppLifecycleMonitor(context) private val lifecycleMonitor = AppLifecycleMonitor(context)
companion object { companion object {
private const val TAG = "UploadEvents" private const val TAG = "UploadEvents"
const val DEFAULT_UPLOAD_PERIOD_IN_SECONDS = 60L // 接口上传间隔时间 秒 const val DEFAULT_UPLOAD_PERIOD_IN_SECONDS = 45L // 接口上传间隔时间 秒
const val DEFAULT_BATCH_LIMIT = 25 // 一次上传event最多数量 const val DEFAULT_BATCH_LIMIT = 25 // 一次上传event最多数量
const val DEFAULT_EVENT_EXPIRED_IN_DAYS = 7 const val DEFAULT_EVENT_EXPIRED_IN_DAYS = 7
@ -80,7 +76,6 @@ internal class EventEngine internal constructor(
@Volatile @Volatile
var sessionActivated = false var sessionActivated = false
fun logDebug(message: String, vararg args: Any?) { fun logDebug(message: String, vararg args: Any?) {
if (GuruAnalytics.INSTANCE.isDebug()) { if (GuruAnalytics.INSTANCE.isDebug()) {
Timber.tag(TAG).d(message, args) Timber.tag(TAG).d(message, args)
@ -101,13 +96,10 @@ internal class EventEngine internal constructor(
} }
private var compositeDisposable: CompositeDisposable = CompositeDisposable() private var compositeDisposable: CompositeDisposable = CompositeDisposable()
private val pendingEventSubject: PublishSubject<Int> = PublishSubject.create()
private val forceTriggerSubject: PublishSubject<String> = PublishSubject.create()
internal val started = AtomicBoolean(false) internal val started = AtomicBoolean(false)
private var enableUpload = true private var enableUpload = true
private var latestValidActionTs = 0L private var latestValidActionTs = 0L
fun setEnableUpload(enable: Boolean) { fun setEnableUpload(enable: Boolean) {
enableUpload = enable enableUpload = enable
val extMap = mapOf("enable" to enable) val extMap = mapOf("enable" to enable)
@ -116,6 +108,7 @@ internal class EventEngine internal constructor(
fun start(startUploadDelay: Long?) { fun start(startUploadDelay: Long?) {
if (started.compareAndSet(false, true)) { if (started.compareAndSet(false, true)) {
AnchorAt.recordAt(AnchorAt.SESSION_START)
prepare() prepare()
connectivity.bind() connectivity.bind()
lifecycleMonitor.initialize() lifecycleMonitor.initialize()
@ -131,8 +124,7 @@ internal class EventEngine internal constructor(
logDebug("session started error!") logDebug("session started error!")
} }
scheduler.scheduleDirect({ scheduler.scheduleDirect({
EventDispatcher.start() EventSink.start()
logFirstOpen()
startWork() startWork()
val extMap = mapOf("startUploadDelayInSecond" to startUploadDelay) val extMap = mapOf("startUploadDelayInSecond" to startUploadDelay)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.STATE_START_WORK, extMap) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.STATE_START_WORK, extMap)
@ -141,16 +133,6 @@ internal class EventEngine internal constructor(
} }
} }
private fun logFirstOpen() {
if (preferencesManager.isFirstOpen == true) {
GuruAnalytics.INSTANCE.logEvent(
Constants.Event.FIRST_OPEN, options = AnalyticsOptions(EventPriority.EMERGENCE)
)
preferencesManager.isFirstOpen = false
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_FIRST_OPEN)
}
}
private fun logSessionActive() { private fun logSessionActive() {
@ -172,6 +154,7 @@ internal class EventEngine internal constructor(
logEvent(event) logEvent(event)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_SESSION_ACTIVE) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_SESSION_ACTIVE)
sessionActivated = true sessionActivated = true
}.onErrorReturn { }.onErrorReturn {
Timber.e("active error! $it") Timber.e("active error! $it")
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_SESSION_START_ERROR, it.message) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_SESSION_START_ERROR, it.message)
@ -185,7 +168,7 @@ internal class EventEngine internal constructor(
private fun dispatchActiveWorker() { private fun dispatchActiveWorker() {
Timber.e("dispatchActiveWorker...") Timber.e("dispatchActiveWorker...")
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) .setRequiredNetworkType(NetworkType.CONNECTED)
.build() .build()
val request = OneTimeWorkRequestBuilder<ActiveWorker>() val request = OneTimeWorkRequestBuilder<ActiveWorker>()
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES)
@ -209,7 +192,6 @@ internal class EventEngine internal constructor(
private fun startWork() { private fun startWork() {
Timber.d("startWork") Timber.d("startWork")
pollEvents() pollEvents()
forceUpload("startWork")
Timber.d("UploadEventDaemon started!!") Timber.d("UploadEventDaemon started!!")
} }
@ -236,19 +218,25 @@ internal class EventEngine internal constructor(
private fun pollEvents() { private fun pollEvents() {
logDebug("pollEvents()!! $uploadPeriodInSeconds $batchLimit") logDebug("pollEvents()!! $uploadPeriodInSeconds $batchLimit")
val periodFlowable = Flowable.interval(5, uploadPeriodInSeconds, TimeUnit.SECONDS).onBackpressureDrop() val periodFlowable = Flowable.interval(5, uploadPeriodInSeconds, TimeUnit.SECONDS).onBackpressureDrop()
val forceFlowable = forceTriggerSubject.toFlowable(BackpressureStrategy.DROP).map { scene -> val forceFlowable = EventSink.forceFlowable.map { scene ->
logDebug("force trigger: $scene") logDebug("Force Trigger: $scene")
} }
val networkFlowable = connectivity.networkAvailableFlowable compositeDisposable.add(
compositeDisposable.add(Flowable.combineLatest( Flowable.combineLatest(
periodFlowable, forceFlowable, networkFlowable, periodFlowable, forceFlowable,
) { _, _, available -> ) { _, _ ->
GuruAnalyticsAudit.connectionState = available val available = try {
val uploadedRate = GuruAnalyticsAudit.uploaded / GuruAnalyticsAudit.total.toFloat() connectivity.isNetworkAvailable()
val ignoreAvailable = !GuruAnalyticsAudit.uploadReady && uploadedRate < 0.6 } catch (throwable: Throwable) {
logDebug("enableUpload:$enableUpload && (network:$available || ignoreAvailable: (${ignoreAvailable})[uploaded(${GuruAnalyticsAudit.uploaded}) / total(${GuruAnalyticsAudit.total}) = $uploadedRate])") logInfo("networkAvailable error: $throwable")
return@combineLatest enableUpload && (available || ignoreAvailable) GuruAnalyticsAudit.networkAvailable
}.flatMap { uploadEvents(100) }.subscribe() }
GuruAnalyticsAudit.networkAvailable = available
val uploadedRate = GuruAnalyticsAudit.sessionUploaded / GuruAnalyticsAudit.sessionTotal.toFloat()
val ignoreAvailable = !GuruAnalyticsAudit.uploadReady && uploadedRate < 0.6
logDebug("enableUpload:$enableUpload && (network:$available || ignoreAvailable: (${ignoreAvailable})[uploaded(${GuruAnalyticsAudit.uploaded}) / total(${GuruAnalyticsAudit.total}) = $uploadedRate])")
return@combineLatest enableUpload && available
}.flatMap { uploadEvents(256) }.subscribe()
) )
} }
@ -286,14 +274,16 @@ internal class EventEngine internal constructor(
val eventDao = GuruAnalyticsDatabase.getInstance().eventDao() val eventDao = GuruAnalyticsDatabase.getInstance().eventDao()
GuruAnalyticsAudit.uploadReady = true GuruAnalyticsAudit.uploadReady = true
logDebug("uploadEvents: $count") logDebug("uploadEvents: $count")
return Flowable.just(count).map { eventDao.loadAndMarkUploadEvents(it) }.onErrorReturn { return Flowable.just(count)
try { .map { eventDao.loadAndMarkUploadEvents(it) }
eventDao.loadAndMarkUploadEvents(batchLimit) .onErrorReturn {
} catch (throwable: Throwable) { try {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_LOAD_MARK, it.message) eventDao.loadAndMarkUploadEvents(batchLimit)
emptyList() } catch (throwable: Throwable) {
} EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_LOAD_MARK, it.message)
}.filter { it.isNotEmpty() }.subscribeOn(dbScheduler).observeOn(scheduler).concatMap { splitEntities(it) } emptyList()
}
}.filter { it.isNotEmpty() }.subscribeOn(dbScheduler).observeOn(scheduler).concatMap { splitEntities(it) }
.flatMapSingle { uploadEventsInternal(it) }.filter { it.isNotEmpty() }.doOnNext { .flatMapSingle { uploadEventsInternal(it) }.filter { it.isNotEmpty() }.doOnNext {
eventDao.deleteEvents(it) eventDao.deleteEvents(it)
if (GuruAnalytics.INSTANCE.isDebug()) { if (GuruAnalytics.INSTANCE.isDebug()) {
@ -306,76 +296,42 @@ internal class EventEngine internal constructor(
private fun uploadEventsInternal(entities: List<EventEntity>): Single<List<EventEntity>> { private fun uploadEventsInternal(entities: List<EventEntity>): Single<List<EventEntity>> {
val param = ApiParamUtils.generateApiParam(entities) val param = ApiParamUtils.generateApiParam(entities)
return guruRepository.uploadEvents(param).map { true }.observeOn(dbScheduler).doOnError { return guruRepository.uploadEvents(param)
.map { true }
.observeOn(dbScheduler)
.doOnSuccess {
// 记录上传成功的数量
val eventCountUploaded = preferencesManager.eventCountUploaded ?: 0
val uploaded = eventCountUploaded + entities.size
preferencesManager.eventCountUploaded = uploaded
GuruAnalyticsAudit.uploaded = uploaded
GuruAnalyticsAudit.sessionUploaded += entities.size
}.doOnSuccess { val extMap = mapOf(
// 记录上传成功的数量 "count" to entities.size,
val eventCountUploaded = preferencesManager.eventCountUploaded ?: 0 "eventNames" to entities.joinToString(",") { it.event },
val uploaded = eventCountUploaded + entities.size "allUploadedCount" to preferencesManager.eventCountUploaded,
preferencesManager.eventCountUploaded = uploaded )
GuruAnalyticsAudit.uploaded = uploaded logDebug("uploadEvents success: $extMap")
GuruAnalyticsAudit.sessionUploaded += entities.size EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_SUCCESS, extMap)
latestValidActionTs = android.os.SystemClock.elapsedRealtime()
val extMap = mapOf( }.onErrorReturn {
"count" to entities.size, GuruAnalyticsDatabase.getInstance().eventDao().updateEventDefault(entities)
"eventNames" to entities.joinToString(",") { it.event }, EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_FAIL, it.message)
"allUploadedCount" to preferencesManager.eventCountUploaded, // val elapsed = android.os.SystemClock.elapsedRealtime()
) // val uploadedRate = GuruAnalyticsAudit.sessionUploaded / max(GuruAnalyticsAudit.sessionTotal, 1)
logDebug("uploadEvents success: $extMap") // val isActiveNetworkMetered = connectivity.isActiveNetworkMetered
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_SUCCESS, extMap) // val exceededValidActionGap = elapsed - latestValidActionTs > SESSION_ACTIVE_INTERVAL
latestValidActionTs = android.os.SystemClock.elapsedRealtime() // logInfo("uploadEvent error $it sessionActivated:$sessionActivated uploadedRate:$uploadedRate isActiveNetworkMetered:$isActiveNetworkMetered gap:${(elapsed - latestValidActionTs) / 1000}s")
}.onErrorReturn { // /// 如果没有激活并且当前是计费网络并且距离上次上传成功时间超过15分钟激活worker
GuruAnalyticsDatabase.getInstance().eventDao().updateEventDefault(entities) // /// 因为这个 worker 会在非计费网络下尝试执行
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_FAIL, it.message) // if (!sessionActivated && uploadedRate < 0.6 && isActiveNetworkMetered && exceededValidActionGap) {
val elapsed = android.os.SystemClock.elapsedRealtime() // logInfo("Metered Network Active! But upload event failed rate > 0.4! So dispatch ActiveWorker")
val uploadedRate = GuruAnalyticsAudit.sessionUploaded / GuruAnalyticsAudit.sessionTotal //// dispatchActiveWorker()
val isActiveNetworkMetered = connectivity.isActiveNetworkMetered // latestValidActionTs = elapsed
val exceededValidActionGap = elapsed - latestValidActionTs > SESSION_ACTIVE_INTERVAL // }
logInfo("uploadEvent error $it sessionActivated:$sessionActivated uploadedRate:$uploadedRate isActiveNetworkMetered:$isActiveNetworkMetered gap:${(elapsed - latestValidActionTs) / 1000}s") false
/// 如果没有激活并且当前是计费网络并且距离上次上传成功时间超过15分钟激活worker }.map { if (it) entities else emptyList() }
/// 因为这个 worker 会在非计费网络下尝试执行
if (!sessionActivated && uploadedRate < 0.6 && isActiveNetworkMetered && exceededValidActionGap) {
logInfo("Metered Network Active! But upload event failed rate > 0.4! So dispatch ActiveWorker")
dispatchActiveWorker()
latestValidActionTs = elapsed
}
false
}.map { if (it) entities else emptyList() }
}
override fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
// 记录收到的事件数量
val delivered = increaseEventCount()
pendingEventSubject.onNext(1)
if (options.priority == EventPriority.EMERGENCE) {
logInfo("EMERGENCE: ${item.eventName} forceUpload")
forceUpload("EMERGENCE")
} else if (delivered and 0x1F == 0x1F) {
logInfo("Already delivered $delivered events!! forceUpload")
forceUpload("DELIVERED")
}
}
private fun increaseEventCount(): Int {
val eventCountAll = preferencesManager.eventCountAll ?: 0
val total = eventCountAll + 1
preferencesManager.eventCountAll = total
GuruAnalyticsAudit.total = total
GuruAnalyticsAudit.sessionTotal++
return total
}
override fun deliverProperty(name: String, value: String) {
}
override fun removeProperties(keys: Set<String>) {
}
internal fun forceUpload(scene: String = "unknown") {
forceTriggerSubject.onNext(scene)
} }
fun getEventsStatics(): EventStatistic { fun getEventsStatics(): EventStatistic {

View File

@ -0,0 +1,233 @@
package guru.core.analytics.impl
import android.annotation.SuppressLint
import android.content.Context
import android.os.SystemClock
import guru.core.analytics.Constants
import guru.core.analytics.data.db.GuruAnalyticsDatabase
import guru.core.analytics.data.db.model.EventPriority
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.data.model.AnalyticsOptions
import guru.core.analytics.data.model.EventItem
import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.data.store.EventInfoStore
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
import guru.core.analytics.utils.Connectivity
import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
import io.reactivex.subjects.PublishSubject
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
class PendingEvent(
val item: EventItem,
val options: AnalyticsOptions
) {
val at = SystemClock.elapsedRealtime()
}
data object AnchorAt {
private const val PREDICTED_LAUNCHED = "PREDICTED_LAUNCHED"
const val SDK_INIT = "SDK_INIT"
const val SDK_INIT_COMPLETED = "SDK_INIT_COMPLETED"
const val SESSION_START = "SESSION_START"
@JvmStatic
private val anchorAtMap = mutableMapOf(
PREDICTED_LAUNCHED to (SystemClock.elapsedRealtime() - 2000)
)
fun recordAt(name: String) {
anchorAtMap[name] = SystemClock.elapsedRealtime()
}
fun elapsedAt(name: String): Long {
val current = SystemClock.elapsedRealtime()
return current - (anchorAtMap[name] ?: current)
}
fun diffAt(begin: String, end: String): Long {
val current = SystemClock.elapsedRealtime()
return (anchorAtMap[end] ?: current) - (anchorAtMap[begin] ?: current)
}
}
data object EventSink {
private val forceTriggerSubject: PublishSubject<String> = PublishSubject.create()
private lateinit var preferencesManager: PreferencesManager
@SuppressLint("StaticFieldLeak")
private lateinit var connectivity: Connectivity
private val pendingEvents = ConcurrentLinkedDeque<PendingEvent>()
private val started = AtomicBoolean(false)
private val deliverExecutor = Executors.newSingleThreadExecutor()
private val latestNetworkAvailable: AtomicBoolean = AtomicBoolean(false)
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyyMMdd", Locale.US)
val forceFlowable: Flowable<String>
get() = forceTriggerSubject.toFlowable(BackpressureStrategy.DROP)
@SuppressLint("CheckResult")
fun initialize(context: Context, host: String) {
this.preferencesManager = PreferencesManager.getInstance(context)
this.connectivity = Connectivity.of(context)
logSdkInit(host)
connectivity.networkAvailableFlowable.subscribe {
val latest = latestNetworkAvailable.getAndSet(it)
if (latest != it) {
Timber.d("Network available: $it")
forceUpload("NETWORK_AVAILABLE")
}
}
}
private fun dispatchPendingEvent() {
Timber.d("EventDispatcher dispatchPendingEvent ${pendingEvents.size}!")
if (pendingEvents.isNotEmpty()) {
while (true) {
val pendingEvent = pendingEvents.poll() ?: return
dispatch(pendingEvent.item, pendingEvent.options, SystemClock.elapsedRealtime() - pendingEvent.at)
}
}
}
fun start() {
deliver {
if (started.compareAndSet(false, true)) {
Timber.d("EventDispatcher started!")
logSessionStart()
logFirstOpen()
logSdkInitCompleted()
dispatchPendingEvent()
GuruAnalyticsAudit.eventDispatcherStarted = true
}
}
}
private fun increaseEventCount(): Int {
val eventCountAll = preferencesManager.eventCountAll ?: 0
val total = eventCountAll + 1
preferencesManager.eventCountAll = total
GuruAnalyticsAudit.total = total
GuruAnalyticsAudit.sessionTotal++
return total
}
fun forceUpload(scene: String = "unknown") {
forceTriggerSubject.onNext(scene)
}
fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
deliver {
if (started.get()) {
Timber.d("EventDispatcher deliverEvent!")
dispatch(item, options)
} else {
Timber.d("EventDispatcher deliverEvent pending!")
pendingEvents.offer(PendingEvent(item, options))
}
}
}
private fun dispatch(item: EventItem, options: AnalyticsOptions, elapsed: Long = 0L) {
val event = EventInfoStore.deriveEvent(item, priority = options.priority, elapsed = elapsed)
GuruAnalyticsDatabase.getInstance().eventDao().addEvent(event)
val delivered = increaseEventCount()
if (options.priority == EventPriority.EMERGENCE) {
EventEngine.logInfo("EMERGENCE: ${item.eventName} forceUpload")
forceUpload("EMERGENCE")
} else if (delivered and 0x1F == 0x1F) {
EventEngine.logInfo("Already delivered $delivered events!! forceUpload")
forceUpload("DELIVERED")
}
}
private fun deliver(runnable: Runnable) {
deliverExecutor.execute(runnable)
}
fun deliverProperty(name: String, value: String) {
deliver {
EventInfoStore.setUserProperty(name, value)
}
}
fun removeProperties(keys: Set<String>) {
deliver {
EventInfoStore.removeUserProperties(keys)
}
}
private fun logFirstOpen() {
if (preferencesManager.isFirstOpen == true) {
dispatch(
EventItem(Constants.Event.FIRST_OPEN),
AnalyticsOptions(EventPriority.EMERGENCE),
elapsed = AnchorAt.elapsedAt(AnchorAt.SESSION_START)
)
preferencesManager.isFirstOpen = false
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_FIRST_OPEN)
}
}
private fun logSessionStart() {
val sessionDate = preferencesManager.sessionDate ?: ""
val now = Date()
val sessionCount = if (sessionDate != dateFormat.format(now)) {
preferencesManager.sessionDate = dateFormat.format(now)
1
} else {
(preferencesManager.sessionCount ?: 0) + 1
}
preferencesManager.sessionCount = sessionCount
dispatch(
EventItem(Constants.Event.SESSION_START, params = mapOf("session_number" to sessionCount)),
AnalyticsOptions(EventPriority.EMERGENCE),
elapsed = AnchorAt.elapsedAt(AnchorAt.SESSION_START)
)
}
private fun logSdkInit(host: String) {
deliver {
dispatch(
EventItem(
Constants.Event.SDK_INIT, params = mapOf(
"host" to 1,
"total_events" to GuruAnalyticsAudit.total,
"deleted_events" to GuruAnalyticsAudit.deleted,
"uploaded_events" to GuruAnalyticsAudit.uploaded
)
), AnalyticsOptions(EventPriority.EMERGENCE), elapsed = AnchorAt.elapsedAt(AnchorAt.SDK_INIT)
)
}
}
private fun logSdkInitCompleted() {
dispatch(
EventItem(
Constants.Event.SDK_INIT_COMPLETE, params = mapOf(
"duration" to AnchorAt.diffAt(AnchorAt.SDK_INIT, AnchorAt.SDK_INIT_COMPLETED),
)
),
AnalyticsOptions(EventPriority.EMERGENCE),
elapsed = AnchorAt.elapsedAt(AnchorAt.SDK_INIT_COMPLETED)
)
}
}

View File

@ -1,136 +0,0 @@
//package guru.core.analytics.impl
//
//import android.content.Context
//import android.os.SystemClock
//import guru.core.analytics.Constants
//import guru.core.analytics.GuruAnalytics
//import guru.core.analytics.data.db.model.EventEntity
//import guru.core.analytics.data.db.model.EventPriority
//import guru.core.analytics.data.local.PreferencesManager
//import guru.core.analytics.data.model.EventItem
//import guru.core.analytics.data.store.EventInfoStore
//import guru.core.analytics.handler.AnalyticsCode
//import guru.core.analytics.handler.EventHandler
//import io.reactivex.BackpressureStrategy
//import io.reactivex.Flowable
//import io.reactivex.disposables.CompositeDisposable
//import io.reactivex.schedulers.Schedulers
//import io.reactivex.subjects.BehaviorSubject
//import timber.log.Timber
//import java.util.concurrent.Executors
//import java.util.concurrent.TimeUnit
//import java.util.concurrent.atomic.AtomicLong
//
//class FgEventHelper private constructor() {
//
// companion object {
// private const val TAG = "FgEventHelper"
// private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor())
// private const val DEFAULT_FG_UPLOAD_INTERVAL_SECOND = 30L
// private const val MINIMUM_FG_UPLOAD_INTERVAL_SECOND = 15L
// private const val MAXIMUM_FG_UPLOAD_INTERVAL_SECOND = 60 * 60 * 1000L
//
// @Volatile
// private var INSTANCE: FgEventHelper? = null
//
// fun getInstance(): FgEventHelper = INSTANCE ?: synchronized(this) {
// INSTANCE ?: FgEventHelper().also { INSTANCE = it }
// }
// }
//
// private var compositeDisposable: CompositeDisposable = CompositeDisposable()
// private val lifecycleSubject: BehaviorSubject<Boolean> = BehaviorSubject.create()
// private val lastTime = AtomicLong(0L)
// private var fgDuration = AtomicLong(0L)
// private fun updateFgDuration(duration: Long) {
// fgDuration.set(duration)
// preferencesManager?.setTotalDurationFgEvent(duration)
// }
//
// private var preferencesManager: PreferencesManager? = null
//
//
// fun start(context: Context, fgEventPeriodInSeconds: Long? = null) {
// preferencesManager = PreferencesManager.getInstance(context)
// AppLifecycleMonitor.getInstance().addLifecycleMonitor {
// lifecycleSubject.onNext(it)
// Timber.tag(TAG).d("start addLifecycleMonitor isVisible:$it")
// }
//
// val periodInSeconds = fgEventPeriodInSeconds ?: -1L
// val interval =
// if (periodInSeconds < 0) DEFAULT_FG_UPLOAD_INTERVAL_SECOND else if (periodInSeconds < MINIMUM_FG_UPLOAD_INTERVAL_SECOND) MINIMUM_FG_UPLOAD_INTERVAL_SECOND else periodInSeconds
//
// val fgIntervalFlow = Flowable.interval(0, interval, TimeUnit.SECONDS)
// compositeDisposable.add(
// Flowable.combineLatest(
// fgIntervalFlow, lifecycleSubject.toFlowable(BackpressureStrategy.DROP)
// ) { _, isVisible -> isVisible }
// .doOnSubscribe { logFirstFgEvent() }
// .subscribeOn(scheduler)
// .observeOn(scheduler)
// .map { getFgDuration(MAXIMUM_FG_UPLOAD_INTERVAL_SECOND) }
// .filter { it > 0L }
// .subscribe({ duration -> logFgEvent(duration) }, Timber::e)
// )
// Timber.tag(TAG).d("start interval:$interval ${compositeDisposable.size()}")
// }
//
//
// private fun logFirstFgEvent() {
// if (preferencesManager == null) return
// val cachedDuration = 1L.coerceAtLeast(preferencesManager!!.getTotalDurationFgEvent())
// logFgEvent(cachedDuration)
// updateFgDuration(0L)
// Timber.tag(TAG).d("logFirstFgEvent $cachedDuration")
// }
//
// private fun logFgEvent(duration: Long) {
// if (duration <= 0L) return
// val params = mutableMapOf<String, Any>()
// params[Constants.Event.DURATION] = duration
// GuruAnalytics.INSTANCE.logEvent(Constants.Event.FG, parameters = params)
// val extMap = mapOf("duration" to duration)
// EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_FG, extMap)
// }
//
// fun getFgEvent(limit: Long = 0L): EventEntity? {
// val duration = getFgDuration(limit)
// return if (duration > 0L) {
// val extMap = mapOf("duration" to duration)
// EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_FG, extMap)
// EventInfoStore.deriveEvent(
// EventItem(
// Constants.Event.FG, params = mapOf(Pair(Constants.Event.DURATION, duration))
// ),
// priority = EventPriority.EMERGENCE,
// )
// } else {
// null
// }
// }
//
// @Synchronized
// fun getFgDuration(limit: Long = 0L): Long {
// val lastTimeMillions = lastTime.get()
// val currentTimeMillions = SystemClock.elapsedRealtime()
// val isVisible = lifecycleSubject.value ?: false
// lastTime.set(if (isVisible) currentTimeMillions else 0L)
// val totalDuration = if (lastTimeMillions > 0L) {
// fgDuration.get() + currentTimeMillions - lastTimeMillions
// } else {
// fgDuration.get()
// }
// Timber.tag(TAG).d("getFgDuration limit:$limit isVisible:$isVisible totalDuration:$totalDuration")
// return if (totalDuration > 0L.coerceAtLeast(limit)) {
// updateFgDuration(0L)
// totalDuration
// } else {
// if (lastTimeMillions > 0) {
// updateFgDuration(totalDuration)
// }
// 0L
// }
// }
//
//}

View File

@ -4,7 +4,9 @@ import android.content.Context
import android.os.SystemClock import android.os.SystemClock
import guru.core.analytics.Constants import guru.core.analytics.Constants
import guru.core.analytics.GuruAnalytics import guru.core.analytics.GuruAnalytics
import guru.core.analytics.data.db.model.EventPriority
import guru.core.analytics.data.local.PreferencesManager import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.data.model.AnalyticsOptions
import guru.core.analytics.data.model.GuruAnalyticsAudit import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.handler.AnalyticsCode import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler import guru.core.analytics.handler.EventHandler
@ -16,20 +18,17 @@ import java.lang.Math.max
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
internal class FgHelper(context: Context) { internal class FgHelper(context: Context) {
companion object { companion object {
private const val FG_RECORD_INTERVAL_SECOND = 5L private const val FG_RECORD_INTERVAL_SECOND = 5L
private const val FG_REPORT_INTERVAL_MILLIS = 40 * 1000L private const val FG_REPORT_INTERVAL_MILLIS = 60 * 1000L
private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor()) private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor())
} }
@Volatile private var latestActiveAt = AtomicLong(SystemClock.elapsedRealtime())
private var latestRecordAt = SystemClock.elapsedRealtime()
@Volatile
private var currentDuration = 0L
private var disposable: Disposable? = null private var disposable: Disposable? = null
private val initialized = AtomicBoolean(false) private val initialized = AtomicBoolean(false)
@ -37,39 +36,45 @@ internal class FgHelper(context: Context) {
PreferencesManager.getInstance(context) PreferencesManager.getInstance(context)
} }
private val preferenceEditor by lazy {
preferencesManager.getSharedPreferencesDirectly().edit()
}
internal fun ensureInitialized() { internal fun ensureInitialized() {
if (initialized.compareAndSet(false, true)) { if (initialized.compareAndSet(false, true)) {
currentDuration = preferencesManager.getTotalDurationFgEvent() val elapsedAt = SystemClock.elapsedRealtime()
refresh(force = true) val latestElapsedAt = latestActiveAt.getAndSet(elapsedAt)
val duration = kotlin.math.max(1, elapsedAt - latestElapsedAt)
val legacyDuration = preferencesManager.getTotalDurationFgEvent() + duration
preferencesManager.setTotalDurationFgEvent(0L)
if (legacyDuration > 0L) {
logUserEngagement(legacyDuration)
}
} }
} }
private fun refresh(force: Boolean = false): Long { private fun refresh() {
val now = SystemClock.elapsedRealtime() val elapsedAt = SystemClock.elapsedRealtime()
val duration = now - latestRecordAt val duration = elapsedAt - latestActiveAt.get()
val fgDuration = currentDuration + duration Timber.tag("FgHelper").d("refresh: $duration")
latestRecordAt = now if (duration < FG_REPORT_INTERVAL_MILLIS) {
currentDuration = if (force || fgDuration > FG_REPORT_INTERVAL_MILLIS) { preferencesManager.setTotalDurationFgEvent(duration)
logFgEvent(fgDuration.coerceAtLeast(1))
0L
} else { } else {
fgDuration userEngagement(elapsedAt)
} }
preferenceEditor.putLong(PreferencesManager.KEY_TOTAL_DURATION_FG_EVENT, currentDuration)
.commit()
return currentDuration
} }
fun start() { fun start() {
userActive()
}
fun stop() {
userInactive()
}
private fun userActive() {
ensureInitialized() ensureInitialized()
latestRecordAt = SystemClock.elapsedRealtime() latestActiveAt.set(SystemClock.elapsedRealtime())
disposable?.dispose() disposable?.dispose()
disposable = Flowable.interval(0, FG_RECORD_INTERVAL_SECOND, TimeUnit.SECONDS) disposable = Flowable.interval(FG_RECORD_INTERVAL_SECOND, FG_RECORD_INTERVAL_SECOND, TimeUnit.SECONDS).onBackpressureDrop()
.subscribeOn(scheduler) .subscribeOn(scheduler)
.subscribe({ .subscribe({
refresh() refresh()
@ -79,17 +84,32 @@ internal class FgHelper(context: Context) {
GuruAnalyticsAudit.fgHelperInitialized = true GuruAnalyticsAudit.fgHelperInitialized = true
} }
fun stop() { private fun userInactive() {
userEngagement(SystemClock.elapsedRealtime())
disposable?.dispose() disposable?.dispose()
refresh() disposable = null
} }
private fun logFgEvent(duration: Long) { private fun isActive() = disposable != null
if (duration <= 0L) return
val params = mutableMapOf<String, Any>() private fun userEngagement(elapsedAt: Long) {
params[Constants.Event.DURATION] = duration if (!isActive()) {
GuruAnalytics.INSTANCE.logEvent(Constants.Event.FG, parameters = params) Timber.tag("FgHelper").w("inactive fg helper")
val extMap = mapOf("duration" to duration) return
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_FG, extMap) }
val latestElapsedAt = latestActiveAt.getAndSet(elapsedAt)
if (elapsedAt > latestElapsedAt) {
logUserEngagement(elapsedAt - latestElapsedAt)
preferencesManager.setTotalDurationFgEvent(0L)
}
}
private fun logUserEngagement(duration: Long) {
Timber.tag("FgHelper").w("[GURU ENGAGEMENT] duration: $duration")
val params = mapOf(Constants.Event.DURATION to duration)
GuruAnalytics.INSTANCE.logEvent(Constants.Event.FG,
parameters = params,
options = AnalyticsOptions(priority = EventPriority.EMERGENCE))
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_FG, params)
} }
} }

View File

@ -2,7 +2,6 @@ package guru.core.analytics.impl
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.transition.Scene
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import androidx.work.* import androidx.work.*
import guru.core.analytics.Constants import guru.core.analytics.Constants
@ -52,6 +51,11 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
private val initialized = AtomicBoolean(false) private val initialized = AtomicBoolean(false)
private val isMainProcess = AtomicBoolean(true)
// private val = System.currentTimeMillis()
override fun isDebug(): Boolean = debugMode override fun isDebug(): Boolean = debugMode
override fun setDebug(debug: Boolean) { override fun setDebug(debug: Boolean) {
@ -69,6 +73,7 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
Timber.d("setUploadEventBaseUrl:$updateEventBaseUrl") Timber.d("setUploadEventBaseUrl:$updateEventBaseUrl")
} }
@RequiresPermission(allOf = ["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"]) @RequiresPermission(allOf = ["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"])
override fun initialize( override fun initialize(
context: Context, context: Context,
@ -87,7 +92,8 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
mainProcess: String?, mainProcess: String?,
isEnableCronet: Boolean?, isEnableCronet: Boolean?,
uploadIpAddress: List<String>?, uploadIpAddress: List<String>?,
dnsMode: Int? dnsMode: Int?,
guruSdkVersion: String
) { ) {
if (initialized.compareAndSet(false, true)) { if (initialized.compareAndSet(false, true)) {
val debugApp = SystemProperties.read("debug.guru.analytics.app") val debugApp = SystemProperties.read("debug.guru.analytics.app")
@ -100,19 +106,27 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
} catch (_: Throwable) { } catch (_: Throwable) {
} }
} }
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_1) val currentProcessName = AndroidUtils.getProcessName(context)
if (currentProcessName != context.packageName) {
isMainProcess.set(false)
Timber.d("initialize $currentProcessName is not main process!")
return
}
AnchorAt.recordAt(AnchorAt.SDK_INIT)
isMainProcess.set(true)
DeviceInfoStore.GURU_SDK_VERSION = guruSdkVersion
EventInfoStore.initialize(context) EventInfoStore.initialize(context)
delivers.add(EventDispatcher)
eventHandlerCallback?.let { EventHandler.INSTANCE.addEventHandler(it) } eventHandlerCallback?.let { EventHandler.INSTANCE.addEventHandler(it) }
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_2) // EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_2)
val debugUrl = SystemProperties.read("debug.guru.analytics.url") val debugUrl = SystemProperties.read("debug.guru.analytics.url")
debugMode = forceDebug || debug debugMode = forceDebug || debug
Timber.d("[$internalVersion]initialize batchLimit:$batchLimit uploadPeriodInSecond:$uploadPeriodInSeconds startUploadDelayInSecond:$startUploadDelayInSecond eventExpiredInDays:$eventExpiredInDays debug:$debugMode debugUrl:$debugUrl uploadIpAddress:$uploadIpAddress dnsMode:$dnsMode") Timber.d("[$internalVersion] $currentProcessName initialize batchLimit:$batchLimit uploadPeriodInSecond:$uploadPeriodInSeconds startUploadDelayInSecond:$startUploadDelayInSecond eventExpiredInDays:$eventExpiredInDays debug:$debugMode debugUrl:$debugUrl uploadIpAddress:$uploadIpAddress dnsMode:$dnsMode guruSdkVersion:$guruSdkVersion")
GuruAnalyticsDatabase.initialize(context.applicationContext) GuruAnalyticsDatabase.initialize(context.applicationContext)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_3) // EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_3)
val baseUrl = if (forceDebug && debugUrl.isNotEmpty()) debugUrl else (uploadEventBaseUrl ?: AnalyticsApiHost.BASE_URL) val baseUrl = if (forceDebug && debugUrl.isNotEmpty()) debugUrl else (uploadEventBaseUrl ?: AnalyticsApiHost.BASE_URL)
@ -123,7 +137,7 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
ServiceLocator.setUploadIpAddress(uploadIpAddress) ServiceLocator.setUploadIpAddress(uploadIpAddress)
ServiceLocator.setDnsMode(dnsMode ?: 0) ServiceLocator.setDnsMode(dnsMode ?: 0)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_4) // EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_4)
if (isEnableCronet == true) { if (isEnableCronet == true) {
GuruAnalyticsAudit.enabledCronet = true GuruAnalyticsAudit.enabledCronet = true
ServiceLocator.setCronet(true) ServiceLocator.setCronet(true)
@ -135,7 +149,7 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
} }
} }
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_5) // EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_5)
var uploadEventBaseUri: Uri? = null var uploadEventBaseUri: Uri? = null
var hostname: String? = "" var hostname: String? = ""
@ -147,9 +161,10 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
} }
hostname = uploadEventBaseUri.host hostname = uploadEventBaseUri.host
} }
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_6) // EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_6)
ServiceLocator.preloadDns(hostname) ServiceLocator.preloadDns(hostname)
EventSink.initialize(context, hostname ?: AnalyticsApiHost.BASE_URL)
engine = EventEngine( engine = EventEngine(
context, context,
@ -161,28 +176,17 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
), ),
guruRepository = ServiceLocator.provideGuruRepository(context, baseUri = uploadEventBaseUri) /// 此处需要配合 ServiceLocator的重构进行修改 guruRepository = ServiceLocator.provideGuruRepository(context, baseUri = uploadEventBaseUri) /// 此处需要配合 ServiceLocator的重构进行修改
).apply { ).apply {
delivers.add(this)
start(startUploadDelayInSecond) start(startUploadDelayInSecond)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_7) // EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_7)
} }
if (isInitPeriodicWork) { if (isInitPeriodicWork) {
val process = AndroidUtils.getProcessName(context) try {
Timber.d("initialize ${mainProcess == process} currentProcess:$process mainProcess:$mainProcess") initAnalyticsPeriodic(context)
if (mainProcess.isNullOrBlank() || mainProcess == process) { } catch (throwable: Throwable) {
try { Timber.d("init worker error!")
initAnalyticsPeriodic(context)
} catch (throwable: Throwable) {
Timber.d("init worker error!")
}
} else {
if (!process.isNullOrBlank()) {
val params = mutableMapOf<String, Any>()
params[Constants.Event.PROCESS] = process
INSTANCE.logEvent(Constants.Event.ERROR_PROCESS, parameters = params)
}
} }
} }
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_8) // EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_8)
val extMap = mapOf( val extMap = mapOf(
"version_code" to internalVersion, "version_code" to internalVersion,
@ -203,8 +207,9 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
if (uploadIpAddress != null) { if (uploadIpAddress != null) {
PreferencesManager.getInstance(context).uploadIpAddressList = uploadIpAddress.joinToString("|") PreferencesManager.getInstance(context).uploadIpAddressList = uploadIpAddress.joinToString("|")
} }
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_9) // EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_9)
GuruAnalyticsAudit.initialized = true GuruAnalyticsAudit.initialized = true
AnchorAt.recordAt(AnchorAt.SDK_INIT_COMPLETED)
} }
} }
@ -329,19 +334,35 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
} }
override fun addEventHandler(listener: (Int, String?) -> Unit) { override fun addEventHandler(listener: (Int, String?) -> Unit) {
EventHandler.INSTANCE.addEventHandler(listener) if (isMainProcess.get()) {
EventHandler.INSTANCE.addEventHandler(listener)
} else {
Timber.w("addEventHandler error! not in main process")
}
} }
override fun removeEventHandler(listener: (Int, String?) -> Unit) { override fun removeEventHandler(listener: (Int, String?) -> Unit) {
EventHandler.INSTANCE.removeEventHandler(listener) if (isMainProcess.get()) {
EventHandler.INSTANCE.removeEventHandler(listener)
} else {
Timber.w("removeEventHandler error! not in main process")
}
} }
override fun removeUserProperty(key: String) { override fun removeUserProperty(key: String) {
removeProperties(setOf(key)) if (isMainProcess.get()) {
removeProperties(setOf(key))
} else {
Timber.w("removeUserProperty($key) error! not in main process")
}
} }
override fun removeUserProperties(keys: Set<String>) { override fun removeUserProperties(keys: Set<String>) {
removeProperties(keys) if (isMainProcess.get()) {
removeProperties(keys)
} else {
Timber.w("removeUserProperties error! not in main process")
}
} }
override fun getUserProperties(callback: (Map<String, String>) -> Unit) { override fun getUserProperties(callback: (Map<String, String>) -> Unit) {
@ -355,7 +376,12 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
} }
override fun setEnableUpload(enable: Boolean) { override fun setEnableUpload(enable: Boolean) {
engine?.setEnableUpload(enable) if (isMainProcess.get()) {
engine?.setEnableUpload(enable)
} else {
Timber.w("setEnableUpload($enable) error! not in main process")
}
} }
override fun snapshotAnalyticsAudit(): String { override fun snapshotAnalyticsAudit(): String {
@ -364,17 +390,27 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
} }
override fun clearStatistic(context: Context) { override fun clearStatistic(context: Context) {
PreferencesManager.getInstance(context).apply { if (isMainProcess.get()) {
eventCountAll = 0 PreferencesManager.getInstance(context).apply {
eventCountDeleted = 0 eventCountAll = 0
eventCountUploaded = 0 eventCountDeleted = 0
eventCountUploaded = 0
}
} else {
Timber.w("clearStatistic error! not in main process")
} }
} }
override fun forceUpload(scene: String): Boolean { override fun forceUpload(scene: String): Boolean {
if (!isMainProcess.get()) {
Timber.w("forceUpload(${scene}) error! not in main process")
return false
}
return engine?.let { return engine?.let {
if (it.started.get()) { if (it.started.get()) {
it.forceUpload(scene) EventSink.forceUpload(scene)
return@let true return@let true
} }
return@let false return@let false
@ -382,30 +418,30 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
} }
private fun deliverEvent(item: EventItem, options: AnalyticsOptions) { private fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
Timber.tag("GuruAnalytics").d("deliverEvent ${item.eventName}!") if (isMainProcess.get()) {
deliverExecutor.execute { Timber.tag("GuruAnalytics").d("deliverEvent ${item.eventName}!")
for (deliver in delivers) { EventSink.deliverEvent(item, options)
deliver.deliverEvent(item, options) } else {
} Timber.w("deliverEvent(${item.eventName}) error! not in main process")
} }
} }
private fun deliverProperty(name: String, value: String) { private fun deliverProperty(name: String, value: String) {
Timber.tag("GuruAnalytics").d("deliverProperty $name = $value") if (isMainProcess.get()) {
deliverExecutor.execute { Timber.tag("GuruAnalytics").d("deliverProperty $name = $value")
for (deliver in delivers) { EventSink.deliverProperty(name, value)
deliver.deliverProperty(name, value) } else {
} Timber.w("deliverProperty($name=$value) error! not in main process")
} }
} }
private fun removeProperties(keys: Set<String>) { private fun removeProperties(keys: Set<String>) {
Timber.tag("GuruAnalytics").d("removeProperties $keys") if (isMainProcess.get()) {
deliverExecutor.execute { Timber.tag("GuruAnalytics").d("removeProperties $keys")
for (deliver in delivers) { EventSink.removeProperties(keys)
deliver.removeProperties(keys) } else {
} Timber.w("removeProperties($keys) error! not in main process")
} }
} }
} }

View File

@ -1,6 +1,7 @@
package guru.core.analytics.utils package guru.core.analytics.utils
import android.app.ActivityManager import android.app.ActivityManager
import android.app.Application
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Point import android.graphics.Point
@ -19,6 +20,7 @@ import guru.core.analytics.Constants
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.* import java.util.*
@ -119,17 +121,15 @@ object AndroidUtils {
} }
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //try to fix SELinux limit due to unable access /proc file system
//try to fix SELinux limit due to unable access /proc file system val am = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager if (am != null) {
if (null != am) { val appProcessInfoList = am.runningAppProcesses
val appProcessInfoList = am.runningAppProcesses if (null != appProcessInfoList) {
if (null != appProcessInfoList) { for (i in appProcessInfoList) {
for (i in appProcessInfoList) { if (i.pid == Process.myPid()) {
if (i.pid == Process.myPid()) { val result = i.processName.trim { it <= ' ' }
val result = i.processName.trim { it <= ' ' } return result
return result
}
} }
} }
} }
@ -139,5 +139,4 @@ object AndroidUtils {
// the real process name // the real process name
return context.applicationInfo.processName return context.applicationInfo.processName
} }
} }

View File

@ -1,5 +1,6 @@
package guru.core.analytics.utils package guru.core.analytics.utils
import android.annotation.SuppressLint
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -11,6 +12,7 @@ import android.net.NetworkCapabilities
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import guru.core.analytics.data.local.PreferencesManager
import io.reactivex.BackpressureStrategy import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
@ -18,7 +20,8 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class Connectivity(private val context: Context) : BroadcastReceiver() {
class Connectivity private constructor(private val context: Context) : BroadcastReceiver() {
companion object { companion object {
const val CONNECTIVITY_NONE: String = "none" const val CONNECTIVITY_NONE: String = "none"
@ -31,6 +34,14 @@ class Connectivity(private val context: Context) : BroadcastReceiver() {
const val CONNECTIVITY_ACTION: String = "android.net.conn.CONNECTIVITY_CHANGE" const val CONNECTIVITY_ACTION: String = "android.net.conn.CONNECTIVITY_CHANGE"
@SuppressLint("StaticFieldLeak")
@Volatile
private var INSTANCE: Connectivity? = null
fun of(context: Context): Connectivity =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Connectivity(context.applicationContext).also { INSTANCE = it }
}
} }
private val connectivityManager by lazy { private val connectivityManager by lazy {