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

View File

@ -1,17 +1,150 @@
package com.example.guruanalytics
import androidx.appcompat.app.AppCompatActivity
import android.app.usage.NetworkStats
import android.app.usage.NetworkStatsManager
import android.content.Context
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.TrafficStats
import android.os.Build
import android.os.Bundle
import android.os.RemoteException
import android.util.Log
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import guru.core.analytics.GuruAnalytics
import guru.core.analytics.data.api.dns.DnsMode
import guru.core.analytics.data.db.model.EventPriority
import guru.core.analytics.data.model.AnalyticsOptions
import guru.core.analytics.impl.ActiveWorker
import guru.core.analytics.impl.AnalyticsWorker
import java.util.concurrent.TimeUnit
object NetworkUsageUtils {
fun getCurrentAppNetworkUsage(context: Context) {
val uid = getApplicationUid(context)
val mobileRxBytes = TrafficStats.getUidRxBytes(uid) // 获取移动数据接收的字节数
val mobileTxBytes = TrafficStats.getUidTxBytes(uid) // 获取移动数据发送的字节数
val wifiRxBytes = TrafficStats.getTotalRxBytes() - TrafficStats.getMobileRxBytes() // 获取 Wi-Fi 接收的字节数
val wifiTxBytes = TrafficStats.getTotalTxBytes() - TrafficStats.getMobileTxBytes() // 获取 Wi-Fi 发送的字节数
if (mobileRxBytes == TrafficStats.UNSUPPORTED.toLong() || mobileTxBytes == TrafficStats.UNSUPPORTED.toLong()) {
println("该设备不支持 TrafficStats API")
} else {
println("当前应用的移动数据接收字节数: $mobileRxBytes")
println("当前应用的移动数据发送字节数: $mobileTxBytes")
println("当前应用的 Wi-Fi 数据接收字节数: $wifiRxBytes")
println("当前应用的 Wi-Fi 数据发送字节数: $wifiTxBytes")
}
}
private fun getApplicationUid(context: Context): Int {
try {
val applicationInfo = context.packageManager.getApplicationInfo(context.packageName, 0)
return applicationInfo.uid
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
return -1
}
}
fun isBackgroundDataRestricted(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (connectivityManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val status = connectivityManager.restrictBackgroundStatus
if (status == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED) {
// 限制背景数据
return true
}
}
}
return false
}
@RequiresApi(api = Build.VERSION_CODES.M)
fun getMobileDataUsage(context: Context, uid: Int): Long {
val networkStatsManager = context.getSystemService(Context.NETWORK_STATS_SERVICE) as NetworkStatsManager
val bucket: NetworkStats.Bucket
var dataUsage = 0L
try {
bucket = networkStatsManager.querySummaryForDevice(NetworkCapabilities.TRANSPORT_CELLULAR, null, 0, System.currentTimeMillis())
dataUsage = bucket.rxBytes + bucket.txBytes
} catch (e: RemoteException) {
e.printStackTrace()
}
return dataUsage
}
private fun dumpCapabilitiesList(capabilities: NetworkCapabilities?) {
val types: MutableList<String> = ArrayList()
if (capabilities == null
|| !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
) {
// types.add(Connectivity.CONNECTIVITY_NONE)
Log.d("hasCapability", "NetworkCapabilities.NET_CAPABILITY_INTERNET")
}
if (capabilities == null
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
) {
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_WIFI")
}
if (capabilities == null || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
) {
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_WIFI_AWARE")
}
if (capabilities == null || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_ETHERNET")
}
if (capabilities == null || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_VPN")
}
if (capabilities == null || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_CELLULAR")
}
if (capabilities == null || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) {
Log.d("hasTransport", "NetworkCapabilities.TRANSPORT_BLUETOOTH")
}
if (capabilities == null || capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
) {
Log.d("hasCapability", "NetworkCapabilities.NET_CAPABILITY_INTERNET")
}
}
fun dumpCap(context: Context) {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
dumpCapabilitiesList(capabilities)
val isActiveNetworkMetered = connectivityManager.isActiveNetworkMetered;
Log.d("dumpCap", "isActiveNetworkMetered: $isActiveNetworkMetered")
// val appUid = getApplicationUid(context)
// Log.d("dumpCap", "appUid: $appUid")
// val rx = getMobileDataUsage(context, appUid)
// Log.d("dumpCap", "dataUsage: $rx")
}
}
class MainActivity : AppCompatActivity() {
private var enableUpload = true
@RequiresApi(Build.VERSION_CODES.N)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@ -24,46 +157,101 @@ class MainActivity : AppCompatActivity() {
// callbackEventHandler = true,
// )
GuruAnalytics.Builder(this)
.setBatchLimit(25)
.setUploadPeriodInSeconds(60)
.setStartUploadDelayInSecond(3)
.setEventExpiredInDays(7)
.isPersistableLog(true)
.setEventHandlerCallback(eventHandler)
.isInitPeriodicWork(false)
.isDebug(BuildConfig.DEBUG)
// .setUploadEventBaseUrl("https://www.baidu.com")
// .setFgEventPeriodInSeconds(60L)
.setXAppId("test_x_app_id")
.setXDeviceInfo("test_x_device_info")
.setMainProcess("com.example.guruanalytics")
.isEnableCronet(true)
.setUploadIpAddress(listOf("3.210.96.186", "34.196.69.199"))
.build()
findViewById<TextView>(R.id.tvSetProperty).setOnClickListener {
GuruAnalytics.INSTANCE.setScreen("main")
GuruAnalytics.INSTANCE.setAdId("AD_ID_01")
GuruAnalytics.INSTANCE.setFirebaseId("FIREBASE_ID")
GuruAnalytics.INSTANCE.setUid("MBK-XXXXX")
}
findViewById<TextView>(R.id.tvLogEvent).setOnClickListener {
val map = mutableMapOf<String, Any>()
map["percent"] = 0.4
map["level"] = 2
map["from"] = "game"
GuruAnalytics.INSTANCE.logEvent("test_event", "game", "main", 10, map)
for (i in 0..200) {
val map = mutableMapOf<String, Any>()
map["key_$i"] = "value_$i"
map["percent"] = 0.4
map["level"] = 2
map["from"] = "game"
GuruAnalytics.INSTANCE.logEvent("test_event", "game", "main", 10, map)
}
}
findViewById<TextView>(R.id.tvOpenTestProcessActivity).setOnClickListener {
TestProcessActivity.startActivity(this)
// TestProcessActivity.startActivity(this)
GuruAnalytics.Builder(this)
.setBatchLimit(25)
.setUploadPeriodInSeconds(60)
.setStartUploadDelayInSecond(3)
.setEventExpiredInDays(7)
.isPersistableLog(true)
.setEventHandlerCallback(eventHandler)
.isInitPeriodicWork(true)
.isDebug(BuildConfig.DEBUG)
// .setUploadEventBaseUrl("https://www.baidu.com")
// .setFgEventPeriodInSeconds(60L)
.setXAppId("test_x_app_id")
.setXDeviceInfo("test_x_device_info")
.setMainProcess("com.example.guruanalytics")
// .isEnableCronet(true)
.setUploadEventBaseUrl("https://collect4.fungame.cloud")
.setDnsMode(DnsMode.COMPOSITE)
.build()
Log.w("GuruAnalytics", "GuruAnalytics.INSTANCE: completed")
}
findViewById<TextView>(R.id.tvLocalLog).setOnClickListener {
val startTime = System.currentTimeMillis()
GuruAnalytics.INSTANCE.zipLogs(this)
Log.i("get_local_log", "${System.currentTimeMillis() - startTime}")
// GuruAnalytics.INSTANCE.zipLogs(this)
// Log.i("get_local_log", "${System.currentTimeMillis() - startTime}")
// val request = OneTimeWorkRequestBuilder<AnalyticsWorker>(
// ).build()
//// .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.MINUTES)
//// .addTag(AnalyticsWorker.WORKER_TAG)
//// .setConstraints(constraints)
//// .build()
// WorkManager.getInstance(this)
// .enqueue(
// request
// )
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setTriggerContentMaxDelay(1, TimeUnit.MINUTES)
.build()
val request = OneTimeWorkRequestBuilder<ActiveWorker>()
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES)
.addTag(ActiveWorker.WORKER_TAG)
.setConstraints(constraints)
.setInitialDelay(5, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(this)
.enqueueUniqueWork(
ActiveWorker.WORKER_NAME,
ExistingWorkPolicy.KEEP,
request
)
Log.i(
"Worker",
"dispatchActiveWorker"
)
}
findViewById<TextView>(R.id.tvEventStatistic).setOnClickListener {
GuruAnalytics.INSTANCE.getEventsStatics().run {
GuruAnalytics.INSTANCE.snapshotAnalyticsAudit().run {
Log.i(
"UploadEventDaemon_main",
"$eventCountAll $eventCountDeleted $eventCountUploaded"
this
)
}
val eventStatistic = GuruAnalytics.INSTANCE.getEventsStatics()
// val restricted = NetworkUsageUtils.isBackgroundDataRestricted(this)
Log.i("getEventsStatics", "eventStatistic:$eventStatistic")
NetworkUsageUtils.dumpCap(this)
}
findViewById<TextView>(R.id.tvEventEmergence).setOnClickListener {
val map = mutableMapOf<String, Any>()
@ -77,7 +265,7 @@ class MainActivity : AppCompatActivity() {
)
}
findViewById<TextView>(R.id.tvBaseUrl).setOnClickListener {
GuruAnalytics.INSTANCE.setUploadEventBaseUrl(this, "https://www.castbox.fm/")
// GuruAnalytics.INSTANCE.setUploadEventBaseUrl(this, "https://www.castbox.fm/")
}
val tvEnable = findViewById<TextView>(R.id.tvEnable)
@ -88,11 +276,20 @@ class MainActivity : AppCompatActivity() {
tvEnable.text = if (enableUpload) "Enable Upload" else "Disable Upload"
}
val tvClearStatistic = findViewById<TextView>(R.id.tvClearStatistic)
tvClearStatistic?.setOnClickListener {
GuruAnalytics.INSTANCE.clearStatistic(this)
}
GuruAnalytics.INSTANCE.setScreen("main")
GuruAnalytics.INSTANCE.setAdId("AD_ID_01")
GuruAnalytics.INSTANCE.setFirebaseId("FIREBASE_ID")
GuruAnalytics.INSTANCE.setUid("MBK-YYYYY")
GuruAnalytics.INSTANCE.setUserProperty("uid", "110051")
GuruAnalytics.INSTANCE.setUserProperty("age", "12")
GuruAnalytics.INSTANCE.setUserProperty("sex", "male")
Log.d("Test", "setAdId")
val properties = GuruAnalytics.INSTANCE.peakUserProperties()
Log.i("peakUserProperties", "properties:$properties")
@ -100,11 +297,6 @@ class MainActivity : AppCompatActivity() {
GuruAnalytics.INSTANCE.getUserProperties {
Log.i("getUserProperties", "properties:$it")
}
tvEnable?.postDelayed({
val snapshot = GuruAnalytics.INSTANCE.snapshotAnalyticsAudit()
Log.i("snapshotAnalyticsAudit", "snapshot:$snapshot")
}, 10_000)
}
private val eventHandler: (Int, String?) -> Unit = { code, ext ->

View File

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

View File

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

View File

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.9.0'
repositories {
// maven { url 'http://localhost:8081/repository/maven-public/' }
@ -10,7 +10,7 @@ buildscript {
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.1"
classpath "com.android.tools.build:gradle:7.1.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

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
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -1,3 +1,5 @@
import java.text.SimpleDateFormat
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
@ -8,6 +10,15 @@ plugins {
apply from: 'dependencies.gradle'
apply from: 'maven-publish.gradle'
static def buildTs() {
def date = GregorianCalendar.getInstance(TimeZone.getTimeZone('UTC'))
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss")
String ts = dateFormat.format(date.getTime())
return "\"" + ts + "\""
}
android {
compileSdk android.compileSdk
@ -18,6 +29,8 @@ android {
versionCode 1
versionName "1.0"
buildConfigField "String", "buildTs", buildTs()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -4,33 +4,36 @@ import android.content.Context
import android.net.Uri
import android.os.SystemClock
import guru.core.analytics.data.api.cronet.CastboxCronetInterceptor
import guru.core.analytics.data.api.dns.CompositeDns
import guru.core.analytics.data.api.dns.CustomDns
import guru.core.analytics.data.api.dns.DnsMode
import guru.core.analytics.data.api.dns.GoogleDnsApi
import guru.core.analytics.data.api.dns.GoogleDnsApiHost
import guru.core.analytics.data.api.dns.StaticDns
import guru.core.analytics.data.api.logging.Level
import guru.core.analytics.data.api.logging.LoggingInterceptor
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
import guru.core.analytics.utils.AndroidUtils
import guru.core.analytics.utils.DateTimeUtils
import guru.core.analytics.utils.GsonUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.Dispatcher
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.internal.platform.Platform
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import timber.log.Timber
import java.util.concurrent.TimeUnit
// Todo: 该类都需要重构,业务不清晰,不符合打点库使用
object ServiceLocator {
private var debug = false
@ -38,11 +41,16 @@ object ServiceLocator {
@Volatile
private var guruRepository: GuruRepository? = null
@Volatile
private var googleDnsApi: GoogleDnsApi? = null
private val headerParams = mutableMapOf<String, String>()
private val compositeDns: CompositeDns by lazy {
return@lazy CompositeDns()
}
private var isEnabledCronet: Boolean = false
private var dnsMode: Int = 0
private var uploadIpAddress: List<String>? = null
fun addHeaderParam(key: String, value: String?) {
@ -54,10 +62,36 @@ object ServiceLocator {
this.debug = debug
}
@Synchronized
fun setUploadIpAddress(ipList: List<String>?) {
if (compositeDns.ipAddress != ipList) {
compositeDns.setCandidateIpAddress(ipAddress = ipList)
}
uploadIpAddress = ipList
}
fun setCronet(isEnabledCronet: Boolean) {
this.isEnabledCronet = isEnabledCronet
}
fun setDnsMode(dnsMode: Int) {
this.dnsMode = dnsMode
GuruAnalyticsAudit.dnsMode = dnsMode
}
fun preloadDns(hostname: String?) {
Timber.tag("ServiceLocator").i("preloadDns: $hostname")
if (dnsMode == DnsMode.COMPOSITE && !hostname.isNullOrBlank()) {
CoroutineScope(Dispatchers.IO).launch {
try {
val result = compositeDns.lookup(hostname)
Timber.tag(CompositeDns.tag).i("preloadDns: $hostname, result: $result")
} catch (throwable: Throwable) {
}
}
}
}
fun provideGuruRepository(context: Context, baseUri: Uri? = null): GuruRepository {
synchronized(this) {
return guruRepository
@ -97,14 +131,8 @@ object ServiceLocator {
guruRepository?.analyticsApi = createAnalyticsApi(context, baseUrl)
}
fun provideGoogleDnsApi(context: Context): GoogleDnsApi {
synchronized(this) {
return googleDnsApi
?: createGoogleDnsApi(context).apply { googleDnsApi = this }
}
}
private fun createGoogleDnsApi(context: Context): GoogleDnsApi {
fun createGoogleDnsApi(context: Context): GoogleDnsApi {
return GoogleDnsApi.Creator.newInstance(
Retrofit.Builder()
.baseUrl(GoogleDnsApiHost.API)
@ -120,30 +148,37 @@ object ServiceLocator {
private fun createOkHttpClient(
context: Context,
readTimeOut: Long = 30L,
writeTimeOut: Long = 30L
readTimeOut: Long = 90L,
writeTimeOut: Long = 90L,
): OkHttpClient {
val dns = when (dnsMode) {
DnsMode.COMPOSITE -> compositeDns
DnsMode.STATIC -> StaticDns(uploadIpAddress)
else -> CustomDns(context, uploadIpAddress = uploadIpAddress)
}
val builder = OkHttpClient.Builder()
.dispatcher(Dispatcher().apply {
maxRequests = 128
maxRequestsPerHost = 10
maxRequestsPerHost = 5
})
.dns(CustomDns(context, uploadIpAddress))
.connectTimeout(20L, TimeUnit.SECONDS)
.dns(dns)
.connectTimeout(90L, TimeUnit.SECONDS)
.readTimeout(readTimeOut, TimeUnit.SECONDS)
.writeTimeout(writeTimeOut, TimeUnit.SECONDS)
.addInterceptor(createCacheControlInterceptor(context))
.addInterceptor(createAnalyticsApiInterceptor())
.addInterceptor(createLoggingInterceptor())
.addInterceptor(createCronetInterceptor())
if (isEnabledCronet) {
builder.addInterceptor(createCronetInterceptor())
}
return builder.build()
}
private fun createDnsOkHttpClient(context: Context): OkHttpClient {
val builder = OkHttpClient.Builder()
.addInterceptor(createCacheControlInterceptor(context))
.connectTimeout(90L, TimeUnit.SECONDS)
.readTimeout(90L, TimeUnit.SECONDS)
.writeTimeout(90L, TimeUnit.SECONDS)
.addInterceptor(createLoggingInterceptor())
.addInterceptor(createCronetInterceptor())
return builder.build()
}
@ -164,32 +199,6 @@ object ServiceLocator {
return CastboxCronetInterceptor()
}
private fun createCacheControlInterceptor(context: Context) = Interceptor { chain ->
try {
val originalResponse = chain.proceed(chain.request())
when {
originalResponse.headers.names().contains("Cache-Control") -> {
return@Interceptor originalResponse.newBuilder().build()
}
AndroidUtils.isInternetAvailable(context) -> {
val maxAge = 60 * 5 // read from cache for 5 minute
return@Interceptor originalResponse.newBuilder()
.header("Cache-Control", "public, max-age=$maxAge")
.build()
}
else -> {
val maxStale = 60 * 60 * 24 * 28 // tolerate 4-weeks stale
return@Interceptor originalResponse.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=$maxStale")
.build()
}
}
} catch (e: Exception) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_CACHE_CONTROL, e.message)
throw e
}
}
private fun createAnalyticsApiInterceptor(): Interceptor {
return Interceptor { chain ->
val request = chain.request()

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 guru.core.analytics.data.api.ServiceLocator
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
import guru.core.analytics.utils.GsonUtil
@ -14,18 +15,28 @@ import java.net.UnknownHostException
class CustomDns(private val context: Context, private val uploadIpAddress: List<String>? = null) : Dns {
@Volatile
private var googleDnsApi: GoogleDnsApi? = null
private val cachedHostAddress by lazy {
val hostAddressJson = PreferencesManager.getInstance(context).hostAddressJson
return@lazy runCatching {
if (!hostAddressJson.isNullOrBlank()) {
val mapType = object: TypeToken<Map<String, List<String>>>() {}.type
val mapType = object : TypeToken<Map<String, List<String>>>() {}.type
GsonUtil.gson.fromJson(hostAddressJson, mapType) as? MutableMap<String, List<String>>
} else null
}.getOrNull() ?: mutableMapOf()
}
fun provideGoogleDnsApi(context: Context): GoogleDnsApi {
synchronized(this) {
return googleDnsApi
?: ServiceLocator.createGoogleDnsApi(context).apply { googleDnsApi = this }
}
}
override fun lookup(hostname: String): List<InetAddress> {
return try {
val ipList = try {
Dns.SYSTEM.lookup(hostname).also { list ->
cacheHostAddress(hostname, list.map { it.hostAddress })
}
@ -41,10 +52,13 @@ class CustomDns(private val context: Context, private val uploadIpAddress: List<
}
}
}
GuruAnalyticsAudit.serverIp = ipList.toString()
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_LOOKUP, "[Custom]:${ipList}")
return ipList
}
private fun lookupByGoogleDns(hostname: String): List<InetAddress> {
val dnsApi = ServiceLocator.provideGoogleDnsApi(context)
val dnsApi = provideGoogleDnsApi(context)
val ipList = runBlocking {
return@runBlocking dnsApi.ip(hostname).answer?.toMutableList()
?.filter { it.type == 1 }

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

View File

@ -51,13 +51,6 @@ abstract class EventDao {
@Transaction
open fun loadAndMarkUploadEvents(limit: Int): List<EventEntity> {
val events = getEvents(limit).toMutableList()
// if (events.isNotEmpty()) {
// Timber.tag(TAG).d("loadAndMarkUploadEvents limit:$limit size:${events.size}")
// FgEventHelper.getInstance().getFgEvent()?.let { entity ->
// addEvent(entity)
// events.add(entity)
// }
// }
updateEventUploading(events)
return events
}

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package guru.core.analytics.data.local
import android.annotation.SuppressLint
import android.content.Context
import guru.core.analytics.data.api.dns.DnsMode
class PreferencesManager private constructor(
context: Context,
@ -49,4 +50,15 @@ class PreferencesManager private constructor(
var uploadEventBaseUrl: String? by bind("update_event_base_url", "")
var totalDurationFgEvent: Long? by bind("total_duration_fg_event", 0L)
var hostAddressJson: String? by bind("host_address", "")
var uploadIpAddressList: String? by bind("upload_ip_address", "")
var useCompositeDns: Boolean? by bind("use_composite_dns", false)
var dnsMode: Int? by bind("dns_mode", DnsMode.DEFAULT)
var firebaseId: String? by bind("firebase_id", "")
var userId: String? by bind("user_id", "")
var adjustId: String? by bind("adjust_id", "")
var deviceId: String? by bind("device_id", "")
var adId: String? by bind("ad_id", "")
}

View File

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

View File

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

View File

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

View File

@ -1,16 +1,22 @@
package guru.core.analytics.data.store
import android.content.Context
import guru.core.analytics.Constants
import guru.core.analytics.data.db.model.Event
import guru.core.analytics.data.db.model.EventEntity
import guru.core.analytics.data.db.model.EventPriority
import guru.core.analytics.data.db.model.ParamValue
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.data.model.EventItem
import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.utils.AndroidUtils
import guru.core.analytics.utils.DateTimeUtils
import guru.core.analytics.utils.GsonUtil
import io.reactivex.subjects.BehaviorSubject
import timber.log.Timber
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
object EventInfoStore {
@ -19,6 +25,135 @@ object EventInfoStore {
UUID.randomUUID().toString()
}
val initialized = AtomicBoolean(false)
var preferenceMgr: PreferencesManager? = null
fun initialize(ctx: Context) {
if (initialized.compareAndSet(false, true)) {
val appVersion = AndroidUtils.getAppVersion(ctx)
setUserProperty("app_version", appVersion)
DeviceInfoStore.setDeviceInfo(ctx)
preferenceMgr = PreferencesManager.getInstance(ctx).apply {
restoreIds(this)
GuruAnalyticsAudit.total = this.eventCountAll ?: 0
GuruAnalyticsAudit.uploaded = this.eventCountUploaded ?: 0
}
}
}
private fun restoreIds(pm: PreferencesManager) {
val firebaseId = try {
pm.firebaseId ?: ""
} catch (throwable: Throwable) {
""
}
val userId = try {
pm.userId ?: ""
} catch (throwable: Throwable) {
""
}
val adId = try {
pm.adId ?: ""
} catch (throwable: Throwable) {
""
}
val adjustId = try {
pm.adjustId ?: ""
} catch (throwable: Throwable) {
""
}
val deviceId = try {
pm.deviceId ?: ""
} catch (throwable: Throwable) {
""
}
var restoreIdsMsg = "restoreIds: "
if (firebaseId.isNotEmpty()) {
val current = getId(Constants.Ids.FIREBASE_ID)
if (current != null && current != firebaseId) {
pm.firebaseId = current
restoreIdsMsg += " [${Constants.Ids.FIREBASE_ID}]:${firebaseId}=>${current} "
} else {
setIds(Constants.Ids.FIREBASE_ID, firebaseId)
restoreIdsMsg += " [${Constants.Ids.FIREBASE_ID}]:${firebaseId} "
}
} else {
val current = getId(Constants.Ids.FIREBASE_ID)
if (current != null) {
pm.firebaseId = current
}
}
if (userId.isNotEmpty()) {
val current = getId(Constants.Ids.UID)
if (current != null && current != userId) {
pm.userId = current
restoreIdsMsg += " [${Constants.Ids.UID}]:${userId}=>${current} "
} else {
setIds(Constants.Ids.UID, userId)
restoreIdsMsg += " [${Constants.Ids.UID}]:${userId} "
}
} else {
val current = getId(Constants.Ids.UID)
if (current != null) {
pm.userId = current
}
}
if (adjustId.isNotEmpty()) {
val current = getId(Constants.Ids.ADJUST_ID)
if (current != null && current != adjustId) {
pm.adjustId = current
restoreIdsMsg += " [${Constants.Ids.ADJUST_ID}]:${adjustId}=>${current} "
} else {
setIds(Constants.Ids.ADJUST_ID, adjustId)
restoreIdsMsg += " [${Constants.Ids.ADJUST_ID}]:${adjustId} "
}
} else {
val current = getId(Constants.Ids.ADJUST_ID)
if (current != null) {
pm.adjustId = current
}
}
if (deviceId.isNotEmpty()) {
val current = getId(Constants.Ids.DEVICE_ID)
if (current != null && current != deviceId) {
pm.deviceId = current
restoreIdsMsg += " [${Constants.Ids.DEVICE_ID}]:${deviceId}=>${current} "
} else {
setIds(Constants.Ids.DEVICE_ID, deviceId)
restoreIdsMsg += " [${Constants.Ids.DEVICE_ID}]:${deviceId} "
}
} else {
val current = getId(Constants.Ids.DEVICE_ID)
if (current != null) {
pm.deviceId = current
}
}
if (adId.isNotEmpty()) {
val current = getId(Constants.Ids.AD_ID)
if (current != null && current != adId) {
pm.adId = current
restoreIdsMsg += " [${Constants.Ids.AD_ID}]:${adId}=>${current} "
} else {
setIds(Constants.Ids.AD_ID, adId)
restoreIdsMsg += " [${Constants.Ids.AD_ID}]:${adId} "
}
} else {
val current = getId(Constants.Ids.AD_ID)
if (current != null) {
pm.adId = current
}
}
Timber.tag("EventInfoStore").i(restoreIdsMsg)
Timber.tag("EventInfoStore").i(ids.toString())
}
private val propertiesSubject: BehaviorSubject<ConcurrentHashMap<String, String>> =
BehaviorSubject.createDefault(
ConcurrentHashMap<String, String>().apply {
@ -75,24 +210,57 @@ object EventInfoStore {
ids = ids.plus(idName to id)
}
internal fun getId(idName: String): String? = ids[idName]
fun setDeviceId(deviceId: String) {
setIds(Constants.Ids.DEVICE_ID, deviceId)
if (deviceId.isNotEmpty()) {
val prev = getId(Constants.Ids.DEVICE_ID) ?: ""
setIds(Constants.Ids.DEVICE_ID, deviceId)
if (prev != deviceId) {
preferenceMgr?.deviceId = deviceId
}
}
}
fun setUid(uid: String) {
setIds(Constants.Ids.UID, uid)
if (uid.isNotEmpty()) {
val prev = getId(Constants.Ids.UID) ?: ""
setIds(Constants.Ids.UID, uid)
if (prev != uid) {
preferenceMgr?.userId = uid
}
}
}
fun setAdjustId(adjustId: String) {
setIds(Constants.Ids.ADJUST_ID, adjustId)
if (adjustId.isNotEmpty()) {
val prev = getId(Constants.Ids.ADJUST_ID) ?: ""
setIds(Constants.Ids.ADJUST_ID, adjustId)
if (prev != adjustId) {
preferenceMgr?.adjustId = adjustId
}
}
}
fun setAdId(adId: String) {
setIds(Constants.Ids.AD_ID, adId)
if (adId.isNotEmpty()) {
val prev = getId(Constants.Ids.AD_ID) ?: ""
setIds(Constants.Ids.AD_ID, adId)
if (prev != adId) {
preferenceMgr?.adId = adId
}
}
}
fun setFirebaseId(firebaseId: String) {
setIds(Constants.Ids.FIREBASE_ID, firebaseId)
if (firebaseId.isNotEmpty()) {
val prev = getId(Constants.Ids.FIREBASE_ID) ?: ""
setIds(Constants.Ids.FIREBASE_ID, firebaseId)
if (prev != firebaseId) {
preferenceMgr?.firebaseId = firebaseId
}
}
}
private fun createParamValue(value: Any): ParamValue {

View File

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

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
import android.content.Context
import android.net.Uri
import androidx.work.RxWorker
import androidx.work.WorkerParameters
import guru.core.analytics.GuruAnalytics
import guru.core.analytics.data.api.GuruRepository
import guru.core.analytics.data.api.ServiceLocator
import guru.core.analytics.data.api.dns.DnsMode
import guru.core.analytics.data.db.GuruAnalyticsDatabase
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.data.store.DeviceInfoStore
import guru.core.analytics.data.store.EventInfoStore
import guru.core.analytics.log.PersistentTree
import guru.core.analytics.utils.GsonUtil
import io.reactivex.Single
import timber.log.Timber
@ -14,21 +22,55 @@ class AnalyticsWorker(
workerParams: WorkerParameters
) : RxWorker(context.applicationContext, workerParams) {
companion object {
const val WORKER_ID = "GuruAnalytics"
const val WORKER_TAG = "Analytics"
const val WORKER_NAME = "AnalyticsUploader"
const val WORKER_TAG = "AnalyticsUploader"
}
private fun buildGuruRepository(context: Context): GuruRepository {
val preferenceMgr = PreferencesManager.getInstance(context)
val dnsMode = preferenceMgr.dnsMode ?: DnsMode.DEFAULT
ServiceLocator.setDnsMode(dnsMode)
val baseUrl = preferenceMgr.uploadEventBaseUrl
Timber.tag("AnalyticsWorker").i("baseUrl: $baseUrl useCompositeDns: $dnsMode")
if (baseUrl != null) {
try {
if (Uri.parse(baseUrl).scheme?.startsWith("http") == true) {
return ServiceLocator.provideGuruRepository(context, baseUri = Uri.parse(baseUrl))
}
} catch (throwable: Throwable) {
Timber.w("[Worker] Invalid base url: $baseUrl")
}
}
val uploadIpAddress = preferenceMgr.uploadIpAddressList?.split("|")
if (!uploadIpAddress.isNullOrEmpty()) {
ServiceLocator.setUploadIpAddress(uploadIpAddress)
}
return ServiceLocator.provideGuruRepository(context)
}
override fun createWork(): Single<Result> {
Timber.plant(PersistentTree(context))
GuruAnalyticsDatabase.initialize(context)
DeviceInfoStore.setDeviceInfo(context)
Timber.d("OnWork..")
val engine = EventEngine(context)
return engine.validateEvents().doOnSuccess {
Timber.d("validateEvents deleted:${it.first} reset:${it.second}")
}.toFlowable().flatMap { engine.uploadEvents(500) }.map { true }.toList()
.map { Result.success() }
.onErrorReturn { Result.success() }
if (GuruAnalyticsImpl.timberPlanted.compareAndSet(false, true)) {
Timber.plant(PersistentTree(context, debug = true))
}
val success = GuruAnalytics.INSTANCE.forceUpload("AnalyticsWorker")
Timber.d("OnWork..forceUpload:${success}")
return if (success) {
Single.just(Result.success())
} else {
GuruAnalyticsDatabase.initialize(context)
EventInfoStore.initialize(context)
Timber.d("OnWork..initialized")
val engine = EventEngine(context, guruRepository = buildGuruRepository(context))
engine.validateEvents().doOnSuccess {
Timber.d("validateEvents deleted:${it.first} reset:${it.second}")
}.toFlowable().flatMap { engine.uploadEvents(500) }.map { true }.toList()
.map { Result.success() }
.onErrorReturn { Result.success() }
}
}
}

View File

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

View File

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

View File

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

View File

@ -2,6 +2,15 @@ package guru.core.analytics.impl
import android.content.Context
import android.net.Uri
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.SystemClock
import androidx.work.WorkManager
import guru.core.analytics.Constants
import guru.core.analytics.GuruAnalytics
import guru.core.analytics.data.api.GuruRepository
@ -18,8 +27,8 @@ import guru.core.analytics.data.store.EventInfoStore
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
import guru.core.analytics.log.PersistentTree
import guru.core.analytics.utils.AndroidUtils
import guru.core.analytics.utils.ApiParamUtils
import guru.core.analytics.utils.Connectivity
import guru.core.analytics.utils.DateTimeUtils
import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
@ -41,16 +50,17 @@ internal class EventEngine internal constructor(
private val batchLimit: Int = DEFAULT_BATCH_LIMIT,
private val uploadPeriodInSeconds: Long = DEFAULT_UPLOAD_PERIOD_IN_SECONDS,
private val eventExpiredInDays: Int = DEFAULT_EVENT_EXPIRED_IN_DAYS,
private val uploadEventBaseUri: Uri? = null,
private val guruRepository: GuruRepository
) : EventDeliver {
private val guruRepository: GuruRepository by lazy {
ServiceLocator.provideGuruRepository(context.applicationContext, baseUri = uploadEventBaseUri)
}
private val preferencesManager by lazy {
PreferencesManager.getInstance(context)
}
private val connectivity: Connectivity by lazy {
Connectivity(context)
}
private val lifecycleMonitor = AppLifecycleMonitor(context)
companion object {
@ -59,12 +69,17 @@ internal class EventEngine internal constructor(
const val DEFAULT_BATCH_LIMIT = 25 // 一次上传event最多数量
const val DEFAULT_EVENT_EXPIRED_IN_DAYS = 7
const val SESSION_ACTIVE_INTERVAL = 15 * 60 * 1000 // 15分钟
private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor())
private val dbScheduler = Schedulers.from(Executors.newSingleThreadExecutor())
private val dateTimeFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
@Volatile
var sessionActivated = false
fun logDebug(message: String, vararg args: Any?) {
if (GuruAnalytics.INSTANCE.isDebug()) {
@ -72,20 +87,26 @@ internal class EventEngine internal constructor(
}
}
fun logInfo(message: String, vararg args: Any?) {
if (GuruAnalytics.INSTANCE.isDebug()) {
Timber.tag(TAG).i(message, args)
}
}
fun logEvent(entity: EventEntity) {
Timber.log(
PersistentTree.PRIORITY_EVENT,
"[${dateTimeFormatter.format(entity.at)}] ${entity.event} ${entity.json}"
PersistentTree.PRIORITY_EVENT, "[${dateTimeFormatter.format(entity.at)}] ${entity.event} ${entity.json}"
)
}
}
private var compositeDisposable: CompositeDisposable = CompositeDisposable()
private val pendingEventSubject: PublishSubject<Int> = PublishSubject.create()
private val forceUploadSubject: PublishSubject<Boolean> = PublishSubject.create()
private val started = AtomicBoolean(false)
private val forceTriggerSubject: PublishSubject<String> = PublishSubject.create()
internal val started = AtomicBoolean(false)
private var enableUpload = true
private var latestValidActionTs = 0L
fun setEnableUpload(enable: Boolean) {
enableUpload = enable
@ -96,8 +117,19 @@ internal class EventEngine internal constructor(
fun start(startUploadDelay: Long?) {
if (started.compareAndSet(false, true)) {
prepare()
ConnectionStateMonitor.bindConnectionStateChanged(context.applicationContext)
connectivity.bind()
lifecycleMonitor.initialize()
try {
if (connectivity.isNetworkAvailable()) {
logSessionActive()
logDebug("[1] session started!")
} else {
dispatchActiveWorker()
logDebug("dispatchActiveWorker!")
}
} catch (throwable: Throwable) {
logDebug("session started error!")
}
scheduler.scheduleDirect({
EventDispatcher.start()
logFirstOpen()
@ -112,8 +144,7 @@ internal class EventEngine internal constructor(
private fun logFirstOpen() {
if (preferencesManager.isFirstOpen == true) {
GuruAnalytics.INSTANCE.logEvent(
Constants.Event.FIRST_OPEN,
options = AnalyticsOptions(EventPriority.EMERGENCE)
Constants.Event.FIRST_OPEN, options = AnalyticsOptions(EventPriority.EMERGENCE)
)
preferencesManager.isFirstOpen = false
@ -121,9 +152,64 @@ internal class EventEngine internal constructor(
}
}
private fun logSessionActive() {
val item = EventItem(
eventName = "session_active", itemCategory = "success", params = mapOf(
"uploaded" to GuruAnalyticsAudit.uploaded,
"total" to GuruAnalyticsAudit.total,
"uid" to (EventInfoStore.getId(Constants.Ids.UID) ?: ""),
"fid" to (EventInfoStore.getId(Constants.Ids.FIREBASE_ID) ?: ""),
"method" to "start",
"server" to GuruAnalyticsAudit.serverIp
)
)
val event = EventInfoStore.deriveEvent(item, priority = EventPriority.HIGH)
val param = ApiParamUtils.generateApiParam(listOf(event))
guruRepository.uploadEvents(param).map { true }.doOnSuccess {
Timber.i("active success! $it")
logEvent(event)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_SESSION_ACTIVE)
sessionActivated = true
}.onErrorReturn {
Timber.e("active error! $it")
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_SESSION_START_ERROR, it.message)
if (!sessionActivated) {
dispatchActiveWorker()
}
return@onErrorReturn false
}.subscribeOn(Schedulers.io()).subscribe()
}
private fun dispatchActiveWorker() {
Timber.e("dispatchActiveWorker...")
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.build()
val request = OneTimeWorkRequestBuilder<ActiveWorker>()
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES)
.addTag(ActiveWorker.WORKER_TAG)
.setConstraints(constraints)
.setInitialDelay(5, TimeUnit.SECONDS)
.build()
try {
WorkManager.getInstance(context)
.enqueueUniqueWork(
ActiveWorker.WORKER_NAME,
ExistingWorkPolicy.KEEP,
request
)
} catch (throwable: Throwable) {
Timber.tag("EventEngine").i("dispatchActiveWorker error: $throwable")
}
}
private fun startWork() {
Timber.d("startWork")
pollEvents()
forceUpload()
forceUpload("startWork")
Timber.d("UploadEventDaemon started!!")
}
@ -149,28 +235,20 @@ internal class EventEngine internal constructor(
private fun pollEvents() {
logDebug("pollEvents()!! $uploadPeriodInSeconds $batchLimit")
val flowable =
pendingEventSubject.buffer(uploadPeriodInSeconds, TimeUnit.SECONDS, batchLimit)
.toFlowable(BackpressureStrategy.DROP)
.doOnNext {
logDebug("pendingEvent ${it.size}")
}
val networkFlowable = ConnectionStateMonitor.connectStateFlowable.doOnNext {
GuruAnalyticsAudit.connectionState = it
logDebug("network $it")
val periodFlowable = Flowable.interval(5, uploadPeriodInSeconds, TimeUnit.SECONDS).onBackpressureDrop()
val forceFlowable = forceTriggerSubject.toFlowable(BackpressureStrategy.DROP).map { scene ->
logDebug("force trigger: $scene")
}
val forceFlowable = forceUploadSubject.toFlowable(BackpressureStrategy.DROP)
val networkFlowable = connectivity.networkAvailableFlowable
compositeDisposable.add(Flowable.combineLatest(
Flowable.merge(flowable, forceFlowable), networkFlowable
) { _, connected -> connected }
.filter {
logDebug("pollEvent filter $it")
return@filter it
}
.filter { enableUpload }
.flatMap { uploadEvents(500) }
.subscribe()
periodFlowable, forceFlowable, networkFlowable,
) { _, _, available ->
GuruAnalyticsAudit.connectionState = available
val uploadedRate = GuruAnalyticsAudit.uploaded / GuruAnalyticsAudit.total.toFloat()
val ignoreAvailable = !GuruAnalyticsAudit.uploadReady && uploadedRate < 0.6
logDebug("enableUpload:$enableUpload && (network:$available || ignoreAvailable: (${ignoreAvailable})[uploaded(${GuruAnalyticsAudit.uploaded}) / total(${GuruAnalyticsAudit.total}) = $uploadedRate])")
return@combineLatest enableUpload && (available || ignoreAvailable)
}.flatMap { uploadEvents(100) }.subscribe()
)
}
@ -181,8 +259,7 @@ internal class EventEngine internal constructor(
val expiredTs = currentTs - eventExpiredInDays * DateTimeUtils.DAYS_IN_MILLIS
Timber.d("validateEvents $currentTs $expiredTs $eventExpiredInDays")
val pair = GuruAnalyticsDatabase.getInstance().eventDao()
.deleteExpiredEvents(expiredTs)
val pair = GuruAnalyticsDatabase.getInstance().eventDao().deleteExpiredEvents(expiredTs)
// 记录删除的事件数量
if (pair.first > 0) {
val eventCountDeleted = preferencesManager.eventCountDeleted ?: 0
@ -192,35 +269,32 @@ internal class EventEngine internal constructor(
GuruAnalyticsAudit.sessionDeleted += pair.first
val extMap = mapOf(
"expiredCount" to pair.first,
"allDeleteCount" to preferencesManager.eventCountDeleted
"expiredCount" to pair.first, "allDeleteCount" to preferencesManager.eventCountDeleted
)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.DELETE_EXPIRED, extMap)
}
emitter.onSuccess(pair)
}.subscribeOn(dbScheduler)
.onErrorReturn {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_DELETE_EXPIRED, it.message)
Pair(-1, -1)
}
}.subscribeOn(dbScheduler).onErrorReturn {
EventHandler.INSTANCE.notifyEventHandler(
AnalyticsCode.ERROR_DELETE_EXPIRED, it.message
)
Pair(-1, -1)
}
}
internal fun uploadEvents(count: Int): Flowable<List<EventEntity>> {
val eventDao = GuruAnalyticsDatabase.getInstance().eventDao()
GuruAnalyticsAudit.uploadReady = true
logDebug("uploadEvents: $count")
return Flowable.just(count).map { eventDao.loadAndMarkUploadEvents(it) }
.onErrorReturn {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_LOAD_MARK, it.message)
return Flowable.just(count).map { eventDao.loadAndMarkUploadEvents(it) }.onErrorReturn {
try {
eventDao.loadAndMarkUploadEvents(batchLimit)
} catch (throwable: Throwable) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_LOAD_MARK, it.message)
emptyList()
}
.filter { it.isNotEmpty() }
.subscribeOn(dbScheduler)
.observeOn(scheduler)
.concatMap { splitEntities(it) }
.flatMapSingle { uploadEventsInternal(it) }
.filter { it.isNotEmpty() }
.doOnNext {
}.filter { it.isNotEmpty() }.subscribeOn(dbScheduler).observeOn(scheduler).concatMap { splitEntities(it) }
.flatMapSingle { uploadEventsInternal(it) }.filter { it.isNotEmpty() }.doOnNext {
eventDao.deleteEvents(it)
if (GuruAnalytics.INSTANCE.isDebug()) {
for (entity in it) {
@ -230,87 +304,66 @@ internal class EventEngine internal constructor(
}
}
private fun uploadEventsInternal(
entities: List<EventEntity>,
withoutDelay: Boolean = false
): Single<List<EventEntity>> {
private fun uploadEventsInternal(entities: List<EventEntity>): Single<List<EventEntity>> {
val param = ApiParamUtils.generateApiParam(entities)
return guruRepository.uploadEvents(param).map { true }
.observeOn(dbScheduler)
.doOnError {
if (withoutDelay) {
GuruAnalyticsDatabase.getInstance().eventDao().addEvents(entities)
} else {
GuruAnalyticsDatabase.getInstance().eventDao().updateEventDefault(entities)
}
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_FAIL, it.message)
return guruRepository.uploadEvents(param).map { true }.observeOn(dbScheduler).doOnError {
}.doOnSuccess {
// 记录上传成功的数量
val eventCountUploaded = preferencesManager.eventCountUploaded ?: 0
val uploaded = eventCountUploaded + entities.size
preferencesManager.eventCountUploaded = uploaded
GuruAnalyticsAudit.uploaded = uploaded
GuruAnalyticsAudit.sessionUploaded += entities.size
val extMap = mapOf(
"count" to entities.size,
"eventNames" to entities.joinToString(",") { it.event },
"allUploadedCount" to preferencesManager.eventCountUploaded,
)
logDebug("uploadEvents success: $extMap")
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_SUCCESS, extMap)
latestValidActionTs = android.os.SystemClock.elapsedRealtime()
}.onErrorReturn {
GuruAnalyticsDatabase.getInstance().eventDao().updateEventDefault(entities)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_FAIL, it.message)
val elapsed = android.os.SystemClock.elapsedRealtime()
val uploadedRate = GuruAnalyticsAudit.sessionUploaded / GuruAnalyticsAudit.sessionTotal
val isActiveNetworkMetered = connectivity.isActiveNetworkMetered
val exceededValidActionGap = elapsed - latestValidActionTs > SESSION_ACTIVE_INTERVAL
logInfo("uploadEvent error $it sessionActivated:$sessionActivated uploadedRate:$uploadedRate isActiveNetworkMetered:$isActiveNetworkMetered gap:${(elapsed - latestValidActionTs) / 1000}s")
/// 如果没有激活并且当前是计费网络并且距离上次上传成功时间超过15分钟激活worker
/// 因为这个 worker 会在非计费网络下尝试执行
if (!sessionActivated && uploadedRate < 0.6 && isActiveNetworkMetered && exceededValidActionGap) {
logInfo("Metered Network Active! But upload event failed rate > 0.4! So dispatch ActiveWorker")
dispatchActiveWorker()
latestValidActionTs = elapsed
}
.doOnSuccess {
// 记录上传成功的数量
val eventCountUploaded = preferencesManager.eventCountUploaded ?: 0
val uploaded = eventCountUploaded + entities.size
preferencesManager.eventCountUploaded = uploaded
GuruAnalyticsAudit.uploaded = uploaded
GuruAnalyticsAudit.sessionUploaded += entities.size
val extMap = mapOf(
"count" to entities.size,
"eventNames" to entities.joinToString(",") { it.event },
"allUploadedCount" to preferencesManager.eventCountUploaded,
)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_SUCCESS, extMap)
}
.onErrorReturn { false }
.map { if (it) entities else emptyList() }
}
private fun logEventWithoutDelay(
eventName: String,
itemCategory: String? = null,
itemName: String? = null,
value: Int? = null,
parameters: Map<String, Any>? = null
) {
val item = EventItem(
eventName = eventName,
itemCategory = itemCategory,
itemName = itemName,
value = value,
params = parameters
)
val event = EventInfoStore.deriveEvent(item)
compositeDisposable.add(
Flowable.just(mutableListOf(event))
.flatMap {
return@flatMap if (!AndroidUtils.isInternetAvailable(context)
) {
GuruAnalyticsDatabase.getInstance().eventDao().addEvents(it)
Flowable.empty()
} else {
Flowable.just(it)
}
}
.flatMapSingle { uploadEventsInternal(it, withoutDelay = true) }
.subscribe {}
)
false
}.map { if (it) entities else emptyList() }
}
override fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
pendingEventSubject.onNext(1)
// 记录收到的事件数量
increaseEventCount()
val delivered = increaseEventCount()
pendingEventSubject.onNext(1)
if (options.priority == EventPriority.EMERGENCE) {
forceUpload()
logInfo("EMERGENCE: ${item.eventName} forceUpload")
forceUpload("EMERGENCE")
} else if (delivered and 0x1F == 0x1F) {
logInfo("Already delivered $delivered events!! forceUpload")
forceUpload("DELIVERED")
}
}
private fun increaseEventCount() {
private fun increaseEventCount(): Int {
val eventCountAll = preferencesManager.eventCountAll ?: 0
val total = eventCountAll + 1
preferencesManager.eventCountAll = total
GuruAnalyticsAudit.total = total
GuruAnalyticsAudit.sessionTotal ++
GuruAnalyticsAudit.sessionTotal++
return total
}
override fun deliverProperty(name: String, value: String) {
@ -321,8 +374,8 @@ internal class EventEngine internal constructor(
}
private fun forceUpload() {
forceUploadSubject.onNext(true)
internal fun forceUpload(scene: String = "unknown") {
forceTriggerSubject.onNext(scene)
}
fun getEventsStatics(): EventStatistic {

View File

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

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