|
|
@@ -1,298 +0,0 @@
|
|
|
-package com.adealink.frame.statistics.report
|
|
|
-
|
|
|
-import android.database.sqlite.SQLiteFullException
|
|
|
-import android.os.SystemClock
|
|
|
-import androidx.room.Room
|
|
|
-import com.adealink.frame.base.AppBase
|
|
|
-import com.adealink.frame.coroutine.dispatcher.Dispatcher
|
|
|
-import com.adealink.frame.data.json.froJsonErrorNull
|
|
|
-import com.adealink.frame.data.json.toJsonErrorNull
|
|
|
-import com.adealink.frame.statistics.CommonEventKey
|
|
|
-import com.adealink.frame.statistics.IStatConfig
|
|
|
-import com.adealink.frame.statistics.TAG_STAT_WENEXT
|
|
|
-import com.adealink.frame.statistics.report.data.WeNextReportData
|
|
|
-import com.adealink.frame.statistics.report.stat.WeNextReportStatEvent
|
|
|
-import com.adealink.frame.statistics.report.storage.WeNextStatData
|
|
|
-import com.adealink.frame.statistics.report.storage.WeNextStatDatabase
|
|
|
-import com.adealink.frame.util.ONE_MINUTE
|
|
|
-import com.adealink.frame.util.ONE_SECOND
|
|
|
-import com.adealink.frame.util.closeQuietly
|
|
|
-import kotlinx.coroutines.CoroutineScope
|
|
|
-import kotlinx.coroutines.SupervisorJob
|
|
|
-import kotlinx.coroutines.delay
|
|
|
-import kotlinx.coroutines.launch
|
|
|
-import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
|
-import okhttp3.Request
|
|
|
-import okhttp3.RequestBody
|
|
|
-import okhttp3.RequestBody.Companion.toRequestBody
|
|
|
-import okhttp3.Response
|
|
|
-import okio.Buffer
|
|
|
-import java.util.concurrent.atomic.AtomicBoolean
|
|
|
-
|
|
|
-
|
|
|
-/**
|
|
|
- * Created by sunxiaodong on 2024/11/28.
|
|
|
- */
|
|
|
-
|
|
|
-private var statInitiator: (() -> IStatConfig)? = null
|
|
|
-internal val statConfig by lazy { statInitiator!!() }
|
|
|
-
|
|
|
-fun initStat(initiator: (() -> IStatConfig)) {
|
|
|
- statInitiator = initiator
|
|
|
-}
|
|
|
-
|
|
|
-val weNextReporter: IWeNextReporter by lazy { WeNextReporter(statConfig) }
|
|
|
-
|
|
|
-class WeNextReporter(val config: IStatConfig) : IWeNextReporter,
|
|
|
- CoroutineScope by CoroutineScope(SupervisorJob() + Dispatcher.WENEXT_THREAD_POOL) {
|
|
|
-
|
|
|
- companion object {
|
|
|
- private const val REDUCE_DATA_INTERVAL_MS = 5 * ONE_MINUTE //数据裁剪间隔
|
|
|
- private const val REPORT_INTERVAL_MS = 5 * ONE_SECOND //批量上报间隔
|
|
|
- private const val MAX_REPORT_COUNT = 10 //每次最大上报条数
|
|
|
- private const val MAX_CACHE_COUNT = 10000 //本地最大缓存条数
|
|
|
- }
|
|
|
-
|
|
|
- private val database =
|
|
|
- Room.databaseBuilder(config.ctx, WeNextStatDatabase::class.java, "wenext_stat_db.db")
|
|
|
- .build()
|
|
|
- private val statDao = database.statDao()
|
|
|
- private val batchReporting = AtomicBoolean(false)
|
|
|
- private val dataReducing = AtomicBoolean(false)
|
|
|
-
|
|
|
- init {
|
|
|
- scheduleReport()
|
|
|
- scheduleReduceData()
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 定时裁剪数据
|
|
|
- */
|
|
|
- private fun scheduleReduceData() {
|
|
|
- launch {
|
|
|
- while (true) {
|
|
|
- delay(REDUCE_DATA_INTERVAL_MS)
|
|
|
- reduceData()
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 数据裁剪
|
|
|
- */
|
|
|
- private suspend fun reduceData() {
|
|
|
- if (AppBase.debugLog) {
|
|
|
- AppBase.log.d(TAG_STAT_WENEXT, "reduceData, start")
|
|
|
- }
|
|
|
- val ids = try {
|
|
|
- statDao.getAllIds()
|
|
|
- } catch (e: Exception) {
|
|
|
- AppBase.log.e(TAG_STAT_WENEXT, "reduceData, getAllIds, e:$e")
|
|
|
- listOf()
|
|
|
- }
|
|
|
- if (ids.size <= MAX_CACHE_COUNT) {
|
|
|
- if (AppBase.debugLog) {
|
|
|
- AppBase.log.d(TAG_STAT_WENEXT, "reduceData, size:${ids.size} <= $MAX_CACHE_COUNT")
|
|
|
- }
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (!dataReducing.compareAndSet(false, true)) {
|
|
|
- AppBase.log.i(TAG_STAT_WENEXT, "reduceData, reducing")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- val toDelIds = ids.subList(0, ids.size - MAX_CACHE_COUNT)
|
|
|
- //分拆删除,否则会报too many SQL variables (code 1 SQLITE_ERROR)
|
|
|
- toDelIds.chunked(500).forEach { statDao.deleteByIds(it) }
|
|
|
- AppBase.log.i(TAG_STAT_WENEXT, "reduceData, toDelIds:$toDelIds")
|
|
|
- dataReducing.set(false)
|
|
|
- WeNextReportStatEvent(WeNextReportStatEvent.Action.REDUCE_DATA)
|
|
|
- .apply {
|
|
|
- amount to toDelIds.size
|
|
|
- }
|
|
|
- .send()
|
|
|
- }
|
|
|
-
|
|
|
- private suspend fun clearData() {
|
|
|
- if (!dataReducing.compareAndSet(false, true)) {
|
|
|
- AppBase.log.i(TAG_STAT_WENEXT, "clearData, reducing")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- statDao.deleteAll()
|
|
|
- AppBase.log.i(TAG_STAT_WENEXT, "clearData")
|
|
|
- dataReducing.set(false)
|
|
|
- WeNextReportStatEvent(WeNextReportStatEvent.Action.CLEAR_DATA)
|
|
|
- .send()
|
|
|
- }
|
|
|
-
|
|
|
- override fun report(eventId: String, events: MutableMap<String, Any>, immediate: Boolean) {
|
|
|
- if (!AppBase.isRelease) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- events[CommonEventKey.EVENT_ID] = eventId
|
|
|
- events[CommonEventKey.EVENT_TIME] = System.currentTimeMillis()
|
|
|
- if (immediate) {
|
|
|
- reportNow(events)
|
|
|
- } else {
|
|
|
- reportLater(events)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- override fun reportNow(events: Map<String, Any>) {
|
|
|
- launch {
|
|
|
- val data = toJsonErrorNull(events)
|
|
|
- if (data.isNullOrEmpty()) {
|
|
|
- AppBase.log.e(TAG_STAT_WENEXT, "reportNow, data is empty")
|
|
|
- return@launch
|
|
|
- }
|
|
|
-
|
|
|
- if (AppBase.debugLog) {
|
|
|
- AppBase.log.d(TAG_STAT_WENEXT, "reportNow, data:$data")
|
|
|
- }
|
|
|
- if (!sendToServer(data)) {
|
|
|
- try {
|
|
|
- statDao.insert(WeNextStatData(data = data))
|
|
|
- } catch (e: SQLiteFullException) {
|
|
|
- AppBase.log.e(TAG_STAT_WENEXT, "reportNow, e:$e")
|
|
|
- clearData()
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- override fun reportLater(events: Map<String, Any>) {
|
|
|
- launch {
|
|
|
- val data = toJsonErrorNull(events)
|
|
|
- if (data.isNullOrEmpty()) {
|
|
|
- AppBase.log.e(TAG_STAT_WENEXT, "reportLater, data is empty")
|
|
|
- return@launch
|
|
|
- }
|
|
|
-
|
|
|
- if (AppBase.debugLog) {
|
|
|
- AppBase.log.d(TAG_STAT_WENEXT, "reportLater, data:$data")
|
|
|
- }
|
|
|
- try {
|
|
|
- statDao.insert(WeNextStatData(data = data))
|
|
|
- } catch (e: SQLiteFullException) {
|
|
|
- AppBase.log.e(TAG_STAT_WENEXT, "reportLater, e:$e")
|
|
|
- clearData()
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private fun scheduleReport() {
|
|
|
- launch {
|
|
|
- while (true) {
|
|
|
- delay(REPORT_INTERVAL_MS)
|
|
|
- batchReport()
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private suspend fun batchReport() {
|
|
|
- if (AppBase.debugLog) {
|
|
|
- AppBase.log.d(TAG_STAT_WENEXT, "batchReport, start")
|
|
|
- }
|
|
|
-
|
|
|
- if (dataReducing.get()) {
|
|
|
- AppBase.log.i(TAG_STAT_WENEXT, "batchReport, data reducing")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (!batchReporting.compareAndSet(false, true)) {
|
|
|
- AppBase.log.i(TAG_STAT_WENEXT, "batchReport, reporting")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- val dataJsonList: List<WeNextStatData> =
|
|
|
- try {
|
|
|
- database.statDao().getEarliestRecords(MAX_REPORT_COUNT)
|
|
|
- } catch (e: Exception) {
|
|
|
- AppBase.log.e(TAG_STAT_WENEXT, "batchReport, getEarliestRecords, e:$e")
|
|
|
- listOf()
|
|
|
- }
|
|
|
- if (dataJsonList.isEmpty()) {
|
|
|
- batchReporting.set(false)
|
|
|
- if (AppBase.debugLog) {
|
|
|
- AppBase.log.d(TAG_STAT_WENEXT, "batchReport, dataJsonList is empty")
|
|
|
- }
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- val dataList = dataJsonList.mapNotNull { froJsonErrorNull<WeNextReportData>(it.data) }
|
|
|
- val data = toJsonErrorNull(dataList)
|
|
|
- if (data.isNullOrEmpty()) {
|
|
|
- batchReporting.set(false)
|
|
|
- AppBase.log.e(TAG_STAT_WENEXT, "batchReport, data is empty")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (sendToServer(data)) {
|
|
|
- val ids = dataJsonList.map { it.id }
|
|
|
- statDao.deleteByIds(ids)
|
|
|
- }
|
|
|
- batchReporting.set(false)
|
|
|
- }
|
|
|
-
|
|
|
- private fun getRequestBodySize(requestBody: RequestBody): Long {
|
|
|
- try {
|
|
|
- val buffer = Buffer()
|
|
|
- requestBody.writeTo(buffer)
|
|
|
- return buffer.size
|
|
|
- } catch (e: Exception) {
|
|
|
- return 0
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private fun sendToServer(data: String): Boolean {
|
|
|
- val startTime = SystemClock.elapsedRealtime()
|
|
|
- var response: Response? = null
|
|
|
- return try {
|
|
|
- val reqBody = data.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
|
|
- var bodySize = 0L
|
|
|
- if (AppBase.debugLog) {
|
|
|
- bodySize = getRequestBodySize(reqBody)
|
|
|
- }
|
|
|
- val request = Request.Builder()
|
|
|
- .url(config.reportUrl)
|
|
|
- .post(reqBody)
|
|
|
- .addHeader(CommonEventKey.APP, AppBase.appName)
|
|
|
- .build()
|
|
|
- response = config.statHtpClient.newCall(request).execute()
|
|
|
- if (response.isSuccessful) {
|
|
|
- if (AppBase.debugLog) {
|
|
|
- AppBase.log.d(TAG_STAT_WENEXT, "sendToServer, success, bodySize:$bodySize, data:$data")
|
|
|
- }
|
|
|
- true
|
|
|
- } else {
|
|
|
- AppBase.log.e(
|
|
|
- TAG_STAT_WENEXT,
|
|
|
- "sendToServer, failed, code:${response.code}"
|
|
|
- )
|
|
|
-// WeNextReportStatEvent(WeNextReportStatEvent.Action.SEND_SERVER)
|
|
|
-// .apply {
|
|
|
-// code to response.code
|
|
|
-// duration to (SystemClock.elapsedRealtime() - startTime)
|
|
|
-// }
|
|
|
-// .sendLater()
|
|
|
- false
|
|
|
- }
|
|
|
- } catch (e: Exception) {
|
|
|
- AppBase.log.e(TAG_STAT_WENEXT, "sendToServer, error:$e")
|
|
|
-// WeNextReportStatEvent(WeNextReportStatEvent.Action.SEND_SERVER)
|
|
|
-// .apply {
|
|
|
-// code to 1000
|
|
|
-// error to e.toString()
|
|
|
-// duration to (SystemClock.elapsedRealtime() - startTime)
|
|
|
-// }
|
|
|
-// .sendLater()
|
|
|
- false
|
|
|
- } finally {
|
|
|
- closeQuietly(response)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-}
|