diff --git a/guru_analytics/app/src/main/java/com/example/guruanalytics/MainActivity.kt b/guru_analytics/app/src/main/java/com/example/guruanalytics/MainActivity.kt index 0340321..5110a24 100644 --- a/guru_analytics/app/src/main/java/com/example/guruanalytics/MainActivity.kt +++ b/guru_analytics/app/src/main/java/com/example/guruanalytics/MainActivity.kt @@ -179,7 +179,7 @@ class MainActivity : AppCompatActivity() { } findViewById(R.id.tvOpenTestProcessActivity).setOnClickListener { // TestProcessActivity.startActivity(this) - GuruAnalytics.Builder(this) + GuruAnalytics.Builder(this, "v3.0.0") .setBatchLimit(25) .setUploadPeriodInSeconds(60) .setStartUploadDelayInSecond(3) diff --git a/guru_analytics/app/src/main/java/com/example/guruanalytics/TestProcessActivity.kt b/guru_analytics/app/src/main/java/com/example/guruanalytics/TestProcessActivity.kt index e0d5e93..8069595 100644 --- a/guru_analytics/app/src/main/java/com/example/guruanalytics/TestProcessActivity.kt +++ b/guru_analytics/app/src/main/java/com/example/guruanalytics/TestProcessActivity.kt @@ -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) diff --git a/guru_analytics/gradle.properties b/guru_analytics/gradle.properties index cd0519b..bc08de4 100644 --- a/guru_analytics/gradle.properties +++ b/guru_analytics/gradle.properties @@ -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 \ No newline at end of file +android.nonTransitiveRClass=true + +guruAnalyticsSdkVersion=1.1.2 \ No newline at end of file diff --git a/guru_analytics/guru_analytics/build.gradle b/guru_analytics/guru_analytics/build.gradle index 9904b7d..067aac9 100644 --- a/guru_analytics/guru_analytics/build.gradle +++ b/guru_analytics/guru_analytics/build.gradle @@ -30,6 +30,7 @@ android { versionName "1.0" buildConfigField "String", "buildTs", buildTs() + buildConfigField "String", "guruAnalyticsSdkVersion", "\"v$guruAnalyticsSdkVersion\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/guru_analytics/guru_analytics/maven-publish.gradle b/guru_analytics/guru_analytics/maven-publish.gradle index a33b47c..7ebb25f 100644 --- a/guru_analytics/guru_analytics/maven-publish.gradle +++ b/guru_analytics/guru_analytics/maven-publish.gradle @@ -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")) } diff --git a/guru_analytics/guru_analytics/src/main/AndroidManifest.xml b/guru_analytics/guru_analytics/src/main/AndroidManifest.xml index c0f6a45..cec3e92 100644 --- a/guru_analytics/guru_analytics/src/main/AndroidManifest.xml +++ b/guru_analytics/guru_analytics/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/Constants.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/Constants.kt index e6f31d2..d623fab 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/Constants.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/Constants.kt @@ -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 { diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/GuruAnalytics.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/GuruAnalytics.kt index 0f67576..eef97cd 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/GuruAnalytics.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/GuruAnalytics.kt @@ -30,7 +30,8 @@ abstract class GuruAnalytics { mainProcess: String? = null, isEnableCronet: Boolean? = null, uploadIpAddress: List? = 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 diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferencesManager.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferencesManager.kt index 7aca7e8..5e907f7 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferencesManager.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferencesManager.kt @@ -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) } diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/GuruAnalyticsAuditSnapshot.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/GuruAnalyticsAuditSnapshot.kt index 6f94a4a..381c757 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/GuruAnalyticsAuditSnapshot.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/GuruAnalyticsAuditSnapshot.kt @@ -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, diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/DeviceInfoStore.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/DeviceInfoStore.kt index 9328657..b0e55e7 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/DeviceInfoStore.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/DeviceInfoStore.kt @@ -14,7 +14,7 @@ import java.util.* object DeviceInfoStore { - const val SDK_VERSION = "v1.1.0" + var GURU_SDK_VERSION = "" private val deviceInfoSubject: BehaviorSubject> = 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") diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/EventInfoStore.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/EventInfoStore.kt index 8a599b6..d848d4e 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/EventInfoStore.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/EventInfoStore.kt @@ -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> = 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> = @@ -184,7 +188,10 @@ object EventInfoStore { } private var supplementEventParams: Map - 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) } diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/AppLifecycleMonitor.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/AppLifecycleMonitor.kt index c28ddb8..afed110 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/AppLifecycleMonitor.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/AppLifecycleMonitor.kt @@ -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() diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventDispatcher.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventDispatcher.kt deleted file mode 100644 index 1ec1df9..0000000 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventDispatcher.kt +++ /dev/null @@ -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() - - 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) { - EventInfoStore.removeUserProperties(keys) - } -} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventEngine.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventEngine.kt index 716dd6e..3118167 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventEngine.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventEngine.kt @@ -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 = PublishSubject.create() - private val forceTriggerSubject: PublishSubject = 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() .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): Single> { 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) { - - } - - 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 { diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventSink.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventSink.kt new file mode 100644 index 0000000..99de6a6 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventSink.kt @@ -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 = PublishSubject.create() + + private lateinit var preferencesManager: PreferencesManager + + @SuppressLint("StaticFieldLeak") + private lateinit var connectivity: Connectivity + + private val pendingEvents = ConcurrentLinkedDeque() + + 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 + 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) { + 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) + ) + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgEventHelper.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgEventHelper.kt deleted file mode 100644 index f1ded3a..0000000 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgEventHelper.kt +++ /dev/null @@ -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 = 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() -// 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 -// } -// } -// -//} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgHelper.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgHelper.kt index 0f44fbf..59f0c51 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgHelper.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgHelper.kt @@ -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() - 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) } } \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/GuruAnalyticsImpl.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/GuruAnalyticsImpl.kt index 083b37f..f96b0c6 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/GuruAnalyticsImpl.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/GuruAnalyticsImpl.kt @@ -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?, - 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() - 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) { - removeProperties(keys) + if (isMainProcess.get()) { + removeProperties(keys) + } else { + Timber.w("removeUserProperties error! not in main process") + } } override fun getUserProperties(callback: (Map) -> 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) { - 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") } } - } \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/AndroidUtils.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/AndroidUtils.kt index 456bd92..75f3f9f 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/AndroidUtils.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/AndroidUtils.kt @@ -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 } - } \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/Connectivity.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/Connectivity.kt index 6be116d..34a396b 100644 --- a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/Connectivity.kt +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/Connectivity.kt @@ -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 {