upgrade to v1.1.0

Signed-off-by: Haoyi <haoyi.zhang@castbox.fm>
3.1.1
Haoyi 2024-06-14 12:57:01 +08:00
parent 3803d53afe
commit d5ec200617
37 changed files with 1475 additions and 350 deletions

View File

@ -4,12 +4,12 @@ plugins {
} }
android { android {
compileSdk 32 compileSdk 34
defaultConfig { defaultConfig {
applicationId "com.example.guruanalytics" applicationId "com.example.guruanalytics"
minSdk 21 minSdk 23
targetSdk 32 targetSdk 33
versionCode 1 versionCode 1
versionName "1.1.0" versionName "1.1.0"
@ -41,6 +41,10 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 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(project(':guru_analytics'))
// implementation 'guru.core.analytics:guru_analytics:0.1.0' // implementation 'guru.core.analytics:guru_analytics:0.1.0'
} }

View File

@ -1,17 +1,150 @@
package com.example.guruanalytics 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.Bundle
import android.os.RemoteException
import android.util.Log import android.util.Log
import android.widget.TextView 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.GuruAnalytics
import guru.core.analytics.data.api.dns.DnsMode
import guru.core.analytics.data.db.model.EventPriority import guru.core.analytics.data.db.model.EventPriority
import guru.core.analytics.data.model.AnalyticsOptions 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() { class MainActivity : AppCompatActivity() {
private var enableUpload = true private var enableUpload = true
@RequiresApi(Build.VERSION_CODES.N)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
@ -24,6 +157,28 @@ class MainActivity : AppCompatActivity() {
// callbackEventHandler = true, // callbackEventHandler = true,
// ) // )
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 {
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)
GuruAnalytics.Builder(this) GuruAnalytics.Builder(this)
.setBatchLimit(25) .setBatchLimit(25)
.setUploadPeriodInSeconds(60) .setUploadPeriodInSeconds(60)
@ -31,39 +186,72 @@ class MainActivity : AppCompatActivity() {
.setEventExpiredInDays(7) .setEventExpiredInDays(7)
.isPersistableLog(true) .isPersistableLog(true)
.setEventHandlerCallback(eventHandler) .setEventHandlerCallback(eventHandler)
.isInitPeriodicWork(false) .isInitPeriodicWork(true)
.isDebug(BuildConfig.DEBUG) .isDebug(BuildConfig.DEBUG)
// .setUploadEventBaseUrl("https://www.baidu.com") // .setUploadEventBaseUrl("https://www.baidu.com")
// .setFgEventPeriodInSeconds(60L) // .setFgEventPeriodInSeconds(60L)
.setXAppId("test_x_app_id") .setXAppId("test_x_app_id")
.setXDeviceInfo("test_x_device_info") .setXDeviceInfo("test_x_device_info")
.setMainProcess("com.example.guruanalytics") .setMainProcess("com.example.guruanalytics")
.isEnableCronet(true) // .isEnableCronet(true)
.setUploadIpAddress(listOf("3.210.96.186", "34.196.69.199")) .setUploadEventBaseUrl("https://collect4.fungame.cloud")
.setDnsMode(DnsMode.COMPOSITE)
.build() .build()
findViewById<TextView>(R.id.tvLogEvent).setOnClickListener { Log.w("GuruAnalytics", "GuruAnalytics.INSTANCE: completed")
val map = mutableMapOf<String, Any>()
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)
} }
findViewById<TextView>(R.id.tvLocalLog).setOnClickListener { findViewById<TextView>(R.id.tvLocalLog).setOnClickListener {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
GuruAnalytics.INSTANCE.zipLogs(this) // GuruAnalytics.INSTANCE.zipLogs(this)
Log.i("get_local_log", "${System.currentTimeMillis() - startTime}") // Log.i("get_local_log", "${System.currentTimeMillis() - startTime}")
} // val request = OneTimeWorkRequestBuilder<AnalyticsWorker>(
findViewById<TextView>(R.id.tvEventStatistic).setOnClickListener { // ).build()
GuruAnalytics.INSTANCE.getEventsStatics().run { //// .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( Log.i(
"UploadEventDaemon_main", "Worker",
"$eventCountAll $eventCountDeleted $eventCountUploaded" "dispatchActiveWorker"
) )
} }
findViewById<TextView>(R.id.tvEventStatistic).setOnClickListener {
GuruAnalytics.INSTANCE.snapshotAnalyticsAudit().run {
Log.i(
"UploadEventDaemon_main",
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 { findViewById<TextView>(R.id.tvEventEmergence).setOnClickListener {
val map = mutableMapOf<String, Any>() val map = mutableMapOf<String, Any>()
@ -77,7 +265,7 @@ class MainActivity : AppCompatActivity() {
) )
} }
findViewById<TextView>(R.id.tvBaseUrl).setOnClickListener { 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) val tvEnable = findViewById<TextView>(R.id.tvEnable)
@ -88,11 +276,20 @@ class MainActivity : AppCompatActivity() {
tvEnable.text = if (enableUpload) "Enable Upload" else "Disable Upload" 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.setScreen("main")
GuruAnalytics.INSTANCE.setAdId("AD_ID_01") GuruAnalytics.INSTANCE.setAdId("AD_ID_01")
GuruAnalytics.INSTANCE.setFirebaseId("FIREBASE_ID")
GuruAnalytics.INSTANCE.setUid("MBK-YYYYY")
GuruAnalytics.INSTANCE.setUserProperty("uid", "110051") GuruAnalytics.INSTANCE.setUserProperty("uid", "110051")
GuruAnalytics.INSTANCE.setUserProperty("age", "12") GuruAnalytics.INSTANCE.setUserProperty("age", "12")
GuruAnalytics.INSTANCE.setUserProperty("sex", "male") GuruAnalytics.INSTANCE.setUserProperty("sex", "male")
Log.d("Test", "setAdId")
val properties = GuruAnalytics.INSTANCE.peakUserProperties() val properties = GuruAnalytics.INSTANCE.peakUserProperties()
Log.i("peakUserProperties", "properties:$properties") Log.i("peakUserProperties", "properties:$properties")
@ -100,11 +297,6 @@ class MainActivity : AppCompatActivity() {
GuruAnalytics.INSTANCE.getUserProperties { GuruAnalytics.INSTANCE.getUserProperties {
Log.i("getUserProperties", "properties:$it") 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 -> private val eventHandler: (Int, String?) -> Unit = { code, ext ->

View File

@ -32,6 +32,8 @@ class TestProcessActivity : AppCompatActivity() {
.setXAppId("test_x_app_id") .setXAppId("test_x_app_id")
.setXDeviceInfo("test_x_device_info") .setXDeviceInfo("test_x_device_info")
.setMainProcess("com.example.guruanalytics") .setMainProcess("com.example.guruanalytics")
.setUploadIpAddress(listOf("13.248.248.135", "3.33.195.44"))
.build() .build()
findViewById<TextView>(R.id.tvFinish).setOnClickListener { findViewById<TextView>(R.id.tvFinish).setOnClickListener {

View File

@ -11,72 +11,82 @@
android:id="@+id/tvSetProperty" android:id="@+id/tvSetProperty"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="50dp" android:layout_marginTop="50dp"
android:padding="10dp" android:padding="10dp"
android:text="SET PROPERTY" android:text="SET PROPERTY" />
android:layout_gravity="center_horizontal"/>
<TextView <TextView
android:id="@+id/tvLogEvent" android:id="@+id/tvLogEvent"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:padding="10dp" android:padding="10dp"
android:text="LOG EVENT" android:text="LOG EVENT" />
android:layout_gravity="center_horizontal"/>
<TextView <TextView
android:id="@+id/tvEventEmergence" android:id="@+id/tvEventEmergence"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:padding="10dp" android:padding="10dp"
android:text="Event_EMERGENCE" android:text="Event_EMERGENCE" />
android:layout_gravity="center_horizontal"/>
<TextView <TextView
android:id="@+id/tvOpenTestProcessActivity" android:id="@+id/tvOpenTestProcessActivity"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:padding="10dp" android:padding="10dp"
android:text="Open TestProcessActivity" android:text="Open TestProcessActivity" />
android:layout_gravity="center_horizontal" />
<TextView <TextView
android:id="@+id/tvLocalLog" android:id="@+id/tvLocalLog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:padding="10dp" android:padding="10dp"
android:text="get local log file" android:text="get local log file" />
android:layout_gravity="center_horizontal"/>
<TextView <TextView
android:id="@+id/tvEventStatistic" android:id="@+id/tvEventStatistic"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:padding="10dp" android:padding="10dp"
android:text="Event Statistic" android:text="Event Statistic" />
android:layout_gravity="center_horizontal" />
<TextView <TextView
android:id="@+id/tvBaseUrl" android:id="@+id/tvBaseUrl"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:padding="10dp" android:padding="10dp"
android:text="Update BaseUrl" android:text="Update BaseUrl" />
android:layout_gravity="center_horizontal" />
<TextView <TextView
android:id="@+id/tvEnable" android:id="@+id/tvEnable"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:padding="10dp" android:padding="10dp"
android:text="Enable Upload" android:text="Enable Upload" />
android:layout_gravity="center_horizontal" />
<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> </LinearLayout>

View File

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.6.10' ext.kotlin_version = '1.9.0'
repositories { repositories {
// maven { url 'http://localhost:8081/repository/maven-public/' } // maven { url 'http://localhost:8081/repository/maven-public/' }
@ -10,7 +10,7 @@ buildscript {
} }
dependencies { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

BIN
guru_analytics/gradle/wrapper/gradle-wrapper.jar vendored Normal file → Executable file

Binary file not shown.

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -1,3 +1,5 @@
import java.text.SimpleDateFormat
plugins { plugins {
id 'com.android.library' id 'com.android.library'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
@ -8,6 +10,15 @@ plugins {
apply from: 'dependencies.gradle' apply from: 'dependencies.gradle'
apply from: 'maven-publish.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 { android {
compileSdk android.compileSdk compileSdk android.compileSdk
@ -18,6 +29,8 @@ android {
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
buildConfigField "String", "buildTs", buildTs()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@ -1,26 +1,26 @@
ext { ext {
compiler = [ compiler = [
java : JavaVersion.VERSION_1_8, java : JavaVersion.VERSION_17,
kotlin: '1.6.10' kotlin: '1.9.0'
] ]
android = [ android = [
buildTools: '30.0.3', buildTools: '30.0.3',
minSdk : 21, minSdk : 21,
targetSdk : 32, targetSdk : 33,
compileSdk: 32 compileSdk: 34
] ]
androidXCoreVersion = '1.7.0' androidXCoreVersion = '1.7.0'
timberVersion = '4.7.1' timberVersion = '4.7.1'
roomVersion = '2.4.3' roomVersion = '2.6.1'
gsonVersion = '2.8.5' gsonVersion = '2.8.5'
retrofitVersion = '2.7.1' retrofitVersion = '2.7.1'
okhttpVersion = '4.9.3' okhttpVersion = '4.12.0'
preferenceVersion = '1.2.0' preferenceVersion = '1.2.0'
processVersion = '2.4.0' processVersion = '2.4.0'
workVersion = '2.7.1' workVersion = '2.9.0'
cronetOkhttpVersion = '0.1.0' cronetOkhttpVersion = '0.1.0'
playServicesCronetVersion = '18.0.1' playServicesCronetVersion = '18.0.1'
@ -47,7 +47,8 @@ ext {
] ]
okhttpDependencies = [ okhttpDependencies = [
"com.squareup.okhttp3:okhttp:$okhttpVersion" "com.squareup.okhttp3:okhttp:$okhttpVersion",
"com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion"
] ]
workerDependencies = [ workerDependencies = [

View File

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

View File

@ -43,6 +43,7 @@ object Constants {
const val SCREEN_W = "screenW" const val SCREEN_W = "screenW"
const val OS_VERSION = "osVersion" const val OS_VERSION = "osVersion"
const val LANGUAGE = "language" const val LANGUAGE = "language"
const val SDK_INFO = "sdkInfo"
} }
object Properties { object Properties {

View File

@ -1,6 +1,7 @@
package guru.core.analytics package guru.core.analytics
import android.content.Context 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.db.model.EventStatistic
import guru.core.analytics.data.model.AnalyticsInfo import guru.core.analytics.data.model.AnalyticsInfo
import guru.core.analytics.data.model.AnalyticsOptions import guru.core.analytics.data.model.AnalyticsOptions
@ -29,6 +30,7 @@ abstract class GuruAnalytics {
mainProcess: String? = null, mainProcess: String? = null,
isEnableCronet: Boolean? = null, isEnableCronet: Boolean? = null,
uploadIpAddress: List<String>? = null, uploadIpAddress: List<String>? = null,
dnsMode: Int? = null
) )
abstract fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String) abstract fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String)
@ -83,6 +85,10 @@ abstract class GuruAnalytics {
abstract fun snapshotAnalyticsAudit(): String abstract fun snapshotAnalyticsAudit(): String
abstract fun clearStatistic(context: Context)
abstract fun forceUpload(scene: String = "unknown"): Boolean
companion object { companion object {
val INSTANCE: GuruAnalytics by lazy() { val INSTANCE: GuruAnalytics by lazy() {
GuruAnalyticsImpl() GuruAnalyticsImpl()
@ -134,6 +140,9 @@ abstract class GuruAnalytics {
fun setUploadIpAddress(uploadIpAddress: List<String>) = fun setUploadIpAddress(uploadIpAddress: List<String>) =
apply { analyticsInfo.uploadIpAddress = uploadIpAddress } apply { analyticsInfo.uploadIpAddress = uploadIpAddress }
fun setDnsMode(@DnsMode dnsMode: Int) =
apply { analyticsInfo.dnsMode = dnsMode }
fun build(): GuruAnalytics { fun build(): GuruAnalytics {
analyticsInfo.run { analyticsInfo.run {
INSTANCE.initialize( INSTANCE.initialize(
@ -153,6 +162,7 @@ abstract class GuruAnalytics {
mainProcess, mainProcess,
isEnableCronet, isEnableCronet,
uploadIpAddress, uploadIpAddress,
dnsMode
) )
} }
return INSTANCE return INSTANCE

View File

@ -4,33 +4,36 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.SystemClock import android.os.SystemClock
import guru.core.analytics.data.api.cronet.CastboxCronetInterceptor 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.CustomDns
import guru.core.analytics.data.api.dns.DnsMode
import guru.core.analytics.data.api.dns.GoogleDnsApi import guru.core.analytics.data.api.dns.GoogleDnsApi
import guru.core.analytics.data.api.dns.GoogleDnsApiHost 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.Level
import guru.core.analytics.data.api.logging.LoggingInterceptor import guru.core.analytics.data.api.logging.LoggingInterceptor
import guru.core.analytics.data.local.PreferencesManager 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.AnalyticsCode
import guru.core.analytics.handler.EventHandler import guru.core.analytics.handler.EventHandler
import guru.core.analytics.utils.AndroidUtils
import guru.core.analytics.utils.DateTimeUtils import guru.core.analytics.utils.DateTimeUtils
import guru.core.analytics.utils.GsonUtil import guru.core.analytics.utils.GsonUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.Dispatcher import okhttp3.Dispatcher
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient 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 okhttp3.internal.platform.Platform
import retrofit2.Converter import retrofit2.Converter
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
// Todo: 该类都需要重构,业务不清晰,不符合打点库使用
object ServiceLocator { object ServiceLocator {
private var debug = false private var debug = false
@ -38,11 +41,16 @@ object ServiceLocator {
@Volatile @Volatile
private var guruRepository: GuruRepository? = null private var guruRepository: GuruRepository? = null
@Volatile
private var googleDnsApi: GoogleDnsApi? = null
private val headerParams = mutableMapOf<String, String>() 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 private var uploadIpAddress: List<String>? = null
fun addHeaderParam(key: String, value: String?) { fun addHeaderParam(key: String, value: String?) {
@ -54,10 +62,36 @@ object ServiceLocator {
this.debug = debug this.debug = debug
} }
@Synchronized
fun setUploadIpAddress(ipList: List<String>?) { fun setUploadIpAddress(ipList: List<String>?) {
if (compositeDns.ipAddress != ipList) {
compositeDns.setCandidateIpAddress(ipAddress = ipList)
}
uploadIpAddress = 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 { fun provideGuruRepository(context: Context, baseUri: Uri? = null): GuruRepository {
synchronized(this) { synchronized(this) {
return guruRepository return guruRepository
@ -97,14 +131,8 @@ object ServiceLocator {
guruRepository?.analyticsApi = createAnalyticsApi(context, baseUrl) 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( return GoogleDnsApi.Creator.newInstance(
Retrofit.Builder() Retrofit.Builder()
.baseUrl(GoogleDnsApiHost.API) .baseUrl(GoogleDnsApiHost.API)
@ -120,30 +148,37 @@ object ServiceLocator {
private fun createOkHttpClient( private fun createOkHttpClient(
context: Context, context: Context,
readTimeOut: Long = 30L, readTimeOut: Long = 90L,
writeTimeOut: Long = 30L writeTimeOut: Long = 90L,
): OkHttpClient { ): OkHttpClient {
val dns = when (dnsMode) {
DnsMode.COMPOSITE -> compositeDns
DnsMode.STATIC -> StaticDns(uploadIpAddress)
else -> CustomDns(context, uploadIpAddress = uploadIpAddress)
}
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.dispatcher(Dispatcher().apply { .dispatcher(Dispatcher().apply {
maxRequests = 128 maxRequests = 128
maxRequestsPerHost = 10 maxRequestsPerHost = 5
}) })
.dns(CustomDns(context, uploadIpAddress)) .dns(dns)
.connectTimeout(20L, TimeUnit.SECONDS) .connectTimeout(90L, TimeUnit.SECONDS)
.readTimeout(readTimeOut, TimeUnit.SECONDS) .readTimeout(readTimeOut, TimeUnit.SECONDS)
.writeTimeout(writeTimeOut, TimeUnit.SECONDS) .writeTimeout(writeTimeOut, TimeUnit.SECONDS)
.addInterceptor(createCacheControlInterceptor(context))
.addInterceptor(createAnalyticsApiInterceptor()) .addInterceptor(createAnalyticsApiInterceptor())
.addInterceptor(createLoggingInterceptor()) .addInterceptor(createLoggingInterceptor())
.addInterceptor(createCronetInterceptor()) if (isEnabledCronet) {
builder.addInterceptor(createCronetInterceptor())
}
return builder.build() return builder.build()
} }
private fun createDnsOkHttpClient(context: Context): OkHttpClient { private fun createDnsOkHttpClient(context: Context): OkHttpClient {
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.addInterceptor(createCacheControlInterceptor(context)) .connectTimeout(90L, TimeUnit.SECONDS)
.readTimeout(90L, TimeUnit.SECONDS)
.writeTimeout(90L, TimeUnit.SECONDS)
.addInterceptor(createLoggingInterceptor()) .addInterceptor(createLoggingInterceptor())
.addInterceptor(createCronetInterceptor())
return builder.build() return builder.build()
} }
@ -164,32 +199,6 @@ object ServiceLocator {
return CastboxCronetInterceptor() 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 { private fun createAnalyticsApiInterceptor(): Interceptor {
return Interceptor { chain -> return Interceptor { chain ->
val request = chain.request() val request = chain.request()

View File

@ -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)
}
}

View File

@ -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"))
}
}

View File

@ -4,6 +4,7 @@ import android.content.Context
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import guru.core.analytics.data.api.ServiceLocator import guru.core.analytics.data.api.ServiceLocator
import guru.core.analytics.data.local.PreferencesManager 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.AnalyticsCode
import guru.core.analytics.handler.EventHandler import guru.core.analytics.handler.EventHandler
import guru.core.analytics.utils.GsonUtil 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 { class CustomDns(private val context: Context, private val uploadIpAddress: List<String>? = null) : Dns {
@Volatile
private var googleDnsApi: GoogleDnsApi? = null
private val cachedHostAddress by lazy { private val cachedHostAddress by lazy {
val hostAddressJson = PreferencesManager.getInstance(context).hostAddressJson val hostAddressJson = PreferencesManager.getInstance(context).hostAddressJson
return@lazy runCatching { return@lazy runCatching {
if (!hostAddressJson.isNullOrBlank()) { 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>> GsonUtil.gson.fromJson(hostAddressJson, mapType) as? MutableMap<String, List<String>>
} else null } else null
}.getOrNull() ?: mutableMapOf() }.getOrNull() ?: mutableMapOf()
} }
fun provideGoogleDnsApi(context: Context): GoogleDnsApi {
synchronized(this) {
return googleDnsApi
?: ServiceLocator.createGoogleDnsApi(context).apply { googleDnsApi = this }
}
}
override fun lookup(hostname: String): List<InetAddress> { override fun lookup(hostname: String): List<InetAddress> {
return try { val ipList = try {
Dns.SYSTEM.lookup(hostname).also { list -> Dns.SYSTEM.lookup(hostname).also { list ->
cacheHostAddress(hostname, list.map { it.hostAddress }) 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> { private fun lookupByGoogleDns(hostname: String): List<InetAddress> {
val dnsApi = ServiceLocator.provideGoogleDnsApi(context) val dnsApi = provideGoogleDnsApi(context)
val ipList = runBlocking { val ipList = runBlocking {
return@runBlocking dnsApi.ip(hostname).answer?.toMutableList() return@runBlocking dnsApi.ip(hostname).answer?.toMutableList()
?.filter { it.type == 1 } ?.filter { it.type == 1 }

View File

@ -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
}
}

View File

@ -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")
}
}

View File

@ -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
}
}

View File

@ -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.model.EventEntity
import guru.core.analytics.data.db.utils.Converters import guru.core.analytics.data.db.utils.Converters
import guru.core.analytics.data.db.utils.TransactionResult import guru.core.analytics.data.db.utils.TransactionResult
import guru.core.analytics.data.db.utils.runInTransactionEx
import io.reactivex.Maybe import io.reactivex.Maybe
import timber.log.Timber import timber.log.Timber
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
@ -66,12 +65,12 @@ abstract class GuruAnalyticsDatabase : RoomDatabase() {
.build() .build()
} }
fun <T> runInTransaction(callback: () -> TransactionResult<T>, defVal: T?): Maybe<T> { // fun <T> runInTransaction(callback: () -> TransactionResult<T>, defVal: T?): Maybe<T> {
return INSTANCE?.runInTransactionEx(callback, defVal) ?: Maybe.empty() // return INSTANCE?.runInTransactionEx(callback, defVal) ?: Maybe.empty()
} // }
//
fun <T> runInTransaction(callback: () -> TransactionResult<T>): Maybe<T> { // fun <T> runInTransaction(callback: () -> TransactionResult<T>): Maybe<T> {
return INSTANCE?.runInTransactionEx(callback) ?: Maybe.empty() // return INSTANCE?.runInTransactionEx(callback) ?: Maybe.empty()
} // }
} }
} }

View File

@ -51,13 +51,6 @@ abstract class EventDao {
@Transaction @Transaction
open fun loadAndMarkUploadEvents(limit: Int): List<EventEntity> { open fun loadAndMarkUploadEvents(limit: Int): List<EventEntity> {
val events = getEvents(limit).toMutableList() 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) updateEventUploading(events)
return events return events
} }

View File

@ -4,4 +4,8 @@ data class EventStatistic(
val eventCountAll: Int = 0, val eventCountAll: Int = 0,
val eventCountDeleted: Int = 0, val eventCountDeleted: Int = 0,
val eventCountUploaded: Int = 0, val eventCountUploaded: Int = 0,
) ) {
override fun toString(): String {
return "eventCountAll=$eventCountAll, eventCountDeleted=$eventCountDeleted, eventCountUploaded=$eventCountUploaded"
}
}

View File

@ -30,31 +30,31 @@ data class TransactionResult<T>(
} }
} }
fun <T> RoomDatabase.runInTransactionEx( //fun <T> RoomDatabase.runInTransactionEx(
callback: () -> TransactionResult<T>, // callback: () -> TransactionResult<T>,
defVal: T? // defVal: T?
): Maybe<T> { //): Maybe<T> {
return Maybe.create { emitter -> // return Maybe.create { emitter ->
val result = this.runInTransaction(callback) // val result = this.runInTransaction(callback)
?: TransactionResult(defVal, ResultBehavior.SUCCESS) // ?: TransactionResult(defVal, ResultBehavior.SUCCESS)
when (result.behavior) { // when (result.behavior) {
ResultBehavior.SUCCESS -> { // ResultBehavior.SUCCESS -> {
val value = result.value // val value = result.value
if (value != null) { // if (value != null) {
emitter.onSuccess(value) // emitter.onSuccess(value)
} // }
emitter.onComplete() // emitter.onComplete()
} // }
ResultBehavior.ERROR -> { // ResultBehavior.ERROR -> {
emitter.onError(DatabaseException("runInTransaction error!", result.cause)) // emitter.onError(DatabaseException("runInTransaction error!", result.cause))
} // }
else -> { // else -> {
emitter.onComplete() // emitter.onComplete()
} // }
} // }
} // }
} //}
fun <T> RoomDatabase.runInTransactionEx(callback: () -> TransactionResult<T>): Maybe<T> { //fun <T> RoomDatabase.runInTransactionEx(callback: () -> TransactionResult<T>): Maybe<T> {
return runInTransactionEx(callback, null) // return runInTransactionEx(callback, null)
} //}

View File

@ -2,6 +2,7 @@ package guru.core.analytics.data.local
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import guru.core.analytics.data.api.dns.DnsMode
class PreferencesManager private constructor( class PreferencesManager private constructor(
context: Context, context: Context,
@ -49,4 +50,15 @@ class PreferencesManager private constructor(
var uploadEventBaseUrl: String? by bind("update_event_base_url", "") var uploadEventBaseUrl: String? by bind("update_event_base_url", "")
var totalDurationFgEvent: Long? by bind("total_duration_fg_event", 0L) var totalDurationFgEvent: Long? by bind("total_duration_fg_event", 0L)
var hostAddressJson: String? by bind("host_address", "") 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", "")
} }

View File

@ -16,4 +16,5 @@ internal data class AnalyticsInfo(
var mainProcess: String? = null, var mainProcess: String? = null,
var isEnableCronet: Boolean? = null, var isEnableCronet: Boolean? = null,
var uploadIpAddress: List<String>? = null, var uploadIpAddress: List<String>? = null,
var dnsMode: Int? = null
) )

View File

@ -2,6 +2,7 @@ package guru.core.analytics.data.model
import androidx.annotation.Keep import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import guru.core.analytics.data.api.dns.DnsMode
@Keep @Keep
data class GuruAnalyticsAuditSnapshot( data class GuruAnalyticsAuditSnapshot(
@ -18,12 +19,16 @@ data class GuruAnalyticsAuditSnapshot(
@SerializedName("sessionDeleted") val sessionDeleted: Int = 0, @SerializedName("sessionDeleted") val sessionDeleted: Int = 0,
@SerializedName("sessionTotal") val sessionTotal: Int = 0, @SerializedName("sessionTotal") val sessionTotal: Int = 0,
@SerializedName("uploadReady") val uploadReady: Boolean = false, @SerializedName("uploadReady") val uploadReady: Boolean = false,
@SerializedName("dnsMode") val dnsMode: Int = DnsMode.DEFAULT,
@SerializedName("serverIp") var serverIp: String = ""
) )
object GuruAnalyticsAudit { object GuruAnalyticsAudit {
var initialized: Boolean = false var initialized: Boolean = false
var engineInitialized: Boolean = false var engineInitialized: Boolean = false
var useCronet: Boolean = false var useCronet: Boolean = false
var dnsMode: Int = DnsMode.DEFAULT
var enabledCronet: Boolean = false
var eventDispatcherStarted: Boolean = false var eventDispatcherStarted: Boolean = false
var fgHelperInitialized: Boolean = false var fgHelperInitialized: Boolean = false
var connectionState: Boolean = false var connectionState: Boolean = false
@ -40,10 +45,13 @@ object GuruAnalyticsAudit {
var uploadReady: Boolean = false var uploadReady: Boolean = false
var serverIp: String = ""
fun snapshot() = GuruAnalyticsAuditSnapshot( fun snapshot() = GuruAnalyticsAuditSnapshot(
initialized = initialized, initialized = initialized,
engineInitialized = engineInitialized, engineInitialized = engineInitialized,
useCronet = useCronet, useCronet = useCronet,
dnsMode = dnsMode,
eventDispatcherStarted = eventDispatcherStarted, eventDispatcherStarted = eventDispatcherStarted,
fgHelperInitialized = fgHelperInitialized, fgHelperInitialized = fgHelperInitialized,
connectionState = connectionState, connectionState = connectionState,
@ -53,7 +61,8 @@ object GuruAnalyticsAudit {
sessionUploaded = sessionUploaded, sessionUploaded = sessionUploaded,
sessionDeleted = sessionDeleted, sessionDeleted = sessionDeleted,
sessionTotal = sessionTotal, sessionTotal = sessionTotal,
uploadReady = uploadReady uploadReady = uploadReady,
serverIp = serverIp
) )
} }

View File

@ -2,16 +2,20 @@ package guru.core.analytics.data.store
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import guru.core.analytics.BuildConfig
import guru.core.analytics.Constants import guru.core.analytics.Constants
import guru.core.analytics.handler.AnalyticsCode import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler import guru.core.analytics.handler.EventHandler
import guru.core.analytics.utils.AndroidUtils import guru.core.analytics.utils.AndroidUtils
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
import timber.log.Timber
import java.util.* import java.util.*
object DeviceInfoStore { object DeviceInfoStore {
const val SDK_VERSION = "v1.1.0"
private val deviceInfoSubject: BehaviorSubject<Map<String, Any>> = private val deviceInfoSubject: BehaviorSubject<Map<String, Any>> =
BehaviorSubject.createDefault( BehaviorSubject.createDefault(
hashMapOf() hashMapOf()
@ -37,7 +41,9 @@ object DeviceInfoStore {
map[Constants.DeviceInfo.SCREEN_W] = AndroidUtils.getWindowWidth(context) ?: 0 map[Constants.DeviceInfo.SCREEN_W] = AndroidUtils.getWindowWidth(context) ?: 0
map[Constants.DeviceInfo.OS_VERSION] = Build.VERSION.RELEASE map[Constants.DeviceInfo.OS_VERSION] = Build.VERSION.RELEASE
map[Constants.DeviceInfo.LANGUAGE] = Locale.getDefault().language map[Constants.DeviceInfo.LANGUAGE] = Locale.getDefault().language
map[Constants.DeviceInfo.SDK_INFO] = "${SDK_VERSION}-${BuildConfig.buildTs}"
deviceInfoSubject.onNext(map) deviceInfoSubject.onNext(map)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_DEVICE_INFO, map) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_DEVICE_INFO, map)
Timber.tag("DeviceInfoStore").i("DeviceInfo: $map")
} }
} }

View File

@ -1,16 +1,22 @@
package guru.core.analytics.data.store package guru.core.analytics.data.store
import android.content.Context
import guru.core.analytics.Constants import guru.core.analytics.Constants
import guru.core.analytics.data.db.model.Event import guru.core.analytics.data.db.model.Event
import guru.core.analytics.data.db.model.EventEntity import guru.core.analytics.data.db.model.EventEntity
import guru.core.analytics.data.db.model.EventPriority import guru.core.analytics.data.db.model.EventPriority
import guru.core.analytics.data.db.model.ParamValue 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.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.DateTimeUtils
import guru.core.analytics.utils.GsonUtil import guru.core.analytics.utils.GsonUtil
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
import timber.log.Timber
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
object EventInfoStore { object EventInfoStore {
@ -19,6 +25,135 @@ object EventInfoStore {
UUID.randomUUID().toString() 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>> = private val propertiesSubject: BehaviorSubject<ConcurrentHashMap<String, String>> =
BehaviorSubject.createDefault( BehaviorSubject.createDefault(
ConcurrentHashMap<String, String>().apply { ConcurrentHashMap<String, String>().apply {
@ -75,24 +210,57 @@ object EventInfoStore {
ids = ids.plus(idName to id) ids = ids.plus(idName to id)
} }
internal fun getId(idName: String): String? = ids[idName]
fun setDeviceId(deviceId: String) { fun setDeviceId(deviceId: String) {
if (deviceId.isNotEmpty()) {
val prev = getId(Constants.Ids.DEVICE_ID) ?: ""
setIds(Constants.Ids.DEVICE_ID, deviceId) setIds(Constants.Ids.DEVICE_ID, deviceId)
if (prev != deviceId) {
preferenceMgr?.deviceId = deviceId
}
}
} }
fun setUid(uid: String) { fun setUid(uid: String) {
if (uid.isNotEmpty()) {
val prev = getId(Constants.Ids.UID) ?: ""
setIds(Constants.Ids.UID, uid) setIds(Constants.Ids.UID, uid)
if (prev != uid) {
preferenceMgr?.userId = uid
}
}
} }
fun setAdjustId(adjustId: String) { fun setAdjustId(adjustId: String) {
if (adjustId.isNotEmpty()) {
val prev = getId(Constants.Ids.ADJUST_ID) ?: ""
setIds(Constants.Ids.ADJUST_ID, adjustId) setIds(Constants.Ids.ADJUST_ID, adjustId)
if (prev != adjustId) {
preferenceMgr?.adjustId = adjustId
}
}
} }
fun setAdId(adId: String) { fun setAdId(adId: String) {
if (adId.isNotEmpty()) {
val prev = getId(Constants.Ids.AD_ID) ?: ""
setIds(Constants.Ids.AD_ID, adId) setIds(Constants.Ids.AD_ID, adId)
if (prev != adId) {
preferenceMgr?.adId = adId
}
}
} }
fun setFirebaseId(firebaseId: String) { fun setFirebaseId(firebaseId: String) {
if (firebaseId.isNotEmpty()) {
val prev = getId(Constants.Ids.FIREBASE_ID) ?: ""
setIds(Constants.Ids.FIREBASE_ID, firebaseId) setIds(Constants.Ids.FIREBASE_ID, firebaseId)
if (prev != firebaseId) {
preferenceMgr?.firebaseId = firebaseId
}
}
} }
private fun createParamValue(value: Any): ParamValue { private fun createParamValue(value: Any): ParamValue {

View File

@ -28,9 +28,12 @@ enum class AnalyticsCode(val code: Int) {
ERROR_ZIP(107), // zip 错误 ERROR_ZIP(107), // zip 错误
ERROR_DNS_CACHE(108), // zip 错误 ERROR_DNS_CACHE(108), // zip 错误
ERROR_CRONET_INTERCEPTOR(109),// cronet拦截器 ERROR_CRONET_INTERCEPTOR(109),// cronet拦截器
ERROR_SESSION_START_ERROR(110),
EVENT_FIRST_OPEN(1001), // first_open 事件 EVENT_FIRST_OPEN(1001), // first_open 事件
EVENT_FG(1002), // fg 事件 EVENT_FG(1002), // fg 事件
EVENT_LOOKUP(1003),
EVENT_SESSION_ACTIVE(1004),
// 初始化进度 // 初始化进度
INIT_STEP_1(100001), INIT_STEP_1(100001),

View File

@ -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()
}
}
}

View File

@ -1,11 +1,19 @@
package guru.core.analytics.impl package guru.core.analytics.impl
import android.content.Context import android.content.Context
import android.net.Uri
import androidx.work.RxWorker import androidx.work.RxWorker
import androidx.work.WorkerParameters 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.db.GuruAnalyticsDatabase
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.data.store.DeviceInfoStore import guru.core.analytics.data.store.DeviceInfoStore
import guru.core.analytics.data.store.EventInfoStore
import guru.core.analytics.log.PersistentTree import guru.core.analytics.log.PersistentTree
import guru.core.analytics.utils.GsonUtil
import io.reactivex.Single import io.reactivex.Single
import timber.log.Timber import timber.log.Timber
@ -14,21 +22,55 @@ class AnalyticsWorker(
workerParams: WorkerParameters workerParams: WorkerParameters
) : RxWorker(context.applicationContext, workerParams) { ) : RxWorker(context.applicationContext, workerParams) {
companion object { companion object {
const val WORKER_ID = "GuruAnalytics" const val WORKER_NAME = "AnalyticsUploader"
const val WORKER_TAG = "Analytics" 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> { override fun createWork(): Single<Result> {
Timber.plant(PersistentTree(context)) 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) GuruAnalyticsDatabase.initialize(context)
DeviceInfoStore.setDeviceInfo(context) EventInfoStore.initialize(context)
Timber.d("OnWork..") Timber.d("OnWork..initialized")
val engine = EventEngine(context) val engine = EventEngine(context, guruRepository = buildGuruRepository(context))
return engine.validateEvents().doOnSuccess {
engine.validateEvents().doOnSuccess {
Timber.d("validateEvents deleted:${it.first} reset:${it.second}") Timber.d("validateEvents deleted:${it.first} reset:${it.second}")
}.toFlowable().flatMap { engine.uploadEvents(500) }.map { true }.toList() }.toFlowable().flatMap { engine.uploadEvents(500) }.map { true }.toList()
.map { Result.success() } .map { Result.success() }
.onErrorReturn { Result.success() } .onErrorReturn { Result.success() }
} }
}
} }

View File

@ -46,7 +46,6 @@ internal class AppLifecycleMonitor internal constructor(context: Context) {
Timber.d("${TAG}_ON_PAUSE") Timber.d("${TAG}_ON_PAUSE")
fgHelper.stop() fgHelper.stop()
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.LIFECYCLE_PAUSE) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.LIFECYCLE_PAUSE)
} }
Lifecycle.Event.ON_STOP -> Timber.d("${TAG}_ON_STOP") Lifecycle.Event.ON_STOP -> Timber.d("${TAG}_ON_STOP")

View File

@ -6,6 +6,7 @@ import android.net.ConnectivityManager.NetworkCallback
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build
import guru.core.analytics.handler.AnalyticsCode import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler import guru.core.analytics.handler.EventHandler
import guru.core.analytics.utils.AndroidUtils import guru.core.analytics.utils.AndroidUtils
@ -14,48 +15,48 @@ import io.reactivex.Flowable
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
import timber.log.Timber 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 var networkRequest: NetworkRequest? = null
private val connectStateSubject: BehaviorSubject<Boolean> = BehaviorSubject.createDefault(false) private val networkChangedSubject: BehaviorSubject<Int> = BehaviorSubject.createDefault(0)
val connectStateFlowable: Flowable<Boolean> private val connectivityManager by lazy {
get() = connectStateSubject.toFlowable(BackpressureStrategy.DROP) return@lazy context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
}
fun bindConnectionStateChanged(context: Context) { val networkChanged: Flowable<Int>
connectStateSubject.onNext(AndroidUtils.isInternetAvailable(context)) get() = networkChangedSubject.toFlowable(BackpressureStrategy.DROP)
fun bind() {
if (networkRequest == null) { if (networkRequest == null) {
networkRequest = NetworkRequest.Builder() networkRequest = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build() .build()
} }
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager connectivityManager.registerNetworkCallback(networkRequest!!, this)
cm?.registerNetworkCallback(networkRequest!!, this) networkChanged()
} }
fun unbindConnectionStateChanged(context: Context?) { fun unbind(context: Context?) {
val cm = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager val cm = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
cm?.unregisterNetworkCallback(this) cm?.unregisterNetworkCallback(this)
} }
private fun networkChanged() {
networkChangedSubject.onNext((networkChangedSubject.value ?: 0) + 1)
}
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
super.onAvailable(network) super.onAvailable(network)
Timber.d("${TAG}_onAvailable") networkChanged()
if (connectStateSubject.value != true) {
connectStateSubject.onNext(true)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.NETWORK_AVAILABLE)
}
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
super.onLost(network) super.onLost(network)
Timber.d("${TAG}_onLost") networkChanged()
if (connectStateSubject.value != false) {
connectStateSubject.onNext(false)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.NETWORK_LOST)
}
} }
} }

View File

@ -18,7 +18,7 @@ class PendingEvent(
val at = SystemClock.elapsedRealtime() val at = SystemClock.elapsedRealtime()
} }
object EventDispatcher : EventDeliver { data object EventDispatcher : EventDeliver {
private val pendingEvents = ConcurrentLinkedQueue<PendingEvent>() private val pendingEvents = ConcurrentLinkedQueue<PendingEvent>()

View File

@ -2,6 +2,15 @@ package guru.core.analytics.impl
import android.content.Context import android.content.Context
import android.net.Uri 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.Constants
import guru.core.analytics.GuruAnalytics import guru.core.analytics.GuruAnalytics
import guru.core.analytics.data.api.GuruRepository import guru.core.analytics.data.api.GuruRepository
@ -18,8 +27,8 @@ import guru.core.analytics.data.store.EventInfoStore
import guru.core.analytics.handler.AnalyticsCode import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler import guru.core.analytics.handler.EventHandler
import guru.core.analytics.log.PersistentTree import guru.core.analytics.log.PersistentTree
import guru.core.analytics.utils.AndroidUtils
import guru.core.analytics.utils.ApiParamUtils import guru.core.analytics.utils.ApiParamUtils
import guru.core.analytics.utils.Connectivity
import guru.core.analytics.utils.DateTimeUtils import guru.core.analytics.utils.DateTimeUtils
import io.reactivex.BackpressureStrategy import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable import io.reactivex.Flowable
@ -41,16 +50,17 @@ internal class EventEngine internal constructor(
private val batchLimit: Int = DEFAULT_BATCH_LIMIT, private val batchLimit: Int = DEFAULT_BATCH_LIMIT,
private val uploadPeriodInSeconds: Long = DEFAULT_UPLOAD_PERIOD_IN_SECONDS, private val uploadPeriodInSeconds: Long = DEFAULT_UPLOAD_PERIOD_IN_SECONDS,
private val eventExpiredInDays: Int = DEFAULT_EVENT_EXPIRED_IN_DAYS, private val eventExpiredInDays: Int = DEFAULT_EVENT_EXPIRED_IN_DAYS,
private val uploadEventBaseUri: Uri? = null, private val guruRepository: GuruRepository
) : EventDeliver { ) : EventDeliver {
private val guruRepository: GuruRepository by lazy {
ServiceLocator.provideGuruRepository(context.applicationContext, baseUri = uploadEventBaseUri)
}
private val preferencesManager by lazy { private val preferencesManager by lazy {
PreferencesManager.getInstance(context) PreferencesManager.getInstance(context)
} }
private val connectivity: Connectivity by lazy {
Connectivity(context)
}
private val lifecycleMonitor = AppLifecycleMonitor(context) private val lifecycleMonitor = AppLifecycleMonitor(context)
companion object { companion object {
@ -59,12 +69,17 @@ internal class EventEngine internal constructor(
const val DEFAULT_BATCH_LIMIT = 25 // 一次上传event最多数量 const val DEFAULT_BATCH_LIMIT = 25 // 一次上传event最多数量
const val DEFAULT_EVENT_EXPIRED_IN_DAYS = 7 const val DEFAULT_EVENT_EXPIRED_IN_DAYS = 7
const val SESSION_ACTIVE_INTERVAL = 15 * 60 * 1000 // 15分钟
private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor()) private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor())
private val dbScheduler = Schedulers.from(Executors.newSingleThreadExecutor()) private val dbScheduler = Schedulers.from(Executors.newSingleThreadExecutor())
private val dateTimeFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) private val dateTimeFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
@Volatile
var sessionActivated = false
fun logDebug(message: String, vararg args: Any?) { fun logDebug(message: String, vararg args: Any?) {
if (GuruAnalytics.INSTANCE.isDebug()) { if (GuruAnalytics.INSTANCE.isDebug()) {
@ -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) { fun logEvent(entity: EventEntity) {
Timber.log( Timber.log(
PersistentTree.PRIORITY_EVENT, PersistentTree.PRIORITY_EVENT, "[${dateTimeFormatter.format(entity.at)}] ${entity.event} ${entity.json}"
"[${dateTimeFormatter.format(entity.at)}] ${entity.event} ${entity.json}"
) )
} }
} }
private var compositeDisposable: CompositeDisposable = CompositeDisposable() private var compositeDisposable: CompositeDisposable = CompositeDisposable()
private val pendingEventSubject: PublishSubject<Int> = PublishSubject.create() private val pendingEventSubject: PublishSubject<Int> = PublishSubject.create()
private val forceUploadSubject: PublishSubject<Boolean> = PublishSubject.create() private val forceTriggerSubject: PublishSubject<String> = PublishSubject.create()
internal val started = AtomicBoolean(false)
private val started = AtomicBoolean(false)
private var enableUpload = true private var enableUpload = true
private var latestValidActionTs = 0L
fun setEnableUpload(enable: Boolean) { fun setEnableUpload(enable: Boolean) {
enableUpload = enable enableUpload = enable
@ -96,8 +117,19 @@ internal class EventEngine internal constructor(
fun start(startUploadDelay: Long?) { fun start(startUploadDelay: Long?) {
if (started.compareAndSet(false, true)) { if (started.compareAndSet(false, true)) {
prepare() prepare()
ConnectionStateMonitor.bindConnectionStateChanged(context.applicationContext) connectivity.bind()
lifecycleMonitor.initialize() lifecycleMonitor.initialize()
try {
if (connectivity.isNetworkAvailable()) {
logSessionActive()
logDebug("[1] session started!")
} else {
dispatchActiveWorker()
logDebug("dispatchActiveWorker!")
}
} catch (throwable: Throwable) {
logDebug("session started error!")
}
scheduler.scheduleDirect({ scheduler.scheduleDirect({
EventDispatcher.start() EventDispatcher.start()
logFirstOpen() logFirstOpen()
@ -112,8 +144,7 @@ internal class EventEngine internal constructor(
private fun logFirstOpen() { private fun logFirstOpen() {
if (preferencesManager.isFirstOpen == true) { if (preferencesManager.isFirstOpen == true) {
GuruAnalytics.INSTANCE.logEvent( GuruAnalytics.INSTANCE.logEvent(
Constants.Event.FIRST_OPEN, Constants.Event.FIRST_OPEN, options = AnalyticsOptions(EventPriority.EMERGENCE)
options = AnalyticsOptions(EventPriority.EMERGENCE)
) )
preferencesManager.isFirstOpen = false 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() { private fun startWork() {
Timber.d("startWork")
pollEvents() pollEvents()
forceUpload() forceUpload("startWork")
Timber.d("UploadEventDaemon started!!") Timber.d("UploadEventDaemon started!!")
} }
@ -149,28 +235,20 @@ internal class EventEngine internal constructor(
private fun pollEvents() { private fun pollEvents() {
logDebug("pollEvents()!! $uploadPeriodInSeconds $batchLimit") logDebug("pollEvents()!! $uploadPeriodInSeconds $batchLimit")
val flowable = val periodFlowable = Flowable.interval(5, uploadPeriodInSeconds, TimeUnit.SECONDS).onBackpressureDrop()
pendingEventSubject.buffer(uploadPeriodInSeconds, TimeUnit.SECONDS, batchLimit) val forceFlowable = forceTriggerSubject.toFlowable(BackpressureStrategy.DROP).map { scene ->
.toFlowable(BackpressureStrategy.DROP) logDebug("force trigger: $scene")
.doOnNext {
logDebug("pendingEvent ${it.size}")
} }
val networkFlowable = ConnectionStateMonitor.connectStateFlowable.doOnNext { val networkFlowable = connectivity.networkAvailableFlowable
GuruAnalyticsAudit.connectionState = it
logDebug("network $it")
}
val forceFlowable = forceUploadSubject.toFlowable(BackpressureStrategy.DROP)
compositeDisposable.add(Flowable.combineLatest( compositeDisposable.add(Flowable.combineLatest(
Flowable.merge(flowable, forceFlowable), networkFlowable periodFlowable, forceFlowable, networkFlowable,
) { _, connected -> connected } ) { _, _, available ->
.filter { GuruAnalyticsAudit.connectionState = available
logDebug("pollEvent filter $it") val uploadedRate = GuruAnalyticsAudit.uploaded / GuruAnalyticsAudit.total.toFloat()
return@filter it val ignoreAvailable = !GuruAnalyticsAudit.uploadReady && uploadedRate < 0.6
} logDebug("enableUpload:$enableUpload && (network:$available || ignoreAvailable: (${ignoreAvailable})[uploaded(${GuruAnalyticsAudit.uploaded}) / total(${GuruAnalyticsAudit.total}) = $uploadedRate])")
.filter { enableUpload } return@combineLatest enableUpload && (available || ignoreAvailable)
.flatMap { uploadEvents(500) } }.flatMap { uploadEvents(100) }.subscribe()
.subscribe()
) )
} }
@ -181,8 +259,7 @@ internal class EventEngine internal constructor(
val expiredTs = currentTs - eventExpiredInDays * DateTimeUtils.DAYS_IN_MILLIS val expiredTs = currentTs - eventExpiredInDays * DateTimeUtils.DAYS_IN_MILLIS
Timber.d("validateEvents $currentTs $expiredTs $eventExpiredInDays") Timber.d("validateEvents $currentTs $expiredTs $eventExpiredInDays")
val pair = GuruAnalyticsDatabase.getInstance().eventDao() val pair = GuruAnalyticsDatabase.getInstance().eventDao().deleteExpiredEvents(expiredTs)
.deleteExpiredEvents(expiredTs)
// 记录删除的事件数量 // 记录删除的事件数量
if (pair.first > 0) { if (pair.first > 0) {
val eventCountDeleted = preferencesManager.eventCountDeleted ?: 0 val eventCountDeleted = preferencesManager.eventCountDeleted ?: 0
@ -192,15 +269,15 @@ internal class EventEngine internal constructor(
GuruAnalyticsAudit.sessionDeleted += pair.first GuruAnalyticsAudit.sessionDeleted += pair.first
val extMap = mapOf( val extMap = mapOf(
"expiredCount" to pair.first, "expiredCount" to pair.first, "allDeleteCount" to preferencesManager.eventCountDeleted
"allDeleteCount" to preferencesManager.eventCountDeleted
) )
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.DELETE_EXPIRED, extMap) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.DELETE_EXPIRED, extMap)
} }
emitter.onSuccess(pair) emitter.onSuccess(pair)
}.subscribeOn(dbScheduler) }.subscribeOn(dbScheduler).onErrorReturn {
.onErrorReturn { EventHandler.INSTANCE.notifyEventHandler(
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_DELETE_EXPIRED, it.message) AnalyticsCode.ERROR_DELETE_EXPIRED, it.message
)
Pair(-1, -1) Pair(-1, -1)
} }
} }
@ -209,18 +286,15 @@ internal class EventEngine internal constructor(
val eventDao = GuruAnalyticsDatabase.getInstance().eventDao() val eventDao = GuruAnalyticsDatabase.getInstance().eventDao()
GuruAnalyticsAudit.uploadReady = true GuruAnalyticsAudit.uploadReady = true
logDebug("uploadEvents: $count") logDebug("uploadEvents: $count")
return Flowable.just(count).map { eventDao.loadAndMarkUploadEvents(it) } return Flowable.just(count).map { eventDao.loadAndMarkUploadEvents(it) }.onErrorReturn {
.onErrorReturn { try {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_LOAD_MARK, it.message)
eventDao.loadAndMarkUploadEvents(batchLimit) eventDao.loadAndMarkUploadEvents(batchLimit)
} catch (throwable: Throwable) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_LOAD_MARK, it.message)
emptyList()
} }
.filter { it.isNotEmpty() } }.filter { it.isNotEmpty() }.subscribeOn(dbScheduler).observeOn(scheduler).concatMap { splitEntities(it) }
.subscribeOn(dbScheduler) .flatMapSingle { uploadEventsInternal(it) }.filter { it.isNotEmpty() }.doOnNext {
.observeOn(scheduler)
.concatMap { splitEntities(it) }
.flatMapSingle { uploadEventsInternal(it) }
.filter { it.isNotEmpty() }
.doOnNext {
eventDao.deleteEvents(it) eventDao.deleteEvents(it)
if (GuruAnalytics.INSTANCE.isDebug()) { if (GuruAnalytics.INSTANCE.isDebug()) {
for (entity in it) { for (entity in it) {
@ -230,22 +304,11 @@ internal class EventEngine internal constructor(
} }
} }
private fun uploadEventsInternal( private fun uploadEventsInternal(entities: List<EventEntity>): Single<List<EventEntity>> {
entities: List<EventEntity>,
withoutDelay: Boolean = false
): Single<List<EventEntity>> {
val param = ApiParamUtils.generateApiParam(entities) val param = ApiParamUtils.generateApiParam(entities)
return guruRepository.uploadEvents(param).map { true } return guruRepository.uploadEvents(param).map { true }.observeOn(dbScheduler).doOnError {
.observeOn(dbScheduler)
.doOnError { }.doOnSuccess {
if (withoutDelay) {
GuruAnalyticsDatabase.getInstance().eventDao().addEvents(entities)
} else {
GuruAnalyticsDatabase.getInstance().eventDao().updateEventDefault(entities)
}
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_FAIL, it.message)
}
.doOnSuccess {
// 记录上传成功的数量 // 记录上传成功的数量
val eventCountUploaded = preferencesManager.eventCountUploaded ?: 0 val eventCountUploaded = preferencesManager.eventCountUploaded ?: 0
val uploaded = eventCountUploaded + entities.size val uploaded = eventCountUploaded + entities.size
@ -258,59 +321,49 @@ internal class EventEngine internal constructor(
"eventNames" to entities.joinToString(",") { it.event }, "eventNames" to entities.joinToString(",") { it.event },
"allUploadedCount" to preferencesManager.eventCountUploaded, "allUploadedCount" to preferencesManager.eventCountUploaded,
) )
logDebug("uploadEvents success: $extMap")
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_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
} }
.onErrorReturn { false } false
.map { if (it) entities else emptyList() } }.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 {}
)
} }
override fun deliverEvent(item: EventItem, options: AnalyticsOptions) { override fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
pendingEventSubject.onNext(1)
// 记录收到的事件数量 // 记录收到的事件数量
increaseEventCount() val delivered = increaseEventCount()
pendingEventSubject.onNext(1)
if (options.priority == EventPriority.EMERGENCE) { 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 eventCountAll = preferencesManager.eventCountAll ?: 0
val total = eventCountAll + 1 val total = eventCountAll + 1
preferencesManager.eventCountAll = total preferencesManager.eventCountAll = total
GuruAnalyticsAudit.total = total GuruAnalyticsAudit.total = total
GuruAnalyticsAudit.sessionTotal ++ GuruAnalyticsAudit.sessionTotal++
return total
} }
override fun deliverProperty(name: String, value: String) { override fun deliverProperty(name: String, value: String) {
@ -321,8 +374,8 @@ internal class EventEngine internal constructor(
} }
private fun forceUpload() { internal fun forceUpload(scene: String = "unknown") {
forceUploadSubject.onNext(true) forceTriggerSubject.onNext(scene)
} }
fun getEventsStatics(): EventStatistic { fun getEventsStatics(): EventStatistic {

View File

@ -2,12 +2,15 @@ package guru.core.analytics.impl
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.transition.Scene
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import androidx.work.* import androidx.work.*
import guru.core.analytics.Constants import guru.core.analytics.Constants
import guru.core.analytics.GuruAnalytics 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.ServiceLocator
import guru.core.analytics.data.api.cronet.CronetHelper 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.GuruAnalyticsDatabase
import guru.core.analytics.data.db.model.EventStatistic import guru.core.analytics.data.db.model.EventStatistic
import guru.core.analytics.data.local.PreferencesManager import guru.core.analytics.data.local.PreferencesManager
@ -35,26 +38,28 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
companion object { companion object {
private val reservedEventNames = setOf("test") private val reservedEventNames = setOf("test")
private val internalVersion = "0.2.1.0" private val internalVersion = "0.2.1.0"
internal val timberPlanted = AtomicBoolean(false)
} }
private val initialized = AtomicBoolean(false)
private var engine: EventEngine? = null private var engine: EventEngine? = null
private var lifecycleMonitor: AppLifecycleMonitor? = null
private var debugMode = false private var debugMode = false
private val deliverExecutor = Executors.newSingleThreadExecutor() private val deliverExecutor = Executors.newSingleThreadExecutor()
private val delivers = CopyOnWriteArrayList<EventDeliver>() private val delivers = CopyOnWriteArrayList<EventDeliver>()
private val initialized = AtomicBoolean(false)
override fun isDebug(): Boolean = debugMode override fun isDebug(): Boolean = debugMode
override fun setDebug(debug: Boolean) { override fun setDebug(debug: Boolean) {
debugMode = debug debugMode = debug
} }
fun isInitialized() = initialized.get()
override fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String) { override fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String) {
if (updateEventBaseUrl.isEmpty()) { if (updateEventBaseUrl.isEmpty()) {
throw IllegalArgumentException("updateEventBaseUrl:${updateEventBaseUrl} is empty") throw IllegalArgumentException("updateEventBaseUrl:${updateEventBaseUrl} is empty")
@ -82,53 +87,70 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
mainProcess: String?, mainProcess: String?,
isEnableCronet: Boolean?, isEnableCronet: Boolean?,
uploadIpAddress: List<String>?, uploadIpAddress: List<String>?,
dnsMode: Int?
) { ) {
if (initialized.compareAndSet(false, true)) { 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 debugApp = SystemProperties.read("debug.guru.analytics.app")
val forceDebug = debugApp == context.packageName val forceDebug = debugApp == context.packageName
if (forceDebug || persistableLog) { if (forceDebug || persistableLog) {
try {
if (timberPlanted.compareAndSet(false, true)) {
Timber.plant(PersistentTree(context, debug = debug)) 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) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_2)
val debugUrl = SystemProperties.read("debug.guru.analytics.url") val debugUrl = SystemProperties.read("debug.guru.analytics.url")
debugMode = forceDebug || debug debugMode = forceDebug || debug
Timber.d("[$internalVersion]initialize batchLimit:$batchLimit uploadPeriodInSecond:$uploadPeriodInSeconds startUploadDelayInSecond:$startUploadDelayInSecond eventExpiredInDays:$eventExpiredInDays debug:$debugMode debugUrl:$debugUrl") Timber.d("[$internalVersion]initialize batchLimit:$batchLimit uploadPeriodInSecond:$uploadPeriodInSeconds startUploadDelayInSecond:$startUploadDelayInSecond eventExpiredInDays:$eventExpiredInDays debug:$debugMode debugUrl:$debugUrl uploadIpAddress:$uploadIpAddress dnsMode:$dnsMode")
GuruAnalyticsDatabase.initialize(context.applicationContext) GuruAnalyticsDatabase.initialize(context.applicationContext)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_3) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_3)
val baseUrl = if (forceDebug && debugUrl.isNotEmpty()) debugUrl else uploadEventBaseUrl val baseUrl = if (forceDebug && debugUrl.isNotEmpty()) debugUrl else (uploadEventBaseUrl ?: AnalyticsApiHost.BASE_URL)
DeviceInfoStore.setDeviceInfo(context) // TODO: 此处存在安全隐患,后续需重构
ServiceLocator.setDebug(debug) ServiceLocator.setDebug(debug)
ServiceLocator.addHeaderParam("X-APP-ID", xAppId) ServiceLocator.addHeaderParam("X-APP-ID", xAppId)
ServiceLocator.addHeaderParam("X-DEVICE-INFO", xDeviceInfo) ServiceLocator.addHeaderParam("X-DEVICE-INFO", xDeviceInfo)
ServiceLocator.setUploadIpAddress(uploadIpAddress) ServiceLocator.setUploadIpAddress(uploadIpAddress)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_4) ServiceLocator.setDnsMode(dnsMode ?: 0)
CronetHelper.init(context, isEnableCronet) { EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_4)
if (isEnableCronet == true) {
GuruAnalyticsAudit.enabledCronet = true
ServiceLocator.setCronet(true)
CronetHelper.init(context, true) {
if (it) { if (it) {
setUserProperty(Constants.Properties.GURU_ANM, Constants.AnmState.CRONET) setUserProperty(Constants.Properties.GURU_ANM, Constants.AnmState.CRONET)
GuruAnalyticsAudit.useCronet = true GuruAnalyticsAudit.useCronet = true
} }
} }
}
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_5) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_5)
var uploadEventBaseUri: Uri? = null var uploadEventBaseUri: Uri? = null
var hostname: String? = ""
if (!baseUrl.isNullOrBlank()) { if (!baseUrl.isNullOrBlank()) {
uploadEventBaseUri = uploadEventBaseUri =
kotlin.runCatching { Uri.parse(baseUrl) }.getOrNull() kotlin.runCatching { Uri.parse(baseUrl) }.getOrNull()
if (uploadEventBaseUri?.scheme?.startsWith("http") != true) { if (uploadEventBaseUri?.scheme?.startsWith("http") != true) {
throw IllegalArgumentException("initialize updateEventBaseUrl:${baseUrl} incorrect format") throw IllegalArgumentException("initialize updateEventBaseUrl:${baseUrl} incorrect format")
} }
hostname = uploadEventBaseUri.host
} }
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_6) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_6)
ServiceLocator.preloadDns(hostname)
engine = EventEngine( engine = EventEngine(
context, context,
batchLimit = batchLimit ?: EventEngine.DEFAULT_BATCH_LIMIT, 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.coerceAtLeast(
eventExpiredInDays ?: EventEngine.DEFAULT_EVENT_EXPIRED_IN_DAYS eventExpiredInDays ?: EventEngine.DEFAULT_EVENT_EXPIRED_IN_DAYS
), ),
uploadEventBaseUri = uploadEventBaseUri guruRepository = ServiceLocator.provideGuruRepository(context, baseUri = uploadEventBaseUri) /// 此处需要配合 ServiceLocator的重构进行修改
).apply { ).apply {
delivers.add(this) delivers.add(this)
start(startUploadDelayInSecond) start(startUploadDelayInSecond)
@ -147,7 +169,11 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
val process = AndroidUtils.getProcessName(context) val process = AndroidUtils.getProcessName(context)
Timber.d("initialize ${mainProcess == process} currentProcess:$process mainProcess:$mainProcess") Timber.d("initialize ${mainProcess == process} currentProcess:$process mainProcess:$mainProcess")
if (mainProcess.isNullOrBlank() || mainProcess == process) { if (mainProcess.isNullOrBlank() || mainProcess == process) {
try {
initAnalyticsPeriodic(context) initAnalyticsPeriodic(context)
} catch (throwable: Throwable) {
Timber.d("init worker error!")
}
} else { } else {
if (!process.isNullOrBlank()) { if (!process.isNullOrBlank()) {
val params = mutableMapOf<String, Any>() val params = mutableMapOf<String, Any>()
@ -173,6 +199,10 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
if (!uploadEventBaseUrl.isNullOrEmpty()) { if (!uploadEventBaseUrl.isNullOrEmpty()) {
PreferencesManager.getInstance(context).uploadEventBaseUrl = baseUrl 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) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_9)
GuruAnalyticsAudit.initialized = true GuruAnalyticsAudit.initialized = true
} }
@ -180,28 +210,35 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
private fun initAnalyticsPeriodic(context: Context) { private fun initAnalyticsPeriodic(context: Context) {
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) .setRequiredNetworkType(NetworkType.UNMETERED)
.build() .build()
val request = PeriodicWorkRequestBuilder<AnalyticsWorker>( val request = PeriodicWorkRequestBuilder<AnalyticsWorker>(
6, TimeUnit.HOURS, 40, TimeUnit.MINUTES,
15, TimeUnit.MINUTES 15, TimeUnit.MINUTES
) )
.setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES)
.addTag(AnalyticsWorker.WORKER_TAG) .addTag(AnalyticsWorker.WORKER_TAG)
.setConstraints(constraints) .setConstraints(constraints)
.build() .build()
Timber.d("initAnalyticsPeriodic")
WorkManager.getInstance(context) WorkManager.getInstance(context)
.enqueueUniquePeriodicWork( .enqueueUniquePeriodicWork(
AnalyticsWorker.WORKER_ID, AnalyticsWorker.WORKER_NAME,
ExistingPeriodicWorkPolicy.REPLACE, request ExistingPeriodicWorkPolicy.KEEP, request
) )
// WorkManager.getInstance(context)
// .cancelAllWorkByTag(
// "AnalyticsWorker",
// )
val extMap = mapOf( val extMap = mapOf(
"repeatInterval" to "6h", "repeatInterval" to "40m",
"flexTimeInterval" to "15m", "flexTimeInterval" to "15m",
) )
Timber.d("initAnalyticsPeriodic completed")
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.PERIODIC_WORK_ENQUEUE, extMap) EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.PERIODIC_WORK_ENQUEUE, extMap)
} }
@ -326,6 +363,24 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
return GsonUtil.gson.toJson(snapshot) 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) { private fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
Timber.tag("GuruAnalytics").d("deliverEvent ${item.eventName}!") Timber.tag("GuruAnalytics").d("deliverEvent ${item.eventName}!")
deliverExecutor.execute { deliverExecutor.execute {

View File

@ -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())
}
}