parent
3803d53afe
commit
d5ec200617
|
|
@ -4,12 +4,12 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdk 32
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.guruanalytics"
|
||||
minSdk 21
|
||||
targetSdk 32
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 1
|
||||
versionName "1.1.0"
|
||||
|
||||
|
|
@ -41,6 +41,10 @@ dependencies {
|
|||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
implementation "androidx.work:work-runtime:2.7.1"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
implementation "androidx.work:work-rxjava2:2.7.1"
|
||||
|
||||
implementation(project(':guru_analytics'))
|
||||
// implementation 'guru.core.analytics:guru_analytics:0.1.0'
|
||||
}
|
||||
|
|
@ -1,17 +1,150 @@
|
|||
package com.example.guruanalytics
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.app.usage.NetworkStats
|
||||
import android.app.usage.NetworkStatsManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.TrafficStats
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import guru.core.analytics.GuruAnalytics
|
||||
import guru.core.analytics.data.api.dns.DnsMode
|
||||
import guru.core.analytics.data.db.model.EventPriority
|
||||
import guru.core.analytics.data.model.AnalyticsOptions
|
||||
import guru.core.analytics.impl.ActiveWorker
|
||||
import guru.core.analytics.impl.AnalyticsWorker
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object NetworkUsageUtils {
|
||||
fun getCurrentAppNetworkUsage(context: Context) {
|
||||
val uid = getApplicationUid(context)
|
||||
|
||||
val mobileRxBytes = TrafficStats.getUidRxBytes(uid) // 获取移动数据接收的字节数
|
||||
val mobileTxBytes = TrafficStats.getUidTxBytes(uid) // 获取移动数据发送的字节数
|
||||
val wifiRxBytes = TrafficStats.getTotalRxBytes() - TrafficStats.getMobileRxBytes() // 获取 Wi-Fi 接收的字节数
|
||||
val wifiTxBytes = TrafficStats.getTotalTxBytes() - TrafficStats.getMobileTxBytes() // 获取 Wi-Fi 发送的字节数
|
||||
|
||||
if (mobileRxBytes == TrafficStats.UNSUPPORTED.toLong() || mobileTxBytes == TrafficStats.UNSUPPORTED.toLong()) {
|
||||
println("该设备不支持 TrafficStats API")
|
||||
} else {
|
||||
println("当前应用的移动数据接收字节数: $mobileRxBytes")
|
||||
println("当前应用的移动数据发送字节数: $mobileTxBytes")
|
||||
println("当前应用的 Wi-Fi 数据接收字节数: $wifiRxBytes")
|
||||
println("当前应用的 Wi-Fi 数据发送字节数: $wifiTxBytes")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getApplicationUid(context: Context): Int {
|
||||
try {
|
||||
val applicationInfo = context.packageManager.getApplicationInfo(context.packageName, 0)
|
||||
return applicationInfo.uid
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
fun isBackgroundDataRestricted(context: Context): Boolean {
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
if (connectivityManager != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val status = connectivityManager.restrictBackgroundStatus
|
||||
if (status == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED) {
|
||||
// 限制背景数据
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun getMobileDataUsage(context: Context, uid: Int): Long {
|
||||
val networkStatsManager = context.getSystemService(Context.NETWORK_STATS_SERVICE) as NetworkStatsManager
|
||||
val bucket: NetworkStats.Bucket
|
||||
var dataUsage = 0L
|
||||
|
||||
try {
|
||||
bucket = networkStatsManager.querySummaryForDevice(NetworkCapabilities.TRANSPORT_CELLULAR, null, 0, System.currentTimeMillis())
|
||||
dataUsage = bucket.rxBytes + bucket.txBytes
|
||||
} catch (e: RemoteException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return dataUsage
|
||||
}
|
||||
|
||||
private fun dumpCapabilitiesList(capabilities: NetworkCapabilities?) {
|
||||
val types: MutableList<String> = ArrayList()
|
||||
|
||||
if (capabilities == null
|
||||
|| !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
) {
|
||||
// types.add(Connectivity.CONNECTIVITY_NONE)
|
||||
Log.d("hasCapability", "NetworkCapabilities.NET_CAPABILITY_INTERNET")
|
||||
}
|
||||
if (capabilities == null
|
||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
) {
|
||||
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_WIFI")
|
||||
}
|
||||
if (capabilities == null || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
|
||||
) {
|
||||
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_WIFI_AWARE")
|
||||
}
|
||||
if (capabilities == null || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
|
||||
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_ETHERNET")
|
||||
}
|
||||
if (capabilities == null || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_VPN")
|
||||
}
|
||||
if (capabilities == null || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_CELLULAR")
|
||||
}
|
||||
if (capabilities == null || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) {
|
||||
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_BLUETOOTH")
|
||||
}
|
||||
if (capabilities == null || capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
) {
|
||||
Log.d("hasCapability", "NetworkCapabilities.NET_CAPABILITY_INTERNET")
|
||||
}
|
||||
}
|
||||
|
||||
fun dumpCap(context: Context) {
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val network = connectivityManager.activeNetwork
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
dumpCapabilitiesList(capabilities)
|
||||
val isActiveNetworkMetered = connectivityManager.isActiveNetworkMetered;
|
||||
Log.d("dumpCap", "isActiveNetworkMetered: $isActiveNetworkMetered")
|
||||
// val appUid = getApplicationUid(context)
|
||||
// Log.d("dumpCap", "appUid: $appUid")
|
||||
// val rx = getMobileDataUsage(context, appUid)
|
||||
// Log.d("dumpCap", "dataUsage: $rx")
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private var enableUpload = true
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
|
@ -24,46 +157,101 @@ class MainActivity : AppCompatActivity() {
|
|||
// callbackEventHandler = true,
|
||||
// )
|
||||
|
||||
GuruAnalytics.Builder(this)
|
||||
.setBatchLimit(25)
|
||||
.setUploadPeriodInSeconds(60)
|
||||
.setStartUploadDelayInSecond(3)
|
||||
.setEventExpiredInDays(7)
|
||||
.isPersistableLog(true)
|
||||
.setEventHandlerCallback(eventHandler)
|
||||
.isInitPeriodicWork(false)
|
||||
.isDebug(BuildConfig.DEBUG)
|
||||
// .setUploadEventBaseUrl("https://www.baidu.com")
|
||||
// .setFgEventPeriodInSeconds(60L)
|
||||
.setXAppId("test_x_app_id")
|
||||
.setXDeviceInfo("test_x_device_info")
|
||||
.setMainProcess("com.example.guruanalytics")
|
||||
.isEnableCronet(true)
|
||||
.setUploadIpAddress(listOf("3.210.96.186", "34.196.69.199"))
|
||||
.build()
|
||||
findViewById<TextView>(R.id.tvSetProperty).setOnClickListener {
|
||||
GuruAnalytics.INSTANCE.setScreen("main")
|
||||
GuruAnalytics.INSTANCE.setAdId("AD_ID_01")
|
||||
GuruAnalytics.INSTANCE.setFirebaseId("FIREBASE_ID")
|
||||
GuruAnalytics.INSTANCE.setUid("MBK-XXXXX")
|
||||
}
|
||||
|
||||
|
||||
findViewById<TextView>(R.id.tvLogEvent).setOnClickListener {
|
||||
val map = mutableMapOf<String, Any>()
|
||||
map["percent"] = 0.4
|
||||
map["level"] = 2
|
||||
map["from"] = "game"
|
||||
GuruAnalytics.INSTANCE.logEvent("test_event", "game", "main", 10, map)
|
||||
|
||||
for (i in 0..200) {
|
||||
val map = mutableMapOf<String, Any>()
|
||||
map["key_$i"] = "value_$i"
|
||||
map["percent"] = 0.4
|
||||
map["level"] = 2
|
||||
map["from"] = "game"
|
||||
GuruAnalytics.INSTANCE.logEvent("test_event", "game", "main", 10, map)
|
||||
}
|
||||
|
||||
}
|
||||
findViewById<TextView>(R.id.tvOpenTestProcessActivity).setOnClickListener {
|
||||
TestProcessActivity.startActivity(this)
|
||||
// TestProcessActivity.startActivity(this)
|
||||
GuruAnalytics.Builder(this)
|
||||
.setBatchLimit(25)
|
||||
.setUploadPeriodInSeconds(60)
|
||||
.setStartUploadDelayInSecond(3)
|
||||
.setEventExpiredInDays(7)
|
||||
.isPersistableLog(true)
|
||||
.setEventHandlerCallback(eventHandler)
|
||||
.isInitPeriodicWork(true)
|
||||
.isDebug(BuildConfig.DEBUG)
|
||||
// .setUploadEventBaseUrl("https://www.baidu.com")
|
||||
// .setFgEventPeriodInSeconds(60L)
|
||||
.setXAppId("test_x_app_id")
|
||||
.setXDeviceInfo("test_x_device_info")
|
||||
.setMainProcess("com.example.guruanalytics")
|
||||
// .isEnableCronet(true)
|
||||
.setUploadEventBaseUrl("https://collect4.fungame.cloud")
|
||||
.setDnsMode(DnsMode.COMPOSITE)
|
||||
.build()
|
||||
|
||||
Log.w("GuruAnalytics", "GuruAnalytics.INSTANCE: completed")
|
||||
}
|
||||
findViewById<TextView>(R.id.tvLocalLog).setOnClickListener {
|
||||
val startTime = System.currentTimeMillis()
|
||||
GuruAnalytics.INSTANCE.zipLogs(this)
|
||||
Log.i("get_local_log", "${System.currentTimeMillis() - startTime}")
|
||||
// GuruAnalytics.INSTANCE.zipLogs(this)
|
||||
// Log.i("get_local_log", "${System.currentTimeMillis() - startTime}")
|
||||
// val request = OneTimeWorkRequestBuilder<AnalyticsWorker>(
|
||||
// ).build()
|
||||
//// .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.MINUTES)
|
||||
//// .addTag(AnalyticsWorker.WORKER_TAG)
|
||||
//// .setConstraints(constraints)
|
||||
//// .build()
|
||||
// WorkManager.getInstance(this)
|
||||
// .enqueue(
|
||||
// request
|
||||
// )
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.setTriggerContentMaxDelay(1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
val request = OneTimeWorkRequestBuilder<ActiveWorker>()
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES)
|
||||
.addTag(ActiveWorker.WORKER_TAG)
|
||||
.setConstraints(constraints)
|
||||
.setInitialDelay(5, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(this)
|
||||
.enqueueUniqueWork(
|
||||
ActiveWorker.WORKER_NAME,
|
||||
ExistingWorkPolicy.KEEP,
|
||||
request
|
||||
)
|
||||
Log.i(
|
||||
"Worker",
|
||||
"dispatchActiveWorker"
|
||||
)
|
||||
}
|
||||
findViewById<TextView>(R.id.tvEventStatistic).setOnClickListener {
|
||||
GuruAnalytics.INSTANCE.getEventsStatics().run {
|
||||
GuruAnalytics.INSTANCE.snapshotAnalyticsAudit().run {
|
||||
Log.i(
|
||||
"UploadEventDaemon_main",
|
||||
"$eventCountAll $eventCountDeleted $eventCountUploaded"
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
val eventStatistic = GuruAnalytics.INSTANCE.getEventsStatics()
|
||||
|
||||
// val restricted = NetworkUsageUtils.isBackgroundDataRestricted(this)
|
||||
|
||||
Log.i("getEventsStatics", "eventStatistic:$eventStatistic")
|
||||
|
||||
NetworkUsageUtils.dumpCap(this)
|
||||
}
|
||||
findViewById<TextView>(R.id.tvEventEmergence).setOnClickListener {
|
||||
val map = mutableMapOf<String, Any>()
|
||||
|
|
@ -77,7 +265,7 @@ class MainActivity : AppCompatActivity() {
|
|||
)
|
||||
}
|
||||
findViewById<TextView>(R.id.tvBaseUrl).setOnClickListener {
|
||||
GuruAnalytics.INSTANCE.setUploadEventBaseUrl(this, "https://www.castbox.fm/")
|
||||
// GuruAnalytics.INSTANCE.setUploadEventBaseUrl(this, "https://www.castbox.fm/")
|
||||
}
|
||||
|
||||
val tvEnable = findViewById<TextView>(R.id.tvEnable)
|
||||
|
|
@ -88,11 +276,20 @@ class MainActivity : AppCompatActivity() {
|
|||
tvEnable.text = if (enableUpload) "Enable Upload" else "Disable Upload"
|
||||
}
|
||||
|
||||
val tvClearStatistic = findViewById<TextView>(R.id.tvClearStatistic)
|
||||
tvClearStatistic?.setOnClickListener {
|
||||
GuruAnalytics.INSTANCE.clearStatistic(this)
|
||||
}
|
||||
|
||||
|
||||
GuruAnalytics.INSTANCE.setScreen("main")
|
||||
GuruAnalytics.INSTANCE.setAdId("AD_ID_01")
|
||||
GuruAnalytics.INSTANCE.setFirebaseId("FIREBASE_ID")
|
||||
GuruAnalytics.INSTANCE.setUid("MBK-YYYYY")
|
||||
GuruAnalytics.INSTANCE.setUserProperty("uid", "110051")
|
||||
GuruAnalytics.INSTANCE.setUserProperty("age", "12")
|
||||
GuruAnalytics.INSTANCE.setUserProperty("sex", "male")
|
||||
Log.d("Test", "setAdId")
|
||||
|
||||
val properties = GuruAnalytics.INSTANCE.peakUserProperties()
|
||||
Log.i("peakUserProperties", "properties:$properties")
|
||||
|
|
@ -100,11 +297,6 @@ class MainActivity : AppCompatActivity() {
|
|||
GuruAnalytics.INSTANCE.getUserProperties {
|
||||
Log.i("getUserProperties", "properties:$it")
|
||||
}
|
||||
|
||||
tvEnable?.postDelayed({
|
||||
val snapshot = GuruAnalytics.INSTANCE.snapshotAnalyticsAudit()
|
||||
Log.i("snapshotAnalyticsAudit", "snapshot:$snapshot")
|
||||
}, 10_000)
|
||||
}
|
||||
|
||||
private val eventHandler: (Int, String?) -> Unit = { code, ext ->
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ class TestProcessActivity : AppCompatActivity() {
|
|||
.setXAppId("test_x_app_id")
|
||||
.setXDeviceInfo("test_x_device_info")
|
||||
.setMainProcess("com.example.guruanalytics")
|
||||
|
||||
.setUploadIpAddress(listOf("13.248.248.135", "3.33.195.44"))
|
||||
.build()
|
||||
|
||||
findViewById<TextView>(R.id.tvFinish).setOnClickListener {
|
||||
|
|
|
|||
|
|
@ -11,72 +11,82 @@
|
|||
android:id="@+id/tvSetProperty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="50dp"
|
||||
android:padding="10dp"
|
||||
android:text="SET PROPERTY"
|
||||
android:layout_gravity="center_horizontal"/>
|
||||
android:text="SET PROPERTY" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLogEvent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="30dp"
|
||||
android:padding="10dp"
|
||||
android:text="LOG EVENT"
|
||||
android:layout_gravity="center_horizontal"/>
|
||||
android:text="LOG EVENT" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEventEmergence"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="30dp"
|
||||
android:padding="10dp"
|
||||
android:text="Event_EMERGENCE"
|
||||
android:layout_gravity="center_horizontal"/>
|
||||
android:text="Event_EMERGENCE" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvOpenTestProcessActivity"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="30dp"
|
||||
android:padding="10dp"
|
||||
android:text="Open TestProcessActivity"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
android:text="Open TestProcessActivity" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLocalLog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="30dp"
|
||||
android:padding="10dp"
|
||||
android:text="get local log file"
|
||||
android:layout_gravity="center_horizontal"/>
|
||||
android:text="get local log file" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEventStatistic"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="30dp"
|
||||
android:padding="10dp"
|
||||
android:text="Event Statistic"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
android:text="Event Statistic" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvBaseUrl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="30dp"
|
||||
android:padding="10dp"
|
||||
android:text="Update BaseUrl"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
android:text="Update BaseUrl" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEnable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="30dp"
|
||||
android:padding="10dp"
|
||||
android:text="Enable Upload"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
android:text="Enable Upload" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvClearStatistic"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="30dp"
|
||||
android:padding="10dp"
|
||||
android:text="Clear Statistic"
|
||||
android:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.6.10'
|
||||
ext.kotlin_version = '1.9.0'
|
||||
|
||||
repositories {
|
||||
// maven { url 'http://localhost:8081/repository/maven-public/' }
|
||||
|
|
@ -10,7 +10,7 @@ buildscript {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:4.2.1"
|
||||
classpath "com.android.tools.build:gradle:7.1.2"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import java.text.SimpleDateFormat
|
||||
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
|
|
@ -8,6 +10,15 @@ plugins {
|
|||
apply from: 'dependencies.gradle'
|
||||
apply from: 'maven-publish.gradle'
|
||||
|
||||
static def buildTs() {
|
||||
def date = GregorianCalendar.getInstance(TimeZone.getTimeZone('UTC'))
|
||||
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss")
|
||||
String ts = dateFormat.format(date.getTime())
|
||||
|
||||
return "\"" + ts + "\""
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk android.compileSdk
|
||||
|
||||
|
|
@ -18,6 +29,8 @@ android {
|
|||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
buildConfigField "String", "buildTs", buildTs()
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
ext {
|
||||
compiler = [
|
||||
java : JavaVersion.VERSION_1_8,
|
||||
kotlin: '1.6.10'
|
||||
java : JavaVersion.VERSION_17,
|
||||
kotlin: '1.9.0'
|
||||
]
|
||||
|
||||
android = [
|
||||
buildTools: '30.0.3',
|
||||
minSdk : 21,
|
||||
targetSdk : 32,
|
||||
compileSdk: 32
|
||||
targetSdk : 33,
|
||||
compileSdk: 34
|
||||
]
|
||||
|
||||
androidXCoreVersion = '1.7.0'
|
||||
timberVersion = '4.7.1'
|
||||
roomVersion = '2.4.3'
|
||||
roomVersion = '2.6.1'
|
||||
gsonVersion = '2.8.5'
|
||||
|
||||
retrofitVersion = '2.7.1'
|
||||
okhttpVersion = '4.9.3'
|
||||
okhttpVersion = '4.12.0'
|
||||
preferenceVersion = '1.2.0'
|
||||
processVersion = '2.4.0'
|
||||
workVersion = '2.7.1'
|
||||
workVersion = '2.9.0'
|
||||
cronetOkhttpVersion = '0.1.0'
|
||||
playServicesCronetVersion = '18.0.1'
|
||||
|
||||
|
|
@ -47,7 +47,8 @@ ext {
|
|||
]
|
||||
|
||||
okhttpDependencies = [
|
||||
"com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
"com.squareup.okhttp3:okhttp:$okhttpVersion",
|
||||
"com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion"
|
||||
]
|
||||
|
||||
workerDependencies = [
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ publishing { // Repositories *to* which Gradle can publish artifacts
|
|||
maven(MavenPublication) {
|
||||
groupId 'guru.core.analytics'
|
||||
artifactId 'guru_analytics'
|
||||
version '1.0.3' // Your package version
|
||||
version '1.1.0' // Your package version
|
||||
// artifact publishArtifact //Example: *./target/myJavaClasses.jar*
|
||||
// artifact "build/outputs/aar/aar-test-release.aar"//aar包的目录
|
||||
afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) }
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ object Constants {
|
|||
const val SCREEN_W = "screenW"
|
||||
const val OS_VERSION = "osVersion"
|
||||
const val LANGUAGE = "language"
|
||||
const val SDK_INFO = "sdkInfo"
|
||||
}
|
||||
|
||||
object Properties {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package guru.core.analytics
|
||||
|
||||
import android.content.Context
|
||||
import guru.core.analytics.data.api.dns.DnsMode
|
||||
import guru.core.analytics.data.db.model.EventStatistic
|
||||
import guru.core.analytics.data.model.AnalyticsInfo
|
||||
import guru.core.analytics.data.model.AnalyticsOptions
|
||||
|
|
@ -29,6 +30,7 @@ abstract class GuruAnalytics {
|
|||
mainProcess: String? = null,
|
||||
isEnableCronet: Boolean? = null,
|
||||
uploadIpAddress: List<String>? = null,
|
||||
dnsMode: Int? = null
|
||||
)
|
||||
|
||||
abstract fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String)
|
||||
|
|
@ -83,6 +85,10 @@ abstract class GuruAnalytics {
|
|||
|
||||
abstract fun snapshotAnalyticsAudit(): String
|
||||
|
||||
abstract fun clearStatistic(context: Context)
|
||||
|
||||
abstract fun forceUpload(scene: String = "unknown"): Boolean
|
||||
|
||||
companion object {
|
||||
val INSTANCE: GuruAnalytics by lazy() {
|
||||
GuruAnalyticsImpl()
|
||||
|
|
@ -134,6 +140,9 @@ abstract class GuruAnalytics {
|
|||
fun setUploadIpAddress(uploadIpAddress: List<String>) =
|
||||
apply { analyticsInfo.uploadIpAddress = uploadIpAddress }
|
||||
|
||||
fun setDnsMode(@DnsMode dnsMode: Int) =
|
||||
apply { analyticsInfo.dnsMode = dnsMode }
|
||||
|
||||
fun build(): GuruAnalytics {
|
||||
analyticsInfo.run {
|
||||
INSTANCE.initialize(
|
||||
|
|
@ -153,6 +162,7 @@ abstract class GuruAnalytics {
|
|||
mainProcess,
|
||||
isEnableCronet,
|
||||
uploadIpAddress,
|
||||
dnsMode
|
||||
)
|
||||
}
|
||||
return INSTANCE
|
||||
|
|
|
|||
|
|
@ -4,33 +4,36 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import guru.core.analytics.data.api.cronet.CastboxCronetInterceptor
|
||||
import guru.core.analytics.data.api.dns.CompositeDns
|
||||
import guru.core.analytics.data.api.dns.CustomDns
|
||||
import guru.core.analytics.data.api.dns.DnsMode
|
||||
import guru.core.analytics.data.api.dns.GoogleDnsApi
|
||||
import guru.core.analytics.data.api.dns.GoogleDnsApiHost
|
||||
import guru.core.analytics.data.api.dns.StaticDns
|
||||
import guru.core.analytics.data.api.logging.Level
|
||||
import guru.core.analytics.data.api.logging.LoggingInterceptor
|
||||
import guru.core.analytics.data.local.PreferencesManager
|
||||
import guru.core.analytics.data.model.GuruAnalyticsAudit
|
||||
import guru.core.analytics.handler.AnalyticsCode
|
||||
import guru.core.analytics.handler.EventHandler
|
||||
import guru.core.analytics.utils.AndroidUtils
|
||||
import guru.core.analytics.utils.DateTimeUtils
|
||||
import guru.core.analytics.utils.GsonUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Dispatcher
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import okhttp3.internal.platform.Platform
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// Todo: 该类都需要重构,业务不清晰,不符合打点库使用
|
||||
object ServiceLocator {
|
||||
|
||||
private var debug = false
|
||||
|
|
@ -38,11 +41,16 @@ object ServiceLocator {
|
|||
@Volatile
|
||||
private var guruRepository: GuruRepository? = null
|
||||
|
||||
@Volatile
|
||||
private var googleDnsApi: GoogleDnsApi? = null
|
||||
|
||||
private val headerParams = mutableMapOf<String, String>()
|
||||
|
||||
private val compositeDns: CompositeDns by lazy {
|
||||
return@lazy CompositeDns()
|
||||
}
|
||||
|
||||
private var isEnabledCronet: Boolean = false
|
||||
|
||||
private var dnsMode: Int = 0
|
||||
|
||||
private var uploadIpAddress: List<String>? = null
|
||||
|
||||
fun addHeaderParam(key: String, value: String?) {
|
||||
|
|
@ -54,10 +62,36 @@ object ServiceLocator {
|
|||
this.debug = debug
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setUploadIpAddress(ipList: List<String>?) {
|
||||
if (compositeDns.ipAddress != ipList) {
|
||||
compositeDns.setCandidateIpAddress(ipAddress = ipList)
|
||||
}
|
||||
uploadIpAddress = ipList
|
||||
}
|
||||
|
||||
fun setCronet(isEnabledCronet: Boolean) {
|
||||
this.isEnabledCronet = isEnabledCronet
|
||||
}
|
||||
|
||||
fun setDnsMode(dnsMode: Int) {
|
||||
this.dnsMode = dnsMode
|
||||
GuruAnalyticsAudit.dnsMode = dnsMode
|
||||
}
|
||||
|
||||
fun preloadDns(hostname: String?) {
|
||||
Timber.tag("ServiceLocator").i("preloadDns: $hostname")
|
||||
if (dnsMode == DnsMode.COMPOSITE && !hostname.isNullOrBlank()) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val result = compositeDns.lookup(hostname)
|
||||
Timber.tag(CompositeDns.tag).i("preloadDns: $hostname, result: $result")
|
||||
} catch (throwable: Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun provideGuruRepository(context: Context, baseUri: Uri? = null): GuruRepository {
|
||||
synchronized(this) {
|
||||
return guruRepository
|
||||
|
|
@ -97,14 +131,8 @@ object ServiceLocator {
|
|||
guruRepository?.analyticsApi = createAnalyticsApi(context, baseUrl)
|
||||
}
|
||||
|
||||
fun provideGoogleDnsApi(context: Context): GoogleDnsApi {
|
||||
synchronized(this) {
|
||||
return googleDnsApi
|
||||
?: createGoogleDnsApi(context).apply { googleDnsApi = this }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createGoogleDnsApi(context: Context): GoogleDnsApi {
|
||||
fun createGoogleDnsApi(context: Context): GoogleDnsApi {
|
||||
return GoogleDnsApi.Creator.newInstance(
|
||||
Retrofit.Builder()
|
||||
.baseUrl(GoogleDnsApiHost.API)
|
||||
|
|
@ -120,30 +148,37 @@ object ServiceLocator {
|
|||
|
||||
private fun createOkHttpClient(
|
||||
context: Context,
|
||||
readTimeOut: Long = 30L,
|
||||
writeTimeOut: Long = 30L
|
||||
readTimeOut: Long = 90L,
|
||||
writeTimeOut: Long = 90L,
|
||||
): OkHttpClient {
|
||||
val dns = when (dnsMode) {
|
||||
DnsMode.COMPOSITE -> compositeDns
|
||||
DnsMode.STATIC -> StaticDns(uploadIpAddress)
|
||||
else -> CustomDns(context, uploadIpAddress = uploadIpAddress)
|
||||
}
|
||||
val builder = OkHttpClient.Builder()
|
||||
.dispatcher(Dispatcher().apply {
|
||||
maxRequests = 128
|
||||
maxRequestsPerHost = 10
|
||||
maxRequestsPerHost = 5
|
||||
})
|
||||
.dns(CustomDns(context, uploadIpAddress))
|
||||
.connectTimeout(20L, TimeUnit.SECONDS)
|
||||
.dns(dns)
|
||||
.connectTimeout(90L, TimeUnit.SECONDS)
|
||||
.readTimeout(readTimeOut, TimeUnit.SECONDS)
|
||||
.writeTimeout(writeTimeOut, TimeUnit.SECONDS)
|
||||
.addInterceptor(createCacheControlInterceptor(context))
|
||||
.addInterceptor(createAnalyticsApiInterceptor())
|
||||
.addInterceptor(createLoggingInterceptor())
|
||||
.addInterceptor(createCronetInterceptor())
|
||||
if (isEnabledCronet) {
|
||||
builder.addInterceptor(createCronetInterceptor())
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun createDnsOkHttpClient(context: Context): OkHttpClient {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.addInterceptor(createCacheControlInterceptor(context))
|
||||
.connectTimeout(90L, TimeUnit.SECONDS)
|
||||
.readTimeout(90L, TimeUnit.SECONDS)
|
||||
.writeTimeout(90L, TimeUnit.SECONDS)
|
||||
.addInterceptor(createLoggingInterceptor())
|
||||
.addInterceptor(createCronetInterceptor())
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
|
@ -164,32 +199,6 @@ object ServiceLocator {
|
|||
return CastboxCronetInterceptor()
|
||||
}
|
||||
|
||||
private fun createCacheControlInterceptor(context: Context) = Interceptor { chain ->
|
||||
try {
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
when {
|
||||
originalResponse.headers.names().contains("Cache-Control") -> {
|
||||
return@Interceptor originalResponse.newBuilder().build()
|
||||
}
|
||||
AndroidUtils.isInternetAvailable(context) -> {
|
||||
val maxAge = 60 * 5 // read from cache for 5 minute
|
||||
return@Interceptor originalResponse.newBuilder()
|
||||
.header("Cache-Control", "public, max-age=$maxAge")
|
||||
.build()
|
||||
}
|
||||
else -> {
|
||||
val maxStale = 60 * 60 * 24 * 28 // tolerate 4-weeks stale
|
||||
return@Interceptor originalResponse.newBuilder()
|
||||
.header("Cache-Control", "public, only-if-cached, max-stale=$maxStale")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_CACHE_CONTROL, e.message)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAnalyticsApiInterceptor(): Interceptor {
|
||||
return Interceptor { chain ->
|
||||
val request = chain.request()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package guru.core.analytics.data.api.dns
|
||||
|
||||
import okhttp3.Dns
|
||||
import java.net.InetAddress
|
||||
|
||||
class CandidateDns(private var ipAddress: List<String>? = null) : Dns {
|
||||
private fun convert(ip: String): InetAddress? {
|
||||
return runCatching {
|
||||
val byteArr = ip.split(".").map { Integer.parseInt(it).toByte() }.toByteArray()
|
||||
InetAddress.getByAddress(byteArr)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun setIpAddress(ipAddress: List<String>? = null) {
|
||||
this.ipAddress = ipAddress
|
||||
}
|
||||
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
val resultIpList = ipAddress?.mapNotNull { convert(it) }
|
||||
if (!resultIpList.isNullOrEmpty()) {
|
||||
return resultIpList
|
||||
}
|
||||
return Dns.SYSTEM.lookup(hostname)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package guru.core.analytics.data.api.dns
|
||||
|
||||
import guru.core.analytics.data.model.GuruAnalyticsAudit
|
||||
import guru.core.analytics.handler.AnalyticsCode
|
||||
import guru.core.analytics.handler.EventHandler
|
||||
import okhttp3.Cache
|
||||
import okhttp3.Dns
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
|
||||
class CompositeDns(
|
||||
val ipAddress: List<String>? = null
|
||||
) : Dns {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val tag = "CompositeDns"
|
||||
}
|
||||
|
||||
private val dnsOverHttps: Dns by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
val dnsCache = Cache(File("cacheDir", "dnscache"), 10 * 1024 * 1024)
|
||||
val bootstrapClient = OkHttpClient.Builder().cache(dnsCache).build()
|
||||
return@lazy DnsOverHttps.Builder().client(bootstrapClient)
|
||||
.url("https://dns.google/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(InetAddress.getByName("8.8.4.4"), InetAddress.getByName("8.8.8.8"))
|
||||
.build()
|
||||
}
|
||||
|
||||
private val googleDns: GoogleDns by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
GoogleDns()
|
||||
}
|
||||
|
||||
private val candidateDns: CandidateDns by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
CandidateDns(ipAddress)
|
||||
}
|
||||
|
||||
private val lookupService: List<(hostname: String) -> List<InetAddress>> by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
listOf(
|
||||
{ hostname -> dnsOverHttps.lookup(hostname) },
|
||||
{ hostname -> candidateDns.lookup(hostname) }
|
||||
)
|
||||
}
|
||||
|
||||
fun setCandidateIpAddress(ipAddress: List<String>? = null) {
|
||||
candidateDns.setIpAddress(ipAddress)
|
||||
}
|
||||
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
var primaryException: UnknownHostException? = null
|
||||
for ((depth, invokeLookup) in lookupService.withIndex()) {
|
||||
try {
|
||||
val ipList = invokeLookup(hostname)
|
||||
Timber.tag(tag).i("lookup: $hostname, depth: $depth, ipList: $ipList")
|
||||
GuruAnalyticsAudit.serverIp = ipList.toString()
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_LOOKUP, "[Composite($depth)]:${ipList}")
|
||||
return ipList
|
||||
} catch (e: UnknownHostException) {
|
||||
primaryException = primaryException ?: e
|
||||
}
|
||||
}
|
||||
throw (primaryException ?: UnknownHostException("Broken dns lookup of $hostname"))
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import com.google.gson.reflect.TypeToken
|
||||
import guru.core.analytics.data.api.ServiceLocator
|
||||
import guru.core.analytics.data.local.PreferencesManager
|
||||
import guru.core.analytics.data.model.GuruAnalyticsAudit
|
||||
import guru.core.analytics.handler.AnalyticsCode
|
||||
import guru.core.analytics.handler.EventHandler
|
||||
import guru.core.analytics.utils.GsonUtil
|
||||
|
|
@ -14,18 +15,28 @@ import java.net.UnknownHostException
|
|||
|
||||
class CustomDns(private val context: Context, private val uploadIpAddress: List<String>? = null) : Dns {
|
||||
|
||||
@Volatile
|
||||
private var googleDnsApi: GoogleDnsApi? = null
|
||||
|
||||
private val cachedHostAddress by lazy {
|
||||
val hostAddressJson = PreferencesManager.getInstance(context).hostAddressJson
|
||||
return@lazy runCatching {
|
||||
if (!hostAddressJson.isNullOrBlank()) {
|
||||
val mapType = object: TypeToken<Map<String, List<String>>>() {}.type
|
||||
val mapType = object : TypeToken<Map<String, List<String>>>() {}.type
|
||||
GsonUtil.gson.fromJson(hostAddressJson, mapType) as? MutableMap<String, List<String>>
|
||||
} else null
|
||||
}.getOrNull() ?: mutableMapOf()
|
||||
}
|
||||
|
||||
fun provideGoogleDnsApi(context: Context): GoogleDnsApi {
|
||||
synchronized(this) {
|
||||
return googleDnsApi
|
||||
?: ServiceLocator.createGoogleDnsApi(context).apply { googleDnsApi = this }
|
||||
}
|
||||
}
|
||||
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
return try {
|
||||
val ipList = try {
|
||||
Dns.SYSTEM.lookup(hostname).also { list ->
|
||||
cacheHostAddress(hostname, list.map { it.hostAddress })
|
||||
}
|
||||
|
|
@ -41,10 +52,13 @@ class CustomDns(private val context: Context, private val uploadIpAddress: List<
|
|||
}
|
||||
}
|
||||
}
|
||||
GuruAnalyticsAudit.serverIp = ipList.toString()
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_LOOKUP, "[Custom]:${ipList}")
|
||||
return ipList
|
||||
}
|
||||
|
||||
private fun lookupByGoogleDns(hostname: String): List<InetAddress> {
|
||||
val dnsApi = ServiceLocator.provideGoogleDnsApi(context)
|
||||
val dnsApi = provideGoogleDnsApi(context)
|
||||
val ipList = runBlocking {
|
||||
return@runBlocking dnsApi.ip(hostname).answer?.toMutableList()
|
||||
?.filter { it.type == 1 }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package guru.core.analytics.data.api.dns
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
import guru.core.analytics.data.db.model.EventPriority
|
||||
|
||||
|
||||
@IntDef(DnsMode.DEFAULT, DnsMode.COMPOSITE, DnsMode.STATIC)
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Target(
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.LOCAL_VARIABLE,
|
||||
AnnotationTarget.CLASS
|
||||
)
|
||||
annotation class DnsMode {
|
||||
companion object {
|
||||
const val DEFAULT = 0
|
||||
const val COMPOSITE = 1
|
||||
const val STATIC = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package guru.core.analytics.data.api.dns
|
||||
|
||||
import guru.core.analytics.data.api.logging.Level
|
||||
import guru.core.analytics.data.api.logging.LoggingInterceptor
|
||||
import guru.core.analytics.utils.GsonUtil
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Dns
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.internal.platform.Platform
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class GoogleDns : Dns {
|
||||
private val loggingInterceptor by lazy {
|
||||
LoggingInterceptor.Builder()
|
||||
.setLevel(Level.BASIC)
|
||||
.log(Platform.INFO)
|
||||
.request("DnsRequest")
|
||||
.response("DnsResponse")
|
||||
.build()
|
||||
}
|
||||
|
||||
private val googleDnsOkHttpClient by lazy {
|
||||
return@lazy OkHttpClient.Builder()
|
||||
.connectTimeout(90L, TimeUnit.SECONDS)
|
||||
.readTimeout(90L, TimeUnit.SECONDS)
|
||||
.writeTimeout(90L, TimeUnit.SECONDS)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val googleDnsApi: GoogleDnsApi by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
return@lazy GoogleDnsApi.Creator.newInstance(
|
||||
Retrofit.Builder()
|
||||
.baseUrl(GoogleDnsApiHost.API)
|
||||
.client(googleDnsOkHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(GsonUtil.gson))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun convert(ip: String): InetAddress? {
|
||||
return runCatching {
|
||||
val byteArr = ip.split(".").map { Integer.parseInt(it).toByte() }.toByteArray()
|
||||
InetAddress.getByAddress(byteArr)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
val ipList = runBlocking {
|
||||
googleDnsApi.ip(hostname).answer?.toMutableList()
|
||||
?.filter { it.type == 1 }
|
||||
?.mapNotNull { it.data }
|
||||
|
||||
}
|
||||
|
||||
if (!ipList.isNullOrEmpty()) {
|
||||
val resultIpList = ipList.mapNotNull { convert(it) }
|
||||
if (resultIpList.isNotEmpty()) {
|
||||
return resultIpList
|
||||
}
|
||||
}
|
||||
throw UnknownHostException("Broken Google dns lookup of $hostname")
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package guru.core.analytics.data.api.dns
|
||||
|
||||
import guru.core.analytics.data.model.GuruAnalyticsAudit
|
||||
import guru.core.analytics.handler.AnalyticsCode
|
||||
import guru.core.analytics.handler.EventHandler
|
||||
import okhttp3.Dns
|
||||
import java.net.InetAddress
|
||||
|
||||
class StaticDns(private val ipAddress: List<String>? = null) : Dns {
|
||||
|
||||
private fun convert(ip: String): InetAddress? {
|
||||
return runCatching {
|
||||
val byteArr = ip.split(".").map { Integer.parseInt(it).toByte() }.toByteArray()
|
||||
InetAddress.getByAddress(byteArr)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
val resultIpList = ipAddress?.mapNotNull { convert(it) }
|
||||
if (!resultIpList.isNullOrEmpty()) {
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_LOOKUP, "[Static]:${resultIpList}")
|
||||
GuruAnalyticsAudit.serverIp = resultIpList.toString()
|
||||
return resultIpList
|
||||
}
|
||||
val ipList = Dns.SYSTEM.lookup(hostname)
|
||||
GuruAnalyticsAudit.serverIp = ipList.toString()
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_LOOKUP, "[Default]:${ipList}")
|
||||
return ipList
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import guru.core.analytics.data.db.migrations.MIGRATIONS
|
|||
import guru.core.analytics.data.db.model.EventEntity
|
||||
import guru.core.analytics.data.db.utils.Converters
|
||||
import guru.core.analytics.data.db.utils.TransactionResult
|
||||
import guru.core.analytics.data.db.utils.runInTransactionEx
|
||||
import io.reactivex.Maybe
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.SoftReference
|
||||
|
|
@ -66,12 +65,12 @@ abstract class GuruAnalyticsDatabase : RoomDatabase() {
|
|||
.build()
|
||||
}
|
||||
|
||||
fun <T> runInTransaction(callback: () -> TransactionResult<T>, defVal: T?): Maybe<T> {
|
||||
return INSTANCE?.runInTransactionEx(callback, defVal) ?: Maybe.empty()
|
||||
}
|
||||
|
||||
fun <T> runInTransaction(callback: () -> TransactionResult<T>): Maybe<T> {
|
||||
return INSTANCE?.runInTransactionEx(callback) ?: Maybe.empty()
|
||||
}
|
||||
// fun <T> runInTransaction(callback: () -> TransactionResult<T>, defVal: T?): Maybe<T> {
|
||||
// return INSTANCE?.runInTransactionEx(callback, defVal) ?: Maybe.empty()
|
||||
// }
|
||||
//
|
||||
// fun <T> runInTransaction(callback: () -> TransactionResult<T>): Maybe<T> {
|
||||
// return INSTANCE?.runInTransactionEx(callback) ?: Maybe.empty()
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -51,13 +51,6 @@ abstract class EventDao {
|
|||
@Transaction
|
||||
open fun loadAndMarkUploadEvents(limit: Int): List<EventEntity> {
|
||||
val events = getEvents(limit).toMutableList()
|
||||
// if (events.isNotEmpty()) {
|
||||
// Timber.tag(TAG).d("loadAndMarkUploadEvents limit:$limit size:${events.size}")
|
||||
// FgEventHelper.getInstance().getFgEvent()?.let { entity ->
|
||||
// addEvent(entity)
|
||||
// events.add(entity)
|
||||
// }
|
||||
// }
|
||||
updateEventUploading(events)
|
||||
return events
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,8 @@ data class EventStatistic(
|
|||
val eventCountAll: Int = 0,
|
||||
val eventCountDeleted: Int = 0,
|
||||
val eventCountUploaded: Int = 0,
|
||||
)
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "eventCountAll=$eventCountAll, eventCountDeleted=$eventCountDeleted, eventCountUploaded=$eventCountUploaded"
|
||||
}
|
||||
}
|
||||
|
|
@ -30,31 +30,31 @@ data class TransactionResult<T>(
|
|||
}
|
||||
}
|
||||
|
||||
fun <T> RoomDatabase.runInTransactionEx(
|
||||
callback: () -> TransactionResult<T>,
|
||||
defVal: T?
|
||||
): Maybe<T> {
|
||||
return Maybe.create { emitter ->
|
||||
val result = this.runInTransaction(callback)
|
||||
?: TransactionResult(defVal, ResultBehavior.SUCCESS)
|
||||
when (result.behavior) {
|
||||
ResultBehavior.SUCCESS -> {
|
||||
val value = result.value
|
||||
if (value != null) {
|
||||
emitter.onSuccess(value)
|
||||
}
|
||||
emitter.onComplete()
|
||||
}
|
||||
ResultBehavior.ERROR -> {
|
||||
emitter.onError(DatabaseException("runInTransaction error!", result.cause))
|
||||
}
|
||||
else -> {
|
||||
emitter.onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//fun <T> RoomDatabase.runInTransactionEx(
|
||||
// callback: () -> TransactionResult<T>,
|
||||
// defVal: T?
|
||||
//): Maybe<T> {
|
||||
// return Maybe.create { emitter ->
|
||||
// val result = this.runInTransaction(callback)
|
||||
// ?: TransactionResult(defVal, ResultBehavior.SUCCESS)
|
||||
// when (result.behavior) {
|
||||
// ResultBehavior.SUCCESS -> {
|
||||
// val value = result.value
|
||||
// if (value != null) {
|
||||
// emitter.onSuccess(value)
|
||||
// }
|
||||
// emitter.onComplete()
|
||||
// }
|
||||
// ResultBehavior.ERROR -> {
|
||||
// emitter.onError(DatabaseException("runInTransaction error!", result.cause))
|
||||
// }
|
||||
// else -> {
|
||||
// emitter.onComplete()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
fun <T> RoomDatabase.runInTransactionEx(callback: () -> TransactionResult<T>): Maybe<T> {
|
||||
return runInTransactionEx(callback, null)
|
||||
}
|
||||
//fun <T> RoomDatabase.runInTransactionEx(callback: () -> TransactionResult<T>): Maybe<T> {
|
||||
// return runInTransactionEx(callback, null)
|
||||
//}
|
||||
|
|
@ -2,6 +2,7 @@ package guru.core.analytics.data.local
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import guru.core.analytics.data.api.dns.DnsMode
|
||||
|
||||
class PreferencesManager private constructor(
|
||||
context: Context,
|
||||
|
|
@ -49,4 +50,15 @@ class PreferencesManager private constructor(
|
|||
var uploadEventBaseUrl: String? by bind("update_event_base_url", "")
|
||||
var totalDurationFgEvent: Long? by bind("total_duration_fg_event", 0L)
|
||||
var hostAddressJson: String? by bind("host_address", "")
|
||||
var uploadIpAddressList: String? by bind("upload_ip_address", "")
|
||||
var useCompositeDns: Boolean? by bind("use_composite_dns", false)
|
||||
var dnsMode: Int? by bind("dns_mode", DnsMode.DEFAULT)
|
||||
|
||||
var firebaseId: String? by bind("firebase_id", "")
|
||||
var userId: String? by bind("user_id", "")
|
||||
var adjustId: String? by bind("adjust_id", "")
|
||||
var deviceId: String? by bind("device_id", "")
|
||||
var adId: String? by bind("ad_id", "")
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,4 +16,5 @@ internal data class AnalyticsInfo(
|
|||
var mainProcess: String? = null,
|
||||
var isEnableCronet: Boolean? = null,
|
||||
var uploadIpAddress: List<String>? = null,
|
||||
var dnsMode: Int? = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package guru.core.analytics.data.model
|
|||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import guru.core.analytics.data.api.dns.DnsMode
|
||||
|
||||
@Keep
|
||||
data class GuruAnalyticsAuditSnapshot(
|
||||
|
|
@ -18,12 +19,16 @@ data class GuruAnalyticsAuditSnapshot(
|
|||
@SerializedName("sessionDeleted") val sessionDeleted: Int = 0,
|
||||
@SerializedName("sessionTotal") val sessionTotal: Int = 0,
|
||||
@SerializedName("uploadReady") val uploadReady: Boolean = false,
|
||||
@SerializedName("dnsMode") val dnsMode: Int = DnsMode.DEFAULT,
|
||||
@SerializedName("serverIp") var serverIp: String = ""
|
||||
)
|
||||
|
||||
object GuruAnalyticsAudit {
|
||||
var initialized: Boolean = false
|
||||
var engineInitialized: Boolean = false
|
||||
var useCronet: Boolean = false
|
||||
var dnsMode: Int = DnsMode.DEFAULT
|
||||
var enabledCronet: Boolean = false
|
||||
var eventDispatcherStarted: Boolean = false
|
||||
var fgHelperInitialized: Boolean = false
|
||||
var connectionState: Boolean = false
|
||||
|
|
@ -40,10 +45,13 @@ object GuruAnalyticsAudit {
|
|||
|
||||
var uploadReady: Boolean = false
|
||||
|
||||
var serverIp: String = ""
|
||||
|
||||
fun snapshot() = GuruAnalyticsAuditSnapshot(
|
||||
initialized = initialized,
|
||||
engineInitialized = engineInitialized,
|
||||
useCronet = useCronet,
|
||||
dnsMode = dnsMode,
|
||||
eventDispatcherStarted = eventDispatcherStarted,
|
||||
fgHelperInitialized = fgHelperInitialized,
|
||||
connectionState = connectionState,
|
||||
|
|
@ -53,7 +61,8 @@ object GuruAnalyticsAudit {
|
|||
sessionUploaded = sessionUploaded,
|
||||
sessionDeleted = sessionDeleted,
|
||||
sessionTotal = sessionTotal,
|
||||
uploadReady = uploadReady
|
||||
uploadReady = uploadReady,
|
||||
serverIp = serverIp
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -2,16 +2,20 @@ package guru.core.analytics.data.store
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import guru.core.analytics.BuildConfig
|
||||
import guru.core.analytics.Constants
|
||||
import guru.core.analytics.handler.AnalyticsCode
|
||||
import guru.core.analytics.handler.EventHandler
|
||||
import guru.core.analytics.utils.AndroidUtils
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
|
||||
object DeviceInfoStore {
|
||||
|
||||
const val SDK_VERSION = "v1.1.0"
|
||||
|
||||
private val deviceInfoSubject: BehaviorSubject<Map<String, Any>> =
|
||||
BehaviorSubject.createDefault(
|
||||
hashMapOf()
|
||||
|
|
@ -37,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}"
|
||||
deviceInfoSubject.onNext(map)
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_DEVICE_INFO, map)
|
||||
Timber.tag("DeviceInfoStore").i("DeviceInfo: $map")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,22 @@
|
|||
package guru.core.analytics.data.store
|
||||
|
||||
import android.content.Context
|
||||
import guru.core.analytics.Constants
|
||||
import guru.core.analytics.data.db.model.Event
|
||||
import guru.core.analytics.data.db.model.EventEntity
|
||||
import guru.core.analytics.data.db.model.EventPriority
|
||||
import guru.core.analytics.data.db.model.ParamValue
|
||||
import guru.core.analytics.data.local.PreferencesManager
|
||||
import guru.core.analytics.data.model.EventItem
|
||||
import guru.core.analytics.data.model.GuruAnalyticsAudit
|
||||
import guru.core.analytics.utils.AndroidUtils
|
||||
import guru.core.analytics.utils.DateTimeUtils
|
||||
import guru.core.analytics.utils.GsonUtil
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
object EventInfoStore {
|
||||
|
||||
|
|
@ -19,6 +25,135 @@ object EventInfoStore {
|
|||
UUID.randomUUID().toString()
|
||||
}
|
||||
|
||||
val initialized = AtomicBoolean(false)
|
||||
|
||||
var preferenceMgr: PreferencesManager? = null
|
||||
|
||||
fun initialize(ctx: Context) {
|
||||
if (initialized.compareAndSet(false, true)) {
|
||||
val appVersion = AndroidUtils.getAppVersion(ctx)
|
||||
setUserProperty("app_version", appVersion)
|
||||
DeviceInfoStore.setDeviceInfo(ctx)
|
||||
preferenceMgr = PreferencesManager.getInstance(ctx).apply {
|
||||
restoreIds(this)
|
||||
GuruAnalyticsAudit.total = this.eventCountAll ?: 0
|
||||
GuruAnalyticsAudit.uploaded = this.eventCountUploaded ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreIds(pm: PreferencesManager) {
|
||||
val firebaseId = try {
|
||||
pm.firebaseId ?: ""
|
||||
} catch (throwable: Throwable) {
|
||||
""
|
||||
}
|
||||
val userId = try {
|
||||
pm.userId ?: ""
|
||||
} catch (throwable: Throwable) {
|
||||
""
|
||||
}
|
||||
val adId = try {
|
||||
pm.adId ?: ""
|
||||
} catch (throwable: Throwable) {
|
||||
""
|
||||
}
|
||||
val adjustId = try {
|
||||
pm.adjustId ?: ""
|
||||
} catch (throwable: Throwable) {
|
||||
""
|
||||
}
|
||||
val deviceId = try {
|
||||
pm.deviceId ?: ""
|
||||
} catch (throwable: Throwable) {
|
||||
""
|
||||
}
|
||||
var restoreIdsMsg = "restoreIds: "
|
||||
if (firebaseId.isNotEmpty()) {
|
||||
val current = getId(Constants.Ids.FIREBASE_ID)
|
||||
if (current != null && current != firebaseId) {
|
||||
pm.firebaseId = current
|
||||
restoreIdsMsg += " [${Constants.Ids.FIREBASE_ID}]:${firebaseId}=>${current} "
|
||||
} else {
|
||||
setIds(Constants.Ids.FIREBASE_ID, firebaseId)
|
||||
restoreIdsMsg += " [${Constants.Ids.FIREBASE_ID}]:${firebaseId} "
|
||||
}
|
||||
} else {
|
||||
val current = getId(Constants.Ids.FIREBASE_ID)
|
||||
if (current != null) {
|
||||
pm.firebaseId = current
|
||||
}
|
||||
}
|
||||
|
||||
if (userId.isNotEmpty()) {
|
||||
val current = getId(Constants.Ids.UID)
|
||||
if (current != null && current != userId) {
|
||||
pm.userId = current
|
||||
restoreIdsMsg += " [${Constants.Ids.UID}]:${userId}=>${current} "
|
||||
} else {
|
||||
setIds(Constants.Ids.UID, userId)
|
||||
restoreIdsMsg += " [${Constants.Ids.UID}]:${userId} "
|
||||
}
|
||||
} else {
|
||||
val current = getId(Constants.Ids.UID)
|
||||
if (current != null) {
|
||||
pm.userId = current
|
||||
}
|
||||
}
|
||||
|
||||
if (adjustId.isNotEmpty()) {
|
||||
val current = getId(Constants.Ids.ADJUST_ID)
|
||||
if (current != null && current != adjustId) {
|
||||
pm.adjustId = current
|
||||
restoreIdsMsg += " [${Constants.Ids.ADJUST_ID}]:${adjustId}=>${current} "
|
||||
} else {
|
||||
setIds(Constants.Ids.ADJUST_ID, adjustId)
|
||||
restoreIdsMsg += " [${Constants.Ids.ADJUST_ID}]:${adjustId} "
|
||||
}
|
||||
} else {
|
||||
val current = getId(Constants.Ids.ADJUST_ID)
|
||||
if (current != null) {
|
||||
pm.adjustId = current
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceId.isNotEmpty()) {
|
||||
val current = getId(Constants.Ids.DEVICE_ID)
|
||||
if (current != null && current != deviceId) {
|
||||
pm.deviceId = current
|
||||
restoreIdsMsg += " [${Constants.Ids.DEVICE_ID}]:${deviceId}=>${current} "
|
||||
} else {
|
||||
setIds(Constants.Ids.DEVICE_ID, deviceId)
|
||||
restoreIdsMsg += " [${Constants.Ids.DEVICE_ID}]:${deviceId} "
|
||||
}
|
||||
} else {
|
||||
val current = getId(Constants.Ids.DEVICE_ID)
|
||||
if (current != null) {
|
||||
pm.deviceId = current
|
||||
}
|
||||
}
|
||||
|
||||
if (adId.isNotEmpty()) {
|
||||
val current = getId(Constants.Ids.AD_ID)
|
||||
if (current != null && current != adId) {
|
||||
pm.adId = current
|
||||
restoreIdsMsg += " [${Constants.Ids.AD_ID}]:${adId}=>${current} "
|
||||
} else {
|
||||
setIds(Constants.Ids.AD_ID, adId)
|
||||
restoreIdsMsg += " [${Constants.Ids.AD_ID}]:${adId} "
|
||||
}
|
||||
} else {
|
||||
val current = getId(Constants.Ids.AD_ID)
|
||||
if (current != null) {
|
||||
pm.adId = current
|
||||
}
|
||||
}
|
||||
|
||||
Timber.tag("EventInfoStore").i(restoreIdsMsg)
|
||||
Timber.tag("EventInfoStore").i(ids.toString())
|
||||
|
||||
}
|
||||
|
||||
private val propertiesSubject: BehaviorSubject<ConcurrentHashMap<String, String>> =
|
||||
BehaviorSubject.createDefault(
|
||||
ConcurrentHashMap<String, String>().apply {
|
||||
|
|
@ -75,24 +210,57 @@ object EventInfoStore {
|
|||
ids = ids.plus(idName to id)
|
||||
}
|
||||
|
||||
internal fun getId(idName: String): String? = ids[idName]
|
||||
|
||||
fun setDeviceId(deviceId: String) {
|
||||
setIds(Constants.Ids.DEVICE_ID, deviceId)
|
||||
if (deviceId.isNotEmpty()) {
|
||||
val prev = getId(Constants.Ids.DEVICE_ID) ?: ""
|
||||
setIds(Constants.Ids.DEVICE_ID, deviceId)
|
||||
if (prev != deviceId) {
|
||||
preferenceMgr?.deviceId = deviceId
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun setUid(uid: String) {
|
||||
setIds(Constants.Ids.UID, uid)
|
||||
if (uid.isNotEmpty()) {
|
||||
val prev = getId(Constants.Ids.UID) ?: ""
|
||||
setIds(Constants.Ids.UID, uid)
|
||||
if (prev != uid) {
|
||||
preferenceMgr?.userId = uid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAdjustId(adjustId: String) {
|
||||
setIds(Constants.Ids.ADJUST_ID, adjustId)
|
||||
if (adjustId.isNotEmpty()) {
|
||||
val prev = getId(Constants.Ids.ADJUST_ID) ?: ""
|
||||
setIds(Constants.Ids.ADJUST_ID, adjustId)
|
||||
if (prev != adjustId) {
|
||||
preferenceMgr?.adjustId = adjustId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAdId(adId: String) {
|
||||
setIds(Constants.Ids.AD_ID, adId)
|
||||
if (adId.isNotEmpty()) {
|
||||
val prev = getId(Constants.Ids.AD_ID) ?: ""
|
||||
setIds(Constants.Ids.AD_ID, adId)
|
||||
if (prev != adId) {
|
||||
preferenceMgr?.adId = adId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setFirebaseId(firebaseId: String) {
|
||||
setIds(Constants.Ids.FIREBASE_ID, firebaseId)
|
||||
if (firebaseId.isNotEmpty()) {
|
||||
val prev = getId(Constants.Ids.FIREBASE_ID) ?: ""
|
||||
setIds(Constants.Ids.FIREBASE_ID, firebaseId)
|
||||
if (prev != firebaseId) {
|
||||
preferenceMgr?.firebaseId = firebaseId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createParamValue(value: Any): ParamValue {
|
||||
|
|
|
|||
|
|
@ -28,9 +28,12 @@ enum class AnalyticsCode(val code: Int) {
|
|||
ERROR_ZIP(107), // zip 错误
|
||||
ERROR_DNS_CACHE(108), // zip 错误
|
||||
ERROR_CRONET_INTERCEPTOR(109),// cronet拦截器
|
||||
ERROR_SESSION_START_ERROR(110),
|
||||
|
||||
EVENT_FIRST_OPEN(1001), // first_open 事件
|
||||
EVENT_FG(1002), // fg 事件
|
||||
EVENT_LOOKUP(1003),
|
||||
EVENT_SESSION_ACTIVE(1004),
|
||||
|
||||
// 初始化进度
|
||||
INIT_STEP_1(100001),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
package guru.core.analytics.impl
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.RxWorker
|
||||
import androidx.work.WorkerParameters
|
||||
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.api.dns.DnsMode
|
||||
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.EventItem
|
||||
import guru.core.analytics.data.model.GuruAnalyticsAudit
|
||||
import guru.core.analytics.data.store.DeviceInfoStore
|
||||
import guru.core.analytics.data.store.EventInfoStore
|
||||
import guru.core.analytics.handler.AnalyticsCode
|
||||
import guru.core.analytics.handler.EventHandler
|
||||
import guru.core.analytics.log.PersistentTree
|
||||
import guru.core.analytics.utils.ApiParamUtils
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
|
||||
class ActiveWorker(
|
||||
val context: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : RxWorker(context.applicationContext, workerParams) {
|
||||
private fun buildGuruRepository(): GuruRepository {
|
||||
val preferenceMgr = PreferencesManager.getInstance(context)
|
||||
|
||||
val dnsMode = preferenceMgr.dnsMode ?: DnsMode.DEFAULT
|
||||
ServiceLocator.setDnsMode(dnsMode)
|
||||
|
||||
val baseUrl = preferenceMgr.uploadEventBaseUrl
|
||||
|
||||
if (baseUrl != null) {
|
||||
try {
|
||||
if (Uri.parse(baseUrl).scheme?.startsWith("http") == true) {
|
||||
return ServiceLocator.provideGuruRepository(context, baseUri = Uri.parse(baseUrl))
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.w("[Worker] Invalid base url: $baseUrl")
|
||||
}
|
||||
}
|
||||
val uploadIpAddress = preferenceMgr.uploadIpAddressList?.split("|")
|
||||
if (!uploadIpAddress.isNullOrEmpty()) {
|
||||
ServiceLocator.setUploadIpAddress(uploadIpAddress)
|
||||
}
|
||||
Timber.tag("ActiveWorker").i("baseUrl: $baseUrl useCompositeDns: $dnsMode uploadIpAddress:$uploadIpAddress")
|
||||
return ServiceLocator.provideGuruRepository(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WORKER_NAME = "SessionActive"
|
||||
const val WORKER_TAG = "SessionActive"
|
||||
}
|
||||
|
||||
override fun createWork(): Single<Result> {
|
||||
if (GuruAnalyticsImpl.timberPlanted.compareAndSet(false, true)) {
|
||||
Timber.plant(PersistentTree(context, debug = true))
|
||||
}
|
||||
Timber.d("Active OnWork...")
|
||||
val item = EventItem(
|
||||
eventName = "session_active", itemCategory = "success", params = mapOf(
|
||||
"uploaded" to GuruAnalyticsAudit.uploaded,
|
||||
"total" to GuruAnalyticsAudit.total,
|
||||
"uid" to (EventInfoStore.getId(Constants.Ids.UID) ?: ""),
|
||||
"fid" to (EventInfoStore.getId(Constants.Ids.FIREBASE_ID) ?: ""),
|
||||
"method" to "worker",
|
||||
"server" to GuruAnalyticsAudit.serverIp
|
||||
)
|
||||
)
|
||||
EventInfoStore.initialize(context)
|
||||
val event = EventInfoStore.deriveEvent(item, priority = EventPriority.HIGH)
|
||||
val param = ApiParamUtils.generateApiParam(listOf(event))
|
||||
return buildGuruRepository().uploadEvents(param).map { Result.success() }.doOnSuccess {
|
||||
Timber.i("active success! $it")
|
||||
EventEngine.logEvent(event)
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_SESSION_ACTIVE)
|
||||
EventEngine.sessionActivated = true
|
||||
}.onErrorReturn {
|
||||
Timber.e("active error! $it")
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_SESSION_START_ERROR, it.message)
|
||||
return@onErrorReturn Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +1,19 @@
|
|||
package guru.core.analytics.impl
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.work.RxWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import guru.core.analytics.GuruAnalytics
|
||||
import guru.core.analytics.data.api.GuruRepository
|
||||
import guru.core.analytics.data.api.ServiceLocator
|
||||
import guru.core.analytics.data.api.dns.DnsMode
|
||||
import guru.core.analytics.data.db.GuruAnalyticsDatabase
|
||||
import guru.core.analytics.data.local.PreferencesManager
|
||||
import guru.core.analytics.data.store.DeviceInfoStore
|
||||
import guru.core.analytics.data.store.EventInfoStore
|
||||
import guru.core.analytics.log.PersistentTree
|
||||
import guru.core.analytics.utils.GsonUtil
|
||||
import io.reactivex.Single
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -14,21 +22,55 @@ class AnalyticsWorker(
|
|||
workerParams: WorkerParameters
|
||||
) : RxWorker(context.applicationContext, workerParams) {
|
||||
companion object {
|
||||
const val WORKER_ID = "GuruAnalytics"
|
||||
const val WORKER_TAG = "Analytics"
|
||||
const val WORKER_NAME = "AnalyticsUploader"
|
||||
const val WORKER_TAG = "AnalyticsUploader"
|
||||
}
|
||||
|
||||
private fun buildGuruRepository(context: Context): GuruRepository {
|
||||
val preferenceMgr = PreferencesManager.getInstance(context)
|
||||
|
||||
val dnsMode = preferenceMgr.dnsMode ?: DnsMode.DEFAULT
|
||||
ServiceLocator.setDnsMode(dnsMode)
|
||||
|
||||
val baseUrl = preferenceMgr.uploadEventBaseUrl
|
||||
|
||||
Timber.tag("AnalyticsWorker").i("baseUrl: $baseUrl useCompositeDns: $dnsMode")
|
||||
if (baseUrl != null) {
|
||||
try {
|
||||
if (Uri.parse(baseUrl).scheme?.startsWith("http") == true) {
|
||||
return ServiceLocator.provideGuruRepository(context, baseUri = Uri.parse(baseUrl))
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.w("[Worker] Invalid base url: $baseUrl")
|
||||
}
|
||||
}
|
||||
val uploadIpAddress = preferenceMgr.uploadIpAddressList?.split("|")
|
||||
if (!uploadIpAddress.isNullOrEmpty()) {
|
||||
ServiceLocator.setUploadIpAddress(uploadIpAddress)
|
||||
}
|
||||
return ServiceLocator.provideGuruRepository(context)
|
||||
}
|
||||
|
||||
override fun createWork(): Single<Result> {
|
||||
Timber.plant(PersistentTree(context))
|
||||
GuruAnalyticsDatabase.initialize(context)
|
||||
DeviceInfoStore.setDeviceInfo(context)
|
||||
Timber.d("OnWork..")
|
||||
val engine = EventEngine(context)
|
||||
return engine.validateEvents().doOnSuccess {
|
||||
Timber.d("validateEvents deleted:${it.first} reset:${it.second}")
|
||||
}.toFlowable().flatMap { engine.uploadEvents(500) }.map { true }.toList()
|
||||
.map { Result.success() }
|
||||
.onErrorReturn { Result.success() }
|
||||
if (GuruAnalyticsImpl.timberPlanted.compareAndSet(false, true)) {
|
||||
Timber.plant(PersistentTree(context, debug = true))
|
||||
}
|
||||
val success = GuruAnalytics.INSTANCE.forceUpload("AnalyticsWorker")
|
||||
Timber.d("OnWork..forceUpload:${success}")
|
||||
return if (success) {
|
||||
Single.just(Result.success())
|
||||
} else {
|
||||
GuruAnalyticsDatabase.initialize(context)
|
||||
EventInfoStore.initialize(context)
|
||||
Timber.d("OnWork..initialized")
|
||||
val engine = EventEngine(context, guruRepository = buildGuruRepository(context))
|
||||
|
||||
engine.validateEvents().doOnSuccess {
|
||||
Timber.d("validateEvents deleted:${it.first} reset:${it.second}")
|
||||
}.toFlowable().flatMap { engine.uploadEvents(500) }.map { true }.toList()
|
||||
.map { Result.success() }
|
||||
.onErrorReturn { Result.success() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -46,7 +46,6 @@ internal class AppLifecycleMonitor internal constructor(context: Context) {
|
|||
Timber.d("${TAG}_ON_PAUSE")
|
||||
fgHelper.stop()
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.LIFECYCLE_PAUSE)
|
||||
|
||||
}
|
||||
|
||||
Lifecycle.Event.ON_STOP -> Timber.d("${TAG}_ON_STOP")
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.net.ConnectivityManager.NetworkCallback
|
|||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import guru.core.analytics.handler.AnalyticsCode
|
||||
import guru.core.analytics.handler.EventHandler
|
||||
import guru.core.analytics.utils.AndroidUtils
|
||||
|
|
@ -14,48 +15,48 @@ import io.reactivex.Flowable
|
|||
import io.reactivex.subjects.BehaviorSubject
|
||||
import timber.log.Timber
|
||||
|
||||
object ConnectionStateMonitor : NetworkCallback() {
|
||||
class ConnectionStateMonitor(private val context: Context) : NetworkCallback() {
|
||||
|
||||
private const val TAG = "ConnectionStateMonitor"
|
||||
private val TAG = "ConnectionStateMonitor"
|
||||
|
||||
private var networkRequest: NetworkRequest? = null
|
||||
private val connectStateSubject: BehaviorSubject<Boolean> = BehaviorSubject.createDefault(false)
|
||||
private val networkChangedSubject: BehaviorSubject<Int> = BehaviorSubject.createDefault(0)
|
||||
|
||||
val connectStateFlowable: Flowable<Boolean>
|
||||
get() = connectStateSubject.toFlowable(BackpressureStrategy.DROP)
|
||||
private val connectivityManager by lazy {
|
||||
return@lazy context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
}
|
||||
|
||||
fun bindConnectionStateChanged(context: Context) {
|
||||
connectStateSubject.onNext(AndroidUtils.isInternetAvailable(context))
|
||||
val networkChanged: Flowable<Int>
|
||||
get() = networkChangedSubject.toFlowable(BackpressureStrategy.DROP)
|
||||
|
||||
fun bind() {
|
||||
if (networkRequest == null) {
|
||||
networkRequest = NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.build()
|
||||
}
|
||||
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||
cm?.registerNetworkCallback(networkRequest!!, this)
|
||||
connectivityManager.registerNetworkCallback(networkRequest!!, this)
|
||||
networkChanged()
|
||||
}
|
||||
|
||||
fun unbindConnectionStateChanged(context: Context?) {
|
||||
fun unbind(context: Context?) {
|
||||
val cm = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||
cm?.unregisterNetworkCallback(this)
|
||||
}
|
||||
|
||||
|
||||
private fun networkChanged() {
|
||||
networkChangedSubject.onNext((networkChangedSubject.value ?: 0) + 1)
|
||||
}
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
Timber.d("${TAG}_onAvailable")
|
||||
if (connectStateSubject.value != true) {
|
||||
connectStateSubject.onNext(true)
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.NETWORK_AVAILABLE)
|
||||
}
|
||||
networkChanged()
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
super.onLost(network)
|
||||
Timber.d("${TAG}_onLost")
|
||||
if (connectStateSubject.value != false) {
|
||||
connectStateSubject.onNext(false)
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.NETWORK_LOST)
|
||||
}
|
||||
networkChanged()
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ class PendingEvent(
|
|||
val at = SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
||||
object EventDispatcher : EventDeliver {
|
||||
data object EventDispatcher : EventDeliver {
|
||||
|
||||
private val pendingEvents = ConcurrentLinkedQueue<PendingEvent>()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,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
|
||||
|
|
@ -18,8 +27,8 @@ import guru.core.analytics.data.store.EventInfoStore
|
|||
import guru.core.analytics.handler.AnalyticsCode
|
||||
import guru.core.analytics.handler.EventHandler
|
||||
import guru.core.analytics.log.PersistentTree
|
||||
import guru.core.analytics.utils.AndroidUtils
|
||||
import guru.core.analytics.utils.ApiParamUtils
|
||||
import guru.core.analytics.utils.Connectivity
|
||||
import guru.core.analytics.utils.DateTimeUtils
|
||||
import io.reactivex.BackpressureStrategy
|
||||
import io.reactivex.Flowable
|
||||
|
|
@ -41,16 +50,17 @@ internal class EventEngine internal constructor(
|
|||
private val batchLimit: Int = DEFAULT_BATCH_LIMIT,
|
||||
private val uploadPeriodInSeconds: Long = DEFAULT_UPLOAD_PERIOD_IN_SECONDS,
|
||||
private val eventExpiredInDays: Int = DEFAULT_EVENT_EXPIRED_IN_DAYS,
|
||||
private val uploadEventBaseUri: Uri? = null,
|
||||
private val guruRepository: GuruRepository
|
||||
) : EventDeliver {
|
||||
private val guruRepository: GuruRepository by lazy {
|
||||
ServiceLocator.provideGuruRepository(context.applicationContext, baseUri = uploadEventBaseUri)
|
||||
}
|
||||
|
||||
private val preferencesManager by lazy {
|
||||
PreferencesManager.getInstance(context)
|
||||
}
|
||||
|
||||
private val connectivity: Connectivity by lazy {
|
||||
Connectivity(context)
|
||||
}
|
||||
|
||||
private val lifecycleMonitor = AppLifecycleMonitor(context)
|
||||
|
||||
companion object {
|
||||
|
|
@ -59,12 +69,17 @@ internal class EventEngine internal constructor(
|
|||
const val DEFAULT_BATCH_LIMIT = 25 // 一次上传event最多数量
|
||||
const val DEFAULT_EVENT_EXPIRED_IN_DAYS = 7
|
||||
|
||||
const val SESSION_ACTIVE_INTERVAL = 15 * 60 * 1000 // 15分钟
|
||||
|
||||
|
||||
private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor())
|
||||
private val dbScheduler = Schedulers.from(Executors.newSingleThreadExecutor())
|
||||
|
||||
private val dateTimeFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
|
||||
@Volatile
|
||||
var sessionActivated = false
|
||||
|
||||
|
||||
fun logDebug(message: String, vararg args: Any?) {
|
||||
if (GuruAnalytics.INSTANCE.isDebug()) {
|
||||
|
|
@ -72,20 +87,26 @@ internal class EventEngine internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun logInfo(message: String, vararg args: Any?) {
|
||||
if (GuruAnalytics.INSTANCE.isDebug()) {
|
||||
Timber.tag(TAG).i(message, args)
|
||||
}
|
||||
}
|
||||
|
||||
fun logEvent(entity: EventEntity) {
|
||||
Timber.log(
|
||||
PersistentTree.PRIORITY_EVENT,
|
||||
"[${dateTimeFormatter.format(entity.at)}] ${entity.event} ${entity.json}"
|
||||
PersistentTree.PRIORITY_EVENT, "[${dateTimeFormatter.format(entity.at)}] ${entity.event} ${entity.json}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||
private val pendingEventSubject: PublishSubject<Int> = PublishSubject.create()
|
||||
private val forceUploadSubject: PublishSubject<Boolean> = PublishSubject.create()
|
||||
|
||||
private val started = AtomicBoolean(false)
|
||||
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
|
||||
|
|
@ -96,8 +117,19 @@ internal class EventEngine internal constructor(
|
|||
fun start(startUploadDelay: Long?) {
|
||||
if (started.compareAndSet(false, true)) {
|
||||
prepare()
|
||||
ConnectionStateMonitor.bindConnectionStateChanged(context.applicationContext)
|
||||
connectivity.bind()
|
||||
lifecycleMonitor.initialize()
|
||||
try {
|
||||
if (connectivity.isNetworkAvailable()) {
|
||||
logSessionActive()
|
||||
logDebug("[1] session started!")
|
||||
} else {
|
||||
dispatchActiveWorker()
|
||||
logDebug("dispatchActiveWorker!")
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
logDebug("session started error!")
|
||||
}
|
||||
scheduler.scheduleDirect({
|
||||
EventDispatcher.start()
|
||||
logFirstOpen()
|
||||
|
|
@ -112,8 +144,7 @@ internal class EventEngine internal constructor(
|
|||
private fun logFirstOpen() {
|
||||
if (preferencesManager.isFirstOpen == true) {
|
||||
GuruAnalytics.INSTANCE.logEvent(
|
||||
Constants.Event.FIRST_OPEN,
|
||||
options = AnalyticsOptions(EventPriority.EMERGENCE)
|
||||
Constants.Event.FIRST_OPEN, options = AnalyticsOptions(EventPriority.EMERGENCE)
|
||||
)
|
||||
preferencesManager.isFirstOpen = false
|
||||
|
||||
|
|
@ -121,9 +152,64 @@ internal class EventEngine internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun logSessionActive() {
|
||||
|
||||
val item = EventItem(
|
||||
eventName = "session_active", itemCategory = "success", params = mapOf(
|
||||
"uploaded" to GuruAnalyticsAudit.uploaded,
|
||||
"total" to GuruAnalyticsAudit.total,
|
||||
"uid" to (EventInfoStore.getId(Constants.Ids.UID) ?: ""),
|
||||
"fid" to (EventInfoStore.getId(Constants.Ids.FIREBASE_ID) ?: ""),
|
||||
"method" to "start",
|
||||
"server" to GuruAnalyticsAudit.serverIp
|
||||
)
|
||||
)
|
||||
val event = EventInfoStore.deriveEvent(item, priority = EventPriority.HIGH)
|
||||
val param = ApiParamUtils.generateApiParam(listOf(event))
|
||||
|
||||
guruRepository.uploadEvents(param).map { true }.doOnSuccess {
|
||||
Timber.i("active success! $it")
|
||||
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)
|
||||
if (!sessionActivated) {
|
||||
dispatchActiveWorker()
|
||||
}
|
||||
return@onErrorReturn false
|
||||
}.subscribeOn(Schedulers.io()).subscribe()
|
||||
}
|
||||
|
||||
private fun dispatchActiveWorker() {
|
||||
Timber.e("dispatchActiveWorker...")
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.build()
|
||||
val request = OneTimeWorkRequestBuilder<ActiveWorker>()
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES)
|
||||
.addTag(ActiveWorker.WORKER_TAG)
|
||||
.setConstraints(constraints)
|
||||
.setInitialDelay(5, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
try {
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniqueWork(
|
||||
ActiveWorker.WORKER_NAME,
|
||||
ExistingWorkPolicy.KEEP,
|
||||
request
|
||||
)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.tag("EventEngine").i("dispatchActiveWorker error: $throwable")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startWork() {
|
||||
Timber.d("startWork")
|
||||
pollEvents()
|
||||
forceUpload()
|
||||
forceUpload("startWork")
|
||||
Timber.d("UploadEventDaemon started!!")
|
||||
}
|
||||
|
||||
|
|
@ -149,28 +235,20 @@ internal class EventEngine internal constructor(
|
|||
|
||||
private fun pollEvents() {
|
||||
logDebug("pollEvents()!! $uploadPeriodInSeconds $batchLimit")
|
||||
val flowable =
|
||||
pendingEventSubject.buffer(uploadPeriodInSeconds, TimeUnit.SECONDS, batchLimit)
|
||||
.toFlowable(BackpressureStrategy.DROP)
|
||||
.doOnNext {
|
||||
logDebug("pendingEvent ${it.size}")
|
||||
}
|
||||
val networkFlowable = ConnectionStateMonitor.connectStateFlowable.doOnNext {
|
||||
GuruAnalyticsAudit.connectionState = it
|
||||
logDebug("network $it")
|
||||
val periodFlowable = Flowable.interval(5, uploadPeriodInSeconds, TimeUnit.SECONDS).onBackpressureDrop()
|
||||
val forceFlowable = forceTriggerSubject.toFlowable(BackpressureStrategy.DROP).map { scene ->
|
||||
logDebug("force trigger: $scene")
|
||||
}
|
||||
val forceFlowable = forceUploadSubject.toFlowable(BackpressureStrategy.DROP)
|
||||
|
||||
val networkFlowable = connectivity.networkAvailableFlowable
|
||||
compositeDisposable.add(Flowable.combineLatest(
|
||||
Flowable.merge(flowable, forceFlowable), networkFlowable
|
||||
) { _, connected -> connected }
|
||||
.filter {
|
||||
logDebug("pollEvent filter $it")
|
||||
return@filter it
|
||||
}
|
||||
.filter { enableUpload }
|
||||
.flatMap { uploadEvents(500) }
|
||||
.subscribe()
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -181,8 +259,7 @@ internal class EventEngine internal constructor(
|
|||
val expiredTs = currentTs - eventExpiredInDays * DateTimeUtils.DAYS_IN_MILLIS
|
||||
|
||||
Timber.d("validateEvents $currentTs $expiredTs $eventExpiredInDays")
|
||||
val pair = GuruAnalyticsDatabase.getInstance().eventDao()
|
||||
.deleteExpiredEvents(expiredTs)
|
||||
val pair = GuruAnalyticsDatabase.getInstance().eventDao().deleteExpiredEvents(expiredTs)
|
||||
// 记录删除的事件数量
|
||||
if (pair.first > 0) {
|
||||
val eventCountDeleted = preferencesManager.eventCountDeleted ?: 0
|
||||
|
|
@ -192,35 +269,32 @@ internal class EventEngine internal constructor(
|
|||
GuruAnalyticsAudit.sessionDeleted += pair.first
|
||||
|
||||
val extMap = mapOf(
|
||||
"expiredCount" to pair.first,
|
||||
"allDeleteCount" to preferencesManager.eventCountDeleted
|
||||
"expiredCount" to pair.first, "allDeleteCount" to preferencesManager.eventCountDeleted
|
||||
)
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.DELETE_EXPIRED, extMap)
|
||||
}
|
||||
emitter.onSuccess(pair)
|
||||
}.subscribeOn(dbScheduler)
|
||||
.onErrorReturn {
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_DELETE_EXPIRED, it.message)
|
||||
Pair(-1, -1)
|
||||
}
|
||||
}.subscribeOn(dbScheduler).onErrorReturn {
|
||||
EventHandler.INSTANCE.notifyEventHandler(
|
||||
AnalyticsCode.ERROR_DELETE_EXPIRED, it.message
|
||||
)
|
||||
Pair(-1, -1)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun uploadEvents(count: Int): Flowable<List<EventEntity>> {
|
||||
val eventDao = GuruAnalyticsDatabase.getInstance().eventDao()
|
||||
GuruAnalyticsAudit.uploadReady = true
|
||||
logDebug("uploadEvents: $count")
|
||||
return Flowable.just(count).map { eventDao.loadAndMarkUploadEvents(it) }
|
||||
.onErrorReturn {
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_LOAD_MARK, it.message)
|
||||
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 {
|
||||
}.filter { it.isNotEmpty() }.subscribeOn(dbScheduler).observeOn(scheduler).concatMap { splitEntities(it) }
|
||||
.flatMapSingle { uploadEventsInternal(it) }.filter { it.isNotEmpty() }.doOnNext {
|
||||
eventDao.deleteEvents(it)
|
||||
if (GuruAnalytics.INSTANCE.isDebug()) {
|
||||
for (entity in it) {
|
||||
|
|
@ -230,87 +304,66 @@ internal class EventEngine internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun uploadEventsInternal(
|
||||
entities: List<EventEntity>,
|
||||
withoutDelay: Boolean = false
|
||||
): Single<List<EventEntity>> {
|
||||
private fun uploadEventsInternal(entities: List<EventEntity>): Single<List<EventEntity>> {
|
||||
val param = ApiParamUtils.generateApiParam(entities)
|
||||
return guruRepository.uploadEvents(param).map { true }
|
||||
.observeOn(dbScheduler)
|
||||
.doOnError {
|
||||
if (withoutDelay) {
|
||||
GuruAnalyticsDatabase.getInstance().eventDao().addEvents(entities)
|
||||
} else {
|
||||
GuruAnalyticsDatabase.getInstance().eventDao().updateEventDefault(entities)
|
||||
}
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_FAIL, it.message)
|
||||
return guruRepository.uploadEvents(param).map { true }.observeOn(dbScheduler).doOnError {
|
||||
|
||||
}.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
|
||||
}
|
||||
.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,
|
||||
)
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_SUCCESS, extMap)
|
||||
}
|
||||
.onErrorReturn { false }
|
||||
.map { if (it) entities else emptyList() }
|
||||
}
|
||||
|
||||
|
||||
private fun logEventWithoutDelay(
|
||||
eventName: String,
|
||||
itemCategory: String? = null,
|
||||
itemName: String? = null,
|
||||
value: Int? = null,
|
||||
parameters: Map<String, Any>? = null
|
||||
) {
|
||||
val item = EventItem(
|
||||
eventName = eventName,
|
||||
itemCategory = itemCategory,
|
||||
itemName = itemName,
|
||||
value = value,
|
||||
params = parameters
|
||||
)
|
||||
val event = EventInfoStore.deriveEvent(item)
|
||||
compositeDisposable.add(
|
||||
Flowable.just(mutableListOf(event))
|
||||
.flatMap {
|
||||
return@flatMap if (!AndroidUtils.isInternetAvailable(context)
|
||||
) {
|
||||
GuruAnalyticsDatabase.getInstance().eventDao().addEvents(it)
|
||||
Flowable.empty()
|
||||
} else {
|
||||
Flowable.just(it)
|
||||
}
|
||||
}
|
||||
.flatMapSingle { uploadEventsInternal(it, withoutDelay = true) }
|
||||
.subscribe {}
|
||||
)
|
||||
false
|
||||
}.map { if (it) entities else emptyList() }
|
||||
}
|
||||
|
||||
override fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
|
||||
pendingEventSubject.onNext(1)
|
||||
|
||||
// 记录收到的事件数量
|
||||
increaseEventCount()
|
||||
val delivered = increaseEventCount()
|
||||
pendingEventSubject.onNext(1)
|
||||
if (options.priority == EventPriority.EMERGENCE) {
|
||||
forceUpload()
|
||||
logInfo("EMERGENCE: ${item.eventName} forceUpload")
|
||||
forceUpload("EMERGENCE")
|
||||
} else if (delivered and 0x1F == 0x1F) {
|
||||
logInfo("Already delivered $delivered events!! forceUpload")
|
||||
forceUpload("DELIVERED")
|
||||
}
|
||||
}
|
||||
|
||||
private fun increaseEventCount() {
|
||||
private fun increaseEventCount(): Int {
|
||||
val eventCountAll = preferencesManager.eventCountAll ?: 0
|
||||
val total = eventCountAll + 1
|
||||
preferencesManager.eventCountAll = total
|
||||
GuruAnalyticsAudit.total = total
|
||||
GuruAnalyticsAudit.sessionTotal ++
|
||||
GuruAnalyticsAudit.sessionTotal++
|
||||
return total
|
||||
}
|
||||
|
||||
override fun deliverProperty(name: String, value: String) {
|
||||
|
|
@ -321,8 +374,8 @@ internal class EventEngine internal constructor(
|
|||
|
||||
}
|
||||
|
||||
private fun forceUpload() {
|
||||
forceUploadSubject.onNext(true)
|
||||
internal fun forceUpload(scene: String = "unknown") {
|
||||
forceTriggerSubject.onNext(scene)
|
||||
}
|
||||
|
||||
fun getEventsStatics(): EventStatistic {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ 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
|
||||
import guru.core.analytics.GuruAnalytics
|
||||
import guru.core.analytics.data.api.AnalyticsApiHost
|
||||
import guru.core.analytics.data.api.ServiceLocator
|
||||
import guru.core.analytics.data.api.cronet.CronetHelper
|
||||
import guru.core.analytics.data.api.dns.DnsMode
|
||||
import guru.core.analytics.data.db.GuruAnalyticsDatabase
|
||||
import guru.core.analytics.data.db.model.EventStatistic
|
||||
import guru.core.analytics.data.local.PreferencesManager
|
||||
|
|
@ -35,26 +38,28 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
|
|||
companion object {
|
||||
private val reservedEventNames = setOf("test")
|
||||
private val internalVersion = "0.2.1.0"
|
||||
internal val timberPlanted = AtomicBoolean(false)
|
||||
|
||||
}
|
||||
|
||||
private val initialized = AtomicBoolean(false)
|
||||
|
||||
private var engine: EventEngine? = null
|
||||
|
||||
private var lifecycleMonitor: AppLifecycleMonitor? = null
|
||||
|
||||
private var debugMode = false
|
||||
|
||||
private val deliverExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
private val delivers = CopyOnWriteArrayList<EventDeliver>()
|
||||
|
||||
private val initialized = AtomicBoolean(false)
|
||||
|
||||
override fun isDebug(): Boolean = debugMode
|
||||
|
||||
override fun setDebug(debug: Boolean) {
|
||||
debugMode = debug
|
||||
}
|
||||
|
||||
fun isInitialized() = initialized.get()
|
||||
|
||||
override fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String) {
|
||||
if (updateEventBaseUrl.isEmpty()) {
|
||||
throw IllegalArgumentException("updateEventBaseUrl:${updateEventBaseUrl} is empty")
|
||||
|
|
@ -82,53 +87,70 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
|
|||
mainProcess: String?,
|
||||
isEnableCronet: Boolean?,
|
||||
uploadIpAddress: List<String>?,
|
||||
dnsMode: Int?
|
||||
) {
|
||||
if (initialized.compareAndSet(false, true)) {
|
||||
delivers.add(EventDispatcher)
|
||||
eventHandlerCallback?.let { EventHandler.INSTANCE.addEventHandler(it) }
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_1)
|
||||
|
||||
val debugApp = SystemProperties.read("debug.guru.analytics.app")
|
||||
val forceDebug = debugApp == context.packageName
|
||||
if (forceDebug || persistableLog) {
|
||||
Timber.plant(PersistentTree(context, debug = debug))
|
||||
try {
|
||||
if (timberPlanted.compareAndSet(false, true)) {
|
||||
Timber.plant(PersistentTree(context, debug = debug))
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_1)
|
||||
EventInfoStore.initialize(context)
|
||||
delivers.add(EventDispatcher)
|
||||
eventHandlerCallback?.let { EventHandler.INSTANCE.addEventHandler(it) }
|
||||
|
||||
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")
|
||||
Timber.d("[$internalVersion]initialize batchLimit:$batchLimit uploadPeriodInSecond:$uploadPeriodInSeconds startUploadDelayInSecond:$startUploadDelayInSecond eventExpiredInDays:$eventExpiredInDays debug:$debugMode debugUrl:$debugUrl uploadIpAddress:$uploadIpAddress dnsMode:$dnsMode")
|
||||
GuruAnalyticsDatabase.initialize(context.applicationContext)
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_3)
|
||||
|
||||
val baseUrl = if (forceDebug && debugUrl.isNotEmpty()) debugUrl else uploadEventBaseUrl
|
||||
val baseUrl = if (forceDebug && debugUrl.isNotEmpty()) debugUrl else (uploadEventBaseUrl ?: AnalyticsApiHost.BASE_URL)
|
||||
|
||||
DeviceInfoStore.setDeviceInfo(context)
|
||||
// TODO: 此处存在安全隐患,后续需重构
|
||||
ServiceLocator.setDebug(debug)
|
||||
ServiceLocator.addHeaderParam("X-APP-ID", xAppId)
|
||||
ServiceLocator.addHeaderParam("X-DEVICE-INFO", xDeviceInfo)
|
||||
ServiceLocator.setUploadIpAddress(uploadIpAddress)
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_4)
|
||||
ServiceLocator.setDnsMode(dnsMode ?: 0)
|
||||
|
||||
CronetHelper.init(context, isEnableCronet) {
|
||||
if (it) {
|
||||
setUserProperty(Constants.Properties.GURU_ANM, Constants.AnmState.CRONET)
|
||||
GuruAnalyticsAudit.useCronet = true
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_4)
|
||||
if (isEnableCronet == true) {
|
||||
GuruAnalyticsAudit.enabledCronet = true
|
||||
ServiceLocator.setCronet(true)
|
||||
CronetHelper.init(context, true) {
|
||||
if (it) {
|
||||
setUserProperty(Constants.Properties.GURU_ANM, Constants.AnmState.CRONET)
|
||||
GuruAnalyticsAudit.useCronet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_5)
|
||||
|
||||
var uploadEventBaseUri: Uri? = null
|
||||
var hostname: String? = ""
|
||||
if (!baseUrl.isNullOrBlank()) {
|
||||
uploadEventBaseUri =
|
||||
kotlin.runCatching { Uri.parse(baseUrl) }.getOrNull()
|
||||
if (uploadEventBaseUri?.scheme?.startsWith("http") != true) {
|
||||
throw IllegalArgumentException("initialize updateEventBaseUrl:${baseUrl} incorrect format")
|
||||
}
|
||||
hostname = uploadEventBaseUri.host
|
||||
}
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_6)
|
||||
|
||||
ServiceLocator.preloadDns(hostname)
|
||||
|
||||
engine = EventEngine(
|
||||
context,
|
||||
batchLimit = batchLimit ?: EventEngine.DEFAULT_BATCH_LIMIT,
|
||||
|
|
@ -137,7 +159,7 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
|
|||
eventExpiredInDays = EventEngine.DEFAULT_EVENT_EXPIRED_IN_DAYS.coerceAtLeast(
|
||||
eventExpiredInDays ?: EventEngine.DEFAULT_EVENT_EXPIRED_IN_DAYS
|
||||
),
|
||||
uploadEventBaseUri = uploadEventBaseUri
|
||||
guruRepository = ServiceLocator.provideGuruRepository(context, baseUri = uploadEventBaseUri) /// 此处需要配合 ServiceLocator的重构进行修改
|
||||
).apply {
|
||||
delivers.add(this)
|
||||
start(startUploadDelayInSecond)
|
||||
|
|
@ -147,7 +169,11 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
|
|||
val process = AndroidUtils.getProcessName(context)
|
||||
Timber.d("initialize ${mainProcess == process} currentProcess:$process mainProcess:$mainProcess")
|
||||
if (mainProcess.isNullOrBlank() || mainProcess == process) {
|
||||
initAnalyticsPeriodic(context)
|
||||
try {
|
||||
initAnalyticsPeriodic(context)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.d("init worker error!")
|
||||
}
|
||||
} else {
|
||||
if (!process.isNullOrBlank()) {
|
||||
val params = mutableMapOf<String, Any>()
|
||||
|
|
@ -173,6 +199,10 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
|
|||
if (!uploadEventBaseUrl.isNullOrEmpty()) {
|
||||
PreferencesManager.getInstance(context).uploadEventBaseUrl = baseUrl
|
||||
}
|
||||
PreferencesManager.getInstance(context).dnsMode = dnsMode ?: DnsMode.DEFAULT
|
||||
if (uploadIpAddress != null) {
|
||||
PreferencesManager.getInstance(context).uploadIpAddressList = uploadIpAddress.joinToString("|")
|
||||
}
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_9)
|
||||
GuruAnalyticsAudit.initialized = true
|
||||
}
|
||||
|
|
@ -180,28 +210,35 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
|
|||
|
||||
private fun initAnalyticsPeriodic(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<AnalyticsWorker>(
|
||||
6, TimeUnit.HOURS,
|
||||
40, TimeUnit.MINUTES,
|
||||
15, TimeUnit.MINUTES
|
||||
)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.MINUTES)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES)
|
||||
.addTag(AnalyticsWorker.WORKER_TAG)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
Timber.d("initAnalyticsPeriodic")
|
||||
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniquePeriodicWork(
|
||||
AnalyticsWorker.WORKER_ID,
|
||||
ExistingPeriodicWorkPolicy.REPLACE, request
|
||||
AnalyticsWorker.WORKER_NAME,
|
||||
ExistingPeriodicWorkPolicy.KEEP, request
|
||||
)
|
||||
|
||||
// WorkManager.getInstance(context)
|
||||
// .cancelAllWorkByTag(
|
||||
// "AnalyticsWorker",
|
||||
// )
|
||||
|
||||
|
||||
val extMap = mapOf(
|
||||
"repeatInterval" to "6h",
|
||||
"repeatInterval" to "40m",
|
||||
"flexTimeInterval" to "15m",
|
||||
)
|
||||
Timber.d("initAnalyticsPeriodic completed")
|
||||
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.PERIODIC_WORK_ENQUEUE, extMap)
|
||||
}
|
||||
|
||||
|
|
@ -326,6 +363,24 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
|
|||
return GsonUtil.gson.toJson(snapshot)
|
||||
}
|
||||
|
||||
override fun clearStatistic(context: Context) {
|
||||
PreferencesManager.getInstance(context).apply {
|
||||
eventCountAll = 0
|
||||
eventCountDeleted = 0
|
||||
eventCountUploaded = 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun forceUpload(scene: String): Boolean {
|
||||
return engine?.let {
|
||||
if (it.started.get()) {
|
||||
it.forceUpload(scene)
|
||||
return@let true
|
||||
}
|
||||
return@let false
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
|
||||
Timber.tag("GuruAnalytics").d("deliverEvent ${item.eventName}!")
|
||||
deliverExecutor.execute {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
package guru.core.analytics.utils
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import io.reactivex.BackpressureStrategy
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class Connectivity(private val context: Context) : BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
const val CONNECTIVITY_NONE: String = "none"
|
||||
const val CONNECTIVITY_WIFI: String = "wifi"
|
||||
const val CONNECTIVITY_MOBILE: String = "mobile"
|
||||
const val CONNECTIVITY_ETHERNET: String = "ethernet"
|
||||
const val CONNECTIVITY_BLUETOOTH: String = "bluetooth"
|
||||
const val CONNECTIVITY_VPN: String = "vpn"
|
||||
const val CONNECTIVITY_OTHER: String = "other"
|
||||
|
||||
const val CONNECTIVITY_ACTION: String = "android.net.conn.CONNECTIVITY_CHANGE"
|
||||
|
||||
}
|
||||
|
||||
private val connectivityManager by lazy {
|
||||
return@lazy context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
}
|
||||
|
||||
private val mainHandler by lazy {
|
||||
Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
private val networkTypesSubject: BehaviorSubject<List<String>> = BehaviorSubject.createDefault(emptyList())
|
||||
|
||||
val networkAvailableFlowable: Flowable<Boolean>
|
||||
get() = networkTypesSubject.toFlowable(BackpressureStrategy.DROP).map {
|
||||
return@map isNetworkAvailableByTypes(it)
|
||||
}.throttleLast(5, TimeUnit.SECONDS)
|
||||
|
||||
private val bound = AtomicBoolean(false)
|
||||
|
||||
private var networkCallback: NetworkCallback? = null
|
||||
private fun sendCurrentNetworkTypesDelay() {
|
||||
val runnable = Runnable { sendNetworkTypes(getNetworkTypes()) }
|
||||
// Emit events on main thread
|
||||
// 500 milliseconds to avoid race conditions
|
||||
mainHandler.postDelayed(runnable, 500)
|
||||
}
|
||||
|
||||
private fun sendNetworkTypes(types: List<String>) {
|
||||
networkTypesSubject.onNext(types)
|
||||
}
|
||||
|
||||
|
||||
fun getNetworkTypes(): List<String> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val network = connectivityManager.activeNetwork
|
||||
return getCapabilitiesFromNetwork(network)
|
||||
} else {
|
||||
// For legacy versions, return a single type as before or adapt similarly if multiple types
|
||||
// need to be supported
|
||||
return getNetworkTypesLegacy()
|
||||
}
|
||||
}
|
||||
|
||||
fun isNetworkAvailable(): Boolean {
|
||||
val types = getNetworkTypes()
|
||||
return isNetworkAvailableByTypes(types)
|
||||
}
|
||||
|
||||
val isActiveNetworkMetered: Boolean
|
||||
get() = connectivityManager.isActiveNetworkMetered
|
||||
|
||||
private fun isNetworkAvailableByTypes(types: List<String>): Boolean {
|
||||
return types.contains(CONNECTIVITY_WIFI) || types.contains(CONNECTIVITY_MOBILE) || types.contains(CONNECTIVITY_ETHERNET)
|
||||
}
|
||||
|
||||
private fun getCapabilitiesFromNetwork(network: Network?): List<String> {
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
return getCapabilitiesList(capabilities)
|
||||
}
|
||||
|
||||
private fun getCapabilitiesList(capabilities: NetworkCapabilities?): List<String> {
|
||||
val types: MutableList<String> = ArrayList()
|
||||
if (capabilities == null
|
||||
|| !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
) {
|
||||
types.add(CONNECTIVITY_NONE)
|
||||
return types
|
||||
}
|
||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
|
||||
) {
|
||||
types.add(CONNECTIVITY_WIFI)
|
||||
}
|
||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
|
||||
types.add(CONNECTIVITY_ETHERNET)
|
||||
}
|
||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
types.add(CONNECTIVITY_VPN)
|
||||
}
|
||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||
types.add(CONNECTIVITY_MOBILE)
|
||||
}
|
||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) {
|
||||
types.add(CONNECTIVITY_BLUETOOTH)
|
||||
}
|
||||
if (types.isEmpty()
|
||||
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
) {
|
||||
types.add(CONNECTIVITY_OTHER)
|
||||
}
|
||||
if (types.isEmpty()) {
|
||||
types.add(CONNECTIVITY_NONE)
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
private fun getNetworkTypesLegacy(): List<String> {
|
||||
// handle type for Android versions less than Android 6
|
||||
val info = connectivityManager.activeNetworkInfo
|
||||
val types: MutableList<String> = java.util.ArrayList()
|
||||
if (info == null || !info.isConnected) {
|
||||
types.add(CONNECTIVITY_NONE)
|
||||
return types
|
||||
}
|
||||
val type = info.type
|
||||
when (type) {
|
||||
ConnectivityManager.TYPE_BLUETOOTH -> types.add(Connectivity.CONNECTIVITY_BLUETOOTH)
|
||||
ConnectivityManager.TYPE_ETHERNET -> types.add(Connectivity.CONNECTIVITY_ETHERNET)
|
||||
ConnectivityManager.TYPE_WIFI, ConnectivityManager.TYPE_WIMAX -> types.add(Connectivity.CONNECTIVITY_WIFI)
|
||||
ConnectivityManager.TYPE_VPN -> types.add(Connectivity.CONNECTIVITY_VPN)
|
||||
ConnectivityManager.TYPE_MOBILE, ConnectivityManager.TYPE_MOBILE_DUN, ConnectivityManager.TYPE_MOBILE_HIPRI -> types.add(Connectivity.CONNECTIVITY_MOBILE)
|
||||
else -> types.add(Connectivity.CONNECTIVITY_OTHER)
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
fun bind() {
|
||||
if (!bound.compareAndSet(false, true)) {
|
||||
Timber.d("Connectivity Already bind!")
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
networkCallback = object : NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
// onAvailable is called when the phone switches to a new network
|
||||
// e.g. the phone was offline and gets wifi connection
|
||||
// or the phone was on wifi and now switches to mobile.
|
||||
// The plugin sends the current capability connection to the users.
|
||||
val types = getCapabilitiesFromNetwork(network)
|
||||
sendNetworkTypes(types)
|
||||
Timber.tag("Connectivity").d("onAvailable:${types}")
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network, networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
// This callback is called multiple times after a call to onAvailable
|
||||
// this also causes multiple callbacks to the Flutter layer.
|
||||
val types = getCapabilitiesList(networkCapabilities)
|
||||
sendNetworkTypes(types)
|
||||
Timber.tag("Connectivity").d("onCapabilitiesChanged:${types}")
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
// This callback is called when a capability is lost.
|
||||
//
|
||||
// The provided Network object contains information about the
|
||||
// network capability that has been lost, so we cannot use it.
|
||||
//
|
||||
// Instead, post the current network but with a delay long enough
|
||||
// that we avoid a race condition.
|
||||
sendCurrentNetworkTypesDelay()
|
||||
Timber.tag("Connectivity").d("onLost")
|
||||
}
|
||||
}.apply {
|
||||
connectivityManager.registerDefaultNetworkCallback(this)
|
||||
}
|
||||
} else {
|
||||
context.registerReceiver(this, IntentFilter(CONNECTIVITY_ACTION))
|
||||
}
|
||||
|
||||
// Need to emit first event with connectivity types without waiting for first change in system
|
||||
// that might happen much later
|
||||
sendNetworkTypes(getNetworkTypes())
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
networkCallback = networkCallback?.let {
|
||||
connectivityManager.unregisterNetworkCallback(it)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
context.unregisterReceiver(this)
|
||||
} catch (e: Exception) {
|
||||
// listen never called, ignore the error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
sendNetworkTypes(getNetworkTypes())
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue