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 {
// TestProcessActivity.startActivity(this)
GuruAnalytics.Builder(this)
GuruAnalytics.Builder(this, "v3.0.0")
.setBatchLimit(25)
.setUploadPeriodInSeconds(60)
.setStartUploadDelayInSecond(3)

View File

@ -21,7 +21,7 @@ class TestProcessActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_process)
GuruAnalytics.Builder(this)
GuruAnalytics.Builder(this, "v3.0.0")
.setBatchLimit(25)
.setUploadPeriodInSeconds(60)
.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
# resources declared in the library itself and none from the library's dependencies,
# 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"
buildConfigField "String", "buildTs", buildTs()
buildConfigField "String", "guruAnalyticsSdkVersion", "\"v$guruAnalyticsSdkVersion\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@ -18,7 +18,8 @@ publishing { // Repositories *to* which Gradle can publish artifacts
maven(MavenPublication) {
groupId 'guru.core.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 "build/outputs/aar/aar-test-release.aar"//aar
afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) }

View File

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

View File

@ -12,14 +12,18 @@ object Constants {
const val EVENT = "event"
const val PARAM = "param"
const val SCREEN = "screen_name"
const val SESSION_ID = "session_id"
const val ITEM_CATEGORY = "item_category"
const val ITEM_NAME = "item_name"
const val VALUE = "value"
const val FG = "fg"
const val FG = "guru_engagement"
const val DURATION = "duration"
const val FIRST_OPEN = "first_open"
const val ERROR_PROCESS = "error_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 {
@ -43,7 +47,9 @@ object Constants {
const val SCREEN_W = "screenW"
const val OS_VERSION = "osVersion"
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 {

View File

@ -30,7 +30,8 @@ abstract class GuruAnalytics {
mainProcess: String? = null,
isEnableCronet: Boolean? = null,
uploadIpAddress: List<String>? = null,
dnsMode: Int? = null
dnsMode: Int? = null,
guruSdkVersion: 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()
fun setBatchLimit(batchLimit: Int?) = apply { analyticsInfo.batchLimit = batchLimit }
@ -162,7 +163,8 @@ abstract class GuruAnalytics {
mainProcess,
isEnableCronet,
uploadIpAddress,
dnsMode
dnsMode,
guruSdkVersion
)
}
return INSTANCE

View File

@ -60,5 +60,7 @@ class PreferencesManager private constructor(
var deviceId: String? by bind("device_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 eventDispatcherStarted: Boolean = false
var fgHelperInitialized: Boolean = false
var connectionState: Boolean = false
var networkAvailable: Boolean = false
// 整体的
var total: Int = 0
@ -54,7 +54,7 @@ object GuruAnalyticsAudit {
dnsMode = dnsMode,
eventDispatcherStarted = eventDispatcherStarted,
fgHelperInitialized = fgHelperInitialized,
connectionState = connectionState,
connectionState = networkAvailable,
total = total,
deleted = deleted,
uploaded = uploaded,

View File

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

View File

@ -38,6 +38,7 @@ object EventInfoStore {
restoreIds(this)
GuruAnalyticsAudit.total = this.eventCountAll ?: 0
GuruAnalyticsAudit.uploaded = this.eventCountUploaded ?: 0
GuruAnalyticsAudit.deleted = this.eventCountDeleted ?: 0
}
}
}
@ -163,7 +164,10 @@ object EventInfoStore {
private val supplementEventParamsSubject: BehaviorSubject<Map<String, Any>> =
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>> =
@ -184,7 +188,10 @@ object EventInfoStore {
}
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) {
supplementEventParamsSubject.onNext(value)
}

View File

@ -37,11 +37,13 @@ internal class AppLifecycleMonitor internal constructor(context: Context) {
when (event) {
Lifecycle.Event.ON_START -> {
Timber.d("${TAG}_ON_START")
fgHelper.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 -> {
Timber.d("${TAG}_ON_PAUSE")
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
import android.content.Context
import android.net.Uri
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.SystemClock
import androidx.work.WorkManager
import guru.core.analytics.Constants
import guru.core.analytics.GuruAnalytics
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.model.EventEntity
import guru.core.analytics.data.db.model.EventPriority
@ -44,6 +39,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayList
import kotlin.math.ceil
import kotlin.math.max
internal class EventEngine internal constructor(
private val context: Context,
@ -51,21 +47,21 @@ internal class EventEngine internal constructor(
private val uploadPeriodInSeconds: Long = DEFAULT_UPLOAD_PERIOD_IN_SECONDS,
private val eventExpiredInDays: Int = DEFAULT_EVENT_EXPIRED_IN_DAYS,
private val guruRepository: GuruRepository
) : EventDeliver {
) {
private val preferencesManager by lazy {
private val preferencesManager: PreferencesManager by lazy {
PreferencesManager.getInstance(context)
}
private val connectivity: Connectivity by lazy {
Connectivity(context)
Connectivity.of(context)
}
private val lifecycleMonitor = AppLifecycleMonitor(context)
companion object {
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_EVENT_EXPIRED_IN_DAYS = 7
@ -80,7 +76,6 @@ internal class EventEngine internal constructor(
@Volatile
var sessionActivated = false
fun logDebug(message: String, vararg args: Any?) {
if (GuruAnalytics.INSTANCE.isDebug()) {
Timber.tag(TAG).d(message, args)
@ -101,13 +96,10 @@ internal class EventEngine internal constructor(
}
private var compositeDisposable: CompositeDisposable = CompositeDisposable()
private val pendingEventSubject: PublishSubject<Int> = PublishSubject.create()
private val forceTriggerSubject: PublishSubject<String> = PublishSubject.create()
internal val started = AtomicBoolean(false)
private var enableUpload = true
private var latestValidActionTs = 0L
fun setEnableUpload(enable: Boolean) {
enableUpload = enable
val extMap = mapOf("enable" to enable)
@ -116,6 +108,7 @@ internal class EventEngine internal constructor(
fun start(startUploadDelay: Long?) {
if (started.compareAndSet(false, true)) {
AnchorAt.recordAt(AnchorAt.SESSION_START)
prepare()
connectivity.bind()
lifecycleMonitor.initialize()
@ -131,8 +124,7 @@ internal class EventEngine internal constructor(
logDebug("session started error!")
}
scheduler.scheduleDirect({
EventDispatcher.start()
logFirstOpen()
EventSink.start()
startWork()
val extMap = mapOf("startUploadDelayInSecond" to startUploadDelay)
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() {
@ -172,6 +154,7 @@ internal class EventEngine internal constructor(
logEvent(event)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_SESSION_ACTIVE)
sessionActivated = true
}.onErrorReturn {
Timber.e("active error! $it")
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_SESSION_START_ERROR, it.message)
@ -185,7 +168,7 @@ internal class EventEngine internal constructor(
private fun dispatchActiveWorker() {
Timber.e("dispatchActiveWorker...")
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = OneTimeWorkRequestBuilder<ActiveWorker>()
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES)
@ -209,7 +192,6 @@ internal class EventEngine internal constructor(
private fun startWork() {
Timber.d("startWork")
pollEvents()
forceUpload("startWork")
Timber.d("UploadEventDaemon started!!")
}
@ -236,19 +218,25 @@ internal class EventEngine internal constructor(
private fun pollEvents() {
logDebug("pollEvents()!! $uploadPeriodInSeconds $batchLimit")
val periodFlowable = Flowable.interval(5, uploadPeriodInSeconds, TimeUnit.SECONDS).onBackpressureDrop()
val forceFlowable = forceTriggerSubject.toFlowable(BackpressureStrategy.DROP).map { scene ->
logDebug("force trigger: $scene")
val forceFlowable = EventSink.forceFlowable.map { scene ->
logDebug("Force Trigger: $scene")
}
val networkFlowable = connectivity.networkAvailableFlowable
compositeDisposable.add(Flowable.combineLatest(
periodFlowable, forceFlowable, networkFlowable,
) { _, _, available ->
GuruAnalyticsAudit.connectionState = available
val uploadedRate = GuruAnalyticsAudit.uploaded / GuruAnalyticsAudit.total.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 || ignoreAvailable)
}.flatMap { uploadEvents(100) }.subscribe()
compositeDisposable.add(
Flowable.combineLatest(
periodFlowable, forceFlowable,
) { _, _ ->
val available = try {
connectivity.isNetworkAvailable()
} catch (throwable: Throwable) {
logInfo("networkAvailable error: $throwable")
GuruAnalyticsAudit.networkAvailable
}
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()
GuruAnalyticsAudit.uploadReady = true
logDebug("uploadEvents: $count")
return Flowable.just(count).map { eventDao.loadAndMarkUploadEvents(it) }.onErrorReturn {
try {
eventDao.loadAndMarkUploadEvents(batchLimit)
} catch (throwable: Throwable) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_LOAD_MARK, it.message)
emptyList()
}
}.filter { it.isNotEmpty() }.subscribeOn(dbScheduler).observeOn(scheduler).concatMap { splitEntities(it) }
return Flowable.just(count)
.map { eventDao.loadAndMarkUploadEvents(it) }
.onErrorReturn {
try {
eventDao.loadAndMarkUploadEvents(batchLimit)
} catch (throwable: Throwable) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_LOAD_MARK, it.message)
emptyList()
}
}.filter { it.isNotEmpty() }.subscribeOn(dbScheduler).observeOn(scheduler).concatMap { splitEntities(it) }
.flatMapSingle { uploadEventsInternal(it) }.filter { it.isNotEmpty() }.doOnNext {
eventDao.deleteEvents(it)
if (GuruAnalytics.INSTANCE.isDebug()) {
@ -306,76 +296,42 @@ internal class EventEngine internal constructor(
private fun uploadEventsInternal(entities: List<EventEntity>): Single<List<EventEntity>> {
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 eventCountUploaded = preferencesManager.eventCountUploaded ?: 0
val uploaded = eventCountUploaded + entities.size
preferencesManager.eventCountUploaded = uploaded
GuruAnalyticsAudit.uploaded = uploaded
GuruAnalyticsAudit.sessionUploaded += entities.size
val extMap = mapOf(
"count" to entities.size,
"eventNames" to entities.joinToString(",") { it.event },
"allUploadedCount" to preferencesManager.eventCountUploaded,
)
logDebug("uploadEvents success: $extMap")
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_SUCCESS, extMap)
latestValidActionTs = android.os.SystemClock.elapsedRealtime()
}.onErrorReturn {
GuruAnalyticsDatabase.getInstance().eventDao().updateEventDefault(entities)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_FAIL, it.message)
val elapsed = android.os.SystemClock.elapsedRealtime()
val uploadedRate = GuruAnalyticsAudit.sessionUploaded / GuruAnalyticsAudit.sessionTotal
val isActiveNetworkMetered = connectivity.isActiveNetworkMetered
val exceededValidActionGap = elapsed - latestValidActionTs > SESSION_ACTIVE_INTERVAL
logInfo("uploadEvent error $it sessionActivated:$sessionActivated uploadedRate:$uploadedRate isActiveNetworkMetered:$isActiveNetworkMetered gap:${(elapsed - latestValidActionTs) / 1000}s")
/// 如果没有激活并且当前是计费网络并且距离上次上传成功时间超过15分钟激活worker
/// 因为这个 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)
val extMap = mapOf(
"count" to entities.size,
"eventNames" to entities.joinToString(",") { it.event },
"allUploadedCount" to preferencesManager.eventCountUploaded,
)
logDebug("uploadEvents success: $extMap")
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_SUCCESS, extMap)
latestValidActionTs = android.os.SystemClock.elapsedRealtime()
}.onErrorReturn {
GuruAnalyticsDatabase.getInstance().eventDao().updateEventDefault(entities)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_FAIL, it.message)
// val elapsed = android.os.SystemClock.elapsedRealtime()
// val uploadedRate = GuruAnalyticsAudit.sessionUploaded / max(GuruAnalyticsAudit.sessionTotal, 1)
// val isActiveNetworkMetered = connectivity.isActiveNetworkMetered
// val exceededValidActionGap = elapsed - latestValidActionTs > SESSION_ACTIVE_INTERVAL
// logInfo("uploadEvent error $it sessionActivated:$sessionActivated uploadedRate:$uploadedRate isActiveNetworkMetered:$isActiveNetworkMetered gap:${(elapsed - latestValidActionTs) / 1000}s")
// /// 如果没有激活并且当前是计费网络并且距离上次上传成功时间超过15分钟激活worker
// /// 因为这个 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() }
}
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 guru.core.analytics.Constants
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.model.AnalyticsOptions
import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
@ -16,20 +18,17 @@ import java.lang.Math.max
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
internal class FgHelper(context: Context) {
companion object {
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())
}
@Volatile
private var latestRecordAt = SystemClock.elapsedRealtime()
@Volatile
private var currentDuration = 0L
private var latestActiveAt = AtomicLong(SystemClock.elapsedRealtime())
private var disposable: Disposable? = null
private val initialized = AtomicBoolean(false)
@ -37,39 +36,45 @@ internal class FgHelper(context: Context) {
PreferencesManager.getInstance(context)
}
private val preferenceEditor by lazy {
preferencesManager.getSharedPreferencesDirectly().edit()
}
internal fun ensureInitialized() {
if (initialized.compareAndSet(false, true)) {
currentDuration = preferencesManager.getTotalDurationFgEvent()
refresh(force = true)
val elapsedAt = SystemClock.elapsedRealtime()
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 {
val now = SystemClock.elapsedRealtime()
val duration = now - latestRecordAt
val fgDuration = currentDuration + duration
latestRecordAt = now
currentDuration = if (force || fgDuration > FG_REPORT_INTERVAL_MILLIS) {
logFgEvent(fgDuration.coerceAtLeast(1))
0L
private fun refresh() {
val elapsedAt = SystemClock.elapsedRealtime()
val duration = elapsedAt - latestActiveAt.get()
Timber.tag("FgHelper").d("refresh: $duration")
if (duration < FG_REPORT_INTERVAL_MILLIS) {
preferencesManager.setTotalDurationFgEvent(duration)
} else {
fgDuration
userEngagement(elapsedAt)
}
preferenceEditor.putLong(PreferencesManager.KEY_TOTAL_DURATION_FG_EVENT, currentDuration)
.commit()
return currentDuration
}
fun start() {
userActive()
}
fun stop() {
userInactive()
}
private fun userActive() {
ensureInitialized()
latestRecordAt = SystemClock.elapsedRealtime()
latestActiveAt.set(SystemClock.elapsedRealtime())
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)
.subscribe({
refresh()
@ -79,17 +84,32 @@ internal class FgHelper(context: Context) {
GuruAnalyticsAudit.fgHelperInitialized = true
}
fun stop() {
private fun userInactive() {
userEngagement(SystemClock.elapsedRealtime())
disposable?.dispose()
refresh()
disposable = null
}
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)
private fun isActive() = disposable != null
private fun userEngagement(elapsedAt: Long) {
if (!isActive()) {
Timber.tag("FgHelper").w("inactive fg helper")
return
}
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.net.Uri
import android.transition.Scene
import androidx.annotation.RequiresPermission
import androidx.work.*
import guru.core.analytics.Constants
@ -52,6 +51,11 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
private val initialized = AtomicBoolean(false)
private val isMainProcess = AtomicBoolean(true)
// private val = System.currentTimeMillis()
override fun isDebug(): Boolean = debugMode
override fun setDebug(debug: Boolean) {
@ -69,6 +73,7 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
Timber.d("setUploadEventBaseUrl:$updateEventBaseUrl")
}
@RequiresPermission(allOf = ["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"])
override fun initialize(
context: Context,
@ -87,7 +92,8 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
mainProcess: String?,
isEnableCronet: Boolean?,
uploadIpAddress: List<String>?,
dnsMode: Int?
dnsMode: Int?,
guruSdkVersion: String
) {
if (initialized.compareAndSet(false, true)) {
val debugApp = SystemProperties.read("debug.guru.analytics.app")
@ -100,19 +106,27 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
} 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)
delivers.add(EventDispatcher)
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")
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)
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)
@ -123,7 +137,7 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
ServiceLocator.setUploadIpAddress(uploadIpAddress)
ServiceLocator.setDnsMode(dnsMode ?: 0)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_4)
// EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_4)
if (isEnableCronet == true) {
GuruAnalyticsAudit.enabledCronet = 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 hostname: String? = ""
@ -147,9 +161,10 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
}
hostname = uploadEventBaseUri.host
}
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_6)
// EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_6)
ServiceLocator.preloadDns(hostname)
EventSink.initialize(context, hostname ?: AnalyticsApiHost.BASE_URL)
engine = EventEngine(
context,
@ -161,28 +176,17 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
),
guruRepository = ServiceLocator.provideGuruRepository(context, baseUri = uploadEventBaseUri) /// 此处需要配合 ServiceLocator的重构进行修改
).apply {
delivers.add(this)
start(startUploadDelayInSecond)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_7)
// EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_7)
}
if (isInitPeriodicWork) {
val process = AndroidUtils.getProcessName(context)
Timber.d("initialize ${mainProcess == process} currentProcess:$process mainProcess:$mainProcess")
if (mainProcess.isNullOrBlank() || mainProcess == process) {
try {
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)
}
try {
initAnalyticsPeriodic(context)
} catch (throwable: Throwable) {
Timber.d("init worker error!")
}
}
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_8)
// EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_8)
val extMap = mapOf(
"version_code" to internalVersion,
@ -203,8 +207,9 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
if (uploadIpAddress != null) {
PreferencesManager.getInstance(context).uploadIpAddressList = uploadIpAddress.joinToString("|")
}
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_9)
// EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_9)
GuruAnalyticsAudit.initialized = true
AnchorAt.recordAt(AnchorAt.SDK_INIT_COMPLETED)
}
}
@ -329,19 +334,35 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
}
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) {
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) {
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>) {
removeProperties(keys)
if (isMainProcess.get()) {
removeProperties(keys)
} else {
Timber.w("removeUserProperties error! not in main process")
}
}
override fun getUserProperties(callback: (Map<String, String>) -> Unit) {
@ -355,7 +376,12 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
}
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 {
@ -364,17 +390,27 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
}
override fun clearStatistic(context: Context) {
PreferencesManager.getInstance(context).apply {
eventCountAll = 0
eventCountDeleted = 0
eventCountUploaded = 0
if (isMainProcess.get()) {
PreferencesManager.getInstance(context).apply {
eventCountAll = 0
eventCountDeleted = 0
eventCountUploaded = 0
}
} else {
Timber.w("clearStatistic error! not in main process")
}
}
override fun forceUpload(scene: String): Boolean {
if (!isMainProcess.get()) {
Timber.w("forceUpload(${scene}) error! not in main process")
return false
}
return engine?.let {
if (it.started.get()) {
it.forceUpload(scene)
EventSink.forceUpload(scene)
return@let true
}
return@let false
@ -382,30 +418,30 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
}
private fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
Timber.tag("GuruAnalytics").d("deliverEvent ${item.eventName}!")
deliverExecutor.execute {
for (deliver in delivers) {
deliver.deliverEvent(item, options)
}
if (isMainProcess.get()) {
Timber.tag("GuruAnalytics").d("deliverEvent ${item.eventName}!")
EventSink.deliverEvent(item, options)
} else {
Timber.w("deliverEvent(${item.eventName}) error! not in main process")
}
}
private fun deliverProperty(name: String, value: String) {
Timber.tag("GuruAnalytics").d("deliverProperty $name = $value")
deliverExecutor.execute {
for (deliver in delivers) {
deliver.deliverProperty(name, value)
}
if (isMainProcess.get()) {
Timber.tag("GuruAnalytics").d("deliverProperty $name = $value")
EventSink.deliverProperty(name, value)
} else {
Timber.w("deliverProperty($name=$value) error! not in main process")
}
}
private fun removeProperties(keys: Set<String>) {
Timber.tag("GuruAnalytics").d("removeProperties $keys")
deliverExecutor.execute {
for (deliver in delivers) {
deliver.removeProperties(keys)
}
if (isMainProcess.get()) {
Timber.tag("GuruAnalytics").d("removeProperties $keys")
EventSink.removeProperties(keys)
} else {
Timber.w("removeProperties($keys) error! not in main process")
}
}
}

View File

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

View File

@ -1,5 +1,6 @@
package guru.core.analytics.utils
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@ -11,6 +12,7 @@ import android.net.NetworkCapabilities
import android.os.Build
import android.os.Handler
import android.os.Looper
import guru.core.analytics.data.local.PreferencesManager
import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
import io.reactivex.subjects.BehaviorSubject
@ -18,7 +20,8 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
class Connectivity(private val context: Context) : BroadcastReceiver() {
class Connectivity private constructor(private val context: Context) : BroadcastReceiver() {
companion object {
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"
@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 {