? = null
+)
+
+data class Answer(
+ @SerializedName("name") val name: String? = null,
+ @SerializedName("type") val type: Int? = null,
+ @SerializedName("TTL") val ttl: Int? = null,
+ @SerializedName("data") val data: String? = null
+)
+
+object GoogleDnsApiHost {
+ const val API = "https://dns.google.com/"
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/BufferListener.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/BufferListener.kt
new file mode 100644
index 0000000..455b428
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/BufferListener.kt
@@ -0,0 +1,12 @@
+package guru.core.analytics.data.api.logging
+
+import okhttp3.Request
+import java.io.IOException
+
+/**
+ * @author ihsan on 8/12/18.
+ */
+interface BufferListener {
+ @Throws(IOException::class)
+ fun getJsonResponse(request: Request?): String?
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/I.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/I.kt
new file mode 100644
index 0000000..7346efb
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/I.kt
@@ -0,0 +1,36 @@
+package guru.core.analytics.data.api.logging
+
+import okhttp3.internal.platform.Platform.Companion.INFO
+import java.util.logging.Level
+import java.util.logging.Logger
+
+/**
+ * @author ihsan on 10/02/2017.
+ */
+internal open class I protected constructor() {
+ companion object {
+ private val prefix = arrayOf(". ", " .")
+ private var index = 0
+ fun log(type: Int, tag: String, msg: String?, isLogHackEnable: Boolean) {
+ val finalTag = getFinalTag(tag, isLogHackEnable)
+ val logger = Logger.getLogger(if (isLogHackEnable) finalTag else tag)
+ when (type) {
+ INFO -> logger.log(Level.INFO, msg)
+ else -> logger.log(Level.WARNING, msg)
+ }
+ }
+
+ private fun getFinalTag(tag: String, isLogHackEnable: Boolean): String {
+ return if (isLogHackEnable) {
+ index = index xor 1
+ prefix[index] + tag
+ } else {
+ tag
+ }
+ }
+ }
+
+ init {
+ throw UnsupportedOperationException()
+ }
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Level.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Level.kt
new file mode 100644
index 0000000..269b5a7
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Level.kt
@@ -0,0 +1,39 @@
+package guru.core.analytics.data.api.logging
+
+/**
+ * @author ihsan on 21/02/2017.
+ */
+enum class Level {
+ /**
+ * No logs.
+ */
+ NONE,
+ /**
+ *
+ * Example:
+ * `- URL
+ * - Method
+ * - Headers
+ * - Body
+ `
*
+ */
+ BASIC,
+ /**
+ *
+ * Example:
+ * `- URL
+ * - Method
+ * - Headers
+ `
*
+ */
+ HEADERS,
+ /**
+ *
+ * Example:
+ * `- URL
+ * - Method
+ * - Body
+ `
*
+ */
+ BODY
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Logger.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Logger.kt
new file mode 100644
index 0000000..e4d81fb
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Logger.kt
@@ -0,0 +1,19 @@
+package guru.core.analytics.data.api.logging
+
+import okhttp3.internal.platform.Platform
+import okhttp3.internal.platform.Platform.Companion.INFO
+
+/**
+ * @author ihsan on 11/07/2017.
+ */
+interface Logger {
+ fun log(level: Int = INFO, tag: String?= null, msg: String? = null)
+
+ companion object {
+ val DEFAULT: Logger = object : Logger {
+ override fun log(level: Int, tag: String?, msg: String?) {
+ Platform.get().log("$msg", level, null)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/LoggingInterceptor.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/LoggingInterceptor.kt
new file mode 100644
index 0000000..52d6c79
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/LoggingInterceptor.kt
@@ -0,0 +1,263 @@
+package guru.core.analytics.data.api.logging
+
+import okhttp3.*
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.ResponseBody.Companion.toResponseBody
+import okhttp3.internal.platform.Platform.Companion.INFO
+import java.util.*
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+
+
+/**
+ * @author ihsan on 09/02/2017.
+ */
+class LoggingInterceptor private constructor(private val builder: Builder) : Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = addQueryAndHeaders(chain.request())
+
+ if (builder.level == Level.NONE) {
+ return chain.proceed(request)
+ }
+
+ printlnRequestLog(request)
+
+ val startNs = System.nanoTime()
+ val response: Response
+ try {
+ response = proceedResponse(chain, request)
+ } catch (e: Exception) {
+ Printer.printFailed(builder.getTag(false), builder)
+ throw e
+ }
+ val receivedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
+
+ printlnResponseLog(receivedMs, response, request)
+ return response
+ }
+
+ private fun printlnResponseLog(receivedMs: Long, response: Response, request: Request) {
+ Printer.printJsonResponse(
+ builder,
+ receivedMs,
+ response.isSuccessful,
+ response.code,
+ response.headers,
+ response,
+ request.url.encodedPathSegments,
+ response.message,
+ request.url.toString()
+ )
+ }
+
+ private fun printlnRequestLog(request: Request) {
+ Printer.printJsonRequest(
+ builder,
+ request.body,
+ request.url.toUrl().toString(),
+ request.headers,
+ request.method
+ )
+ }
+
+ private fun proceedResponse(chain: Interceptor.Chain, request: Request): Response {
+ return if (builder.isMockEnabled && builder.listener != null) {
+ TimeUnit.MILLISECONDS.sleep(builder.sleepMs)
+ Response.Builder()
+ .body(builder.listener!!.getJsonResponse(request)?.toResponseBody("application/json".toMediaTypeOrNull()))
+ .request(chain.request())
+ .protocol(Protocol.HTTP_2)
+ .message("Mock data from LoggingInterceptor")
+ .code(200)
+ .build()
+ } else chain.proceed(request)
+ }
+
+ private fun addQueryAndHeaders(request: Request): Request {
+ val requestBuilder = request.newBuilder()
+ builder.headers.keys.forEach { key ->
+ builder.headers[key]?.let {
+ requestBuilder.addHeader(key, it)
+ }
+ }
+ val httpUrlBuilder: HttpUrl.Builder? = request.url.newBuilder(request.url.toString())
+ httpUrlBuilder?.let {
+ builder.httpUrl.keys.forEach { key ->
+ httpUrlBuilder.addQueryParameter(key, builder.httpUrl[key])
+ }
+ }
+ return requestBuilder.url(httpUrlBuilder?.build()!!).build()
+ }
+
+ @Suppress("unused")
+ class Builder {
+ val headers: HashMap = HashMap()
+ val httpUrl: HashMap = HashMap()
+ var isLogHackEnable = false
+ private set
+ var isDebugAble = false
+ var type: Int = INFO
+ private set
+ private var requestTag: String? = null
+ private var responseTag: String? = null
+ var level = Level.BASIC
+ private set
+ var logger: Logger? = null
+ private set
+ var isMockEnabled = false
+ var sleepMs: Long = 0
+ var listener: BufferListener? = null
+
+ /**
+ * @param level set log level
+ * @return Builder
+ * @see Level
+ */
+ fun setLevel(level: Level): Builder {
+ this.level = level
+ return this
+ }
+
+ fun getTag(isRequest: Boolean): String {
+ return when (isRequest) {
+ true -> if (requestTag.isNullOrEmpty()) TAG else requestTag!!
+ false -> if (responseTag.isNullOrEmpty()) TAG else responseTag!!
+ }
+ }
+
+ /**
+ * @param name Filed
+ * @param value Value
+ * @return Builder
+ * Add a field with the specified value
+ */
+ fun addHeader(name: String, value: String): Builder {
+ headers[name] = value
+ return this
+ }
+
+ /**
+ * @param name Filed
+ * @param value Value
+ * @return Builder
+ * Add a field with the specified value
+ */
+ fun addQueryParam(name: String, value: String): Builder {
+ httpUrl[name] = value
+ return this
+ }
+
+ /**
+ * Set request and response each log tag
+ *
+ * @param tag general log tag
+ * @return Builder
+ */
+ fun tag(tag: String): Builder {
+ TAG = tag
+ return this
+ }
+
+ /**
+ * Set request log tag
+ *
+ * @param tag request log tag
+ * @return Builder
+ */
+ fun request(tag: String?): Builder {
+ requestTag = tag
+ return this
+ }
+
+ /**
+ * Set response log tag
+ *
+ * @param tag response log tag
+ * @return Builder
+ */
+ fun response(tag: String?): Builder {
+ responseTag = tag
+ return this
+ }
+
+ /**
+ * @param isDebug set can sending log output
+ * @return Builder
+ */
+ @Deprecated(message = "Set level based on your requirement",
+ replaceWith = ReplaceWith(expression = "setLevel(Level.Basic)"),
+ level = DeprecationLevel.ERROR)
+ fun loggable(isDebug: Boolean): Builder {
+ this.isDebugAble = isDebug
+ return this
+ }
+
+ /**
+ * @param type set sending log output type
+ * @return Builder
+ * @see okhttp3.internal.platform.Platform
+ */
+ fun log(type: Int): Builder {
+ this.type = type
+ return this
+ }
+
+ /**
+ * @param logger manuel logging interface
+ * @return Builder
+ * @see Logger
+ */
+ fun logger(logger: Logger?): Builder {
+ this.logger = logger
+ return this
+ }
+
+ /**
+ * @param executor manual executor for printing
+ * @return Builder
+ * @see Logger
+ */
+ @Deprecated(message = "Create your own Logcat filter for best result", level = DeprecationLevel.ERROR)
+ fun executor(executor: Executor?): Builder {
+ TODO("Deprecated")
+ }
+
+ /**
+ * @param useMock let you use json file from asset
+ * @param sleep let you see progress dialog when you request
+ * @return Builder
+ * @see LoggingInterceptor
+ */
+ fun enableMock(useMock: Boolean, sleep: Long, listener: BufferListener?): Builder {
+ isMockEnabled = useMock
+ sleepMs = sleep
+ this.listener = listener
+ return this
+ }
+
+ /**
+ * Call this if you want to have formatted pretty output in Android Studio logCat.
+ * By default this 'hack' is not applied.
+ *
+ * @param useHack setup builder to use hack for Android Studio v3+ in order to have nice
+ * output as it was in previous A.S. versions.
+ * @return Builder
+ * @see Logger
+ */
+ @Deprecated(message = "Android studio has resolved problem for latest versions",
+ level = DeprecationLevel.WARNING)
+ fun enableAndroidStudioV3LogsHack(useHack: Boolean): Builder {
+ isLogHackEnable = useHack
+ return this
+ }
+
+ fun build(): LoggingInterceptor {
+ return LoggingInterceptor(this)
+ }
+
+ companion object {
+ private var TAG = "LoggingI"
+ }
+ }
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Printer.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Printer.kt
new file mode 100644
index 0000000..7104c49
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Printer.kt
@@ -0,0 +1,273 @@
+package guru.core.analytics.data.api.logging
+
+import okhttp3.Headers
+import okhttp3.RequestBody
+import okhttp3.Response
+import okhttp3.internal.http.promisesBody
+import okio.Buffer
+import okio.GzipSource
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.EOFException
+import java.io.IOException
+import java.nio.charset.Charset
+import java.nio.charset.StandardCharsets
+
+/**
+ * @author ihsan on 09/02/2017.
+ */
+class Printer private constructor() {
+ companion object {
+ private const val JSON_INDENT = 3
+ private val LINE_SEPARATOR = System.getProperty("line.separator")
+ private val DOUBLE_SEPARATOR = LINE_SEPARATOR + LINE_SEPARATOR
+ private const val N = "\n"
+ private const val T = "\t"
+ private const val REQUEST_UP_LINE = "┌────── Request ────────────────────────────────────────────────────────────────────────"
+ private const val END_LINE = "└───────────────────────────────────────────────────────────────────────────────────────"
+ private const val RESPONSE_UP_LINE = "┌────── Response ───────────────────────────────────────────────────────────────────────"
+ private const val BODY_TAG = "Body:"
+ private const val URL_TAG = "URL: "
+ private const val METHOD_TAG = "Method: @"
+ private const val HEADERS_TAG = "Headers:"
+ private const val STATUS_CODE_TAG = "Status Code: "
+ private const val RECEIVED_TAG = "Received in: "
+ private const val DEFAULT_LINE = "│ "
+ private val OOM_OMITTED = LINE_SEPARATOR + "Output omitted because of Object size."
+ private fun isEmpty(line: String): Boolean {
+ return line.isEmpty() || N == line || T == line || line.trim { it <= ' ' }.isEmpty()
+ }
+
+ fun printJsonRequest(builder: LoggingInterceptor.Builder, body: RequestBody?, url: String, header: Headers, method: String) {
+ val requestBody = body?.let {
+ LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + bodyToString(body, header)
+ } ?: ""
+ val tag = builder.getTag(true)
+ if (builder.logger == null) I.log(builder.type, tag, REQUEST_UP_LINE, builder.isLogHackEnable)
+ logLines(builder.type, tag, arrayOf(URL_TAG + url), builder.logger, false, builder.isLogHackEnable)
+ logLines(builder.type, tag, getRequest(builder.level, header, method), builder.logger, true, builder.isLogHackEnable)
+ if (builder.level == Level.BASIC || builder.level == Level.BODY) {
+ logLines(builder.type, tag, requestBody.split(LINE_SEPARATOR).toTypedArray(), builder.logger, true, builder.isLogHackEnable)
+ }
+ if (builder.logger == null) I.log(builder.type, tag, END_LINE, builder.isLogHackEnable)
+ }
+
+ fun printJsonResponse(builder: LoggingInterceptor.Builder, chainMs: Long, isSuccessful: Boolean,
+ code: Int, headers: Headers, response: Response, segments: List, message: String, responseUrl: String) {
+ val responseBody = LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + getResponseBody(response)
+ val tag = builder.getTag(false)
+ val urlLine = arrayOf(URL_TAG + responseUrl, N)
+ val responseString = getResponse(headers, chainMs, code, isSuccessful,
+ builder.level, segments, message)
+ if (builder.logger == null) {
+ I.log(builder.type, tag, RESPONSE_UP_LINE, builder.isLogHackEnable)
+ }
+ logLines(builder.type, tag, urlLine, builder.logger, true, builder.isLogHackEnable)
+ logLines(builder.type, tag, responseString, builder.logger, true, builder.isLogHackEnable)
+ if (builder.level == Level.BASIC || builder.level == Level.BODY) {
+ logLines(builder.type, tag, responseBody.split(LINE_SEPARATOR).toTypedArray(), builder.logger,
+ true, builder.isLogHackEnable)
+ }
+ if (builder.logger == null) {
+ I.log(builder.type, tag, END_LINE, builder.isLogHackEnable)
+ }
+ }
+
+ private fun getResponseBody(response: Response): String {
+ val responseBody = response.body!!
+ val headers = response.headers
+ val contentLength = responseBody.contentLength()
+ if (!response.promisesBody()) {
+ return "End request - Promises Body"
+ } else if (bodyHasUnknownEncoding(response.headers)) {
+ return "encoded body omitted"
+ } else {
+ val source = responseBody.source()
+ source.request(Long.MAX_VALUE) // Buffer the entire body.
+ var buffer = source.buffer
+
+ var gzippedLength: Long? = null
+ if ("gzip".equals(headers["Content-Encoding"], ignoreCase = true)) {
+ gzippedLength = buffer.size
+ GzipSource(buffer.clone()).use { gzippedResponseBody ->
+ buffer = Buffer()
+ buffer.writeAll(gzippedResponseBody)
+ }
+ }
+
+ val contentType = responseBody.contentType()
+ val charset: Charset = contentType?.charset(StandardCharsets.UTF_8)
+ ?: StandardCharsets.UTF_8
+
+ if (!buffer.isProbablyUtf8()) {
+ return "End request - binary ${buffer.size}:byte body omitted"
+ }
+
+ if (contentLength != 0L) {
+ return getJsonString(buffer.clone().readString(charset))
+ }
+
+ return if (gzippedLength != null) {
+ "End request - ${buffer.size}:byte, $gzippedLength-gzipped-byte body"
+ } else {
+ "End request - ${buffer.size}:byte body"
+ }
+ }
+ }
+
+ private fun getRequest(level: Level, headers: Headers, method: String): Array {
+ val log: String
+ val loggableHeader = level == Level.HEADERS || level == Level.BASIC
+ log = METHOD_TAG + method + DOUBLE_SEPARATOR +
+ if (isEmpty("$headers")) "" else if (loggableHeader) HEADERS_TAG + LINE_SEPARATOR + dotHeaders(headers) else ""
+ return log.split(LINE_SEPARATOR).toTypedArray()
+ }
+
+ private fun getResponse(headers: Headers, tookMs: Long, code: Int, isSuccessful: Boolean,
+ level: Level, segments: List, message: String): Array {
+ val log: String
+ val loggableHeader = level == Level.HEADERS || level == Level.BASIC
+ val segmentString = slashSegments(segments)
+ log = ((if (segmentString.isNotEmpty()) "$segmentString - " else "") + "[is success : "
+ + isSuccessful + "] - " + RECEIVED_TAG + tookMs + "ms" + DOUBLE_SEPARATOR + STATUS_CODE_TAG +
+ code + " / " + message + DOUBLE_SEPARATOR + when {
+ isEmpty("$headers") -> ""
+ loggableHeader -> HEADERS_TAG + LINE_SEPARATOR +
+ dotHeaders(headers)
+ else -> ""
+ })
+ return log.split(LINE_SEPARATOR).toTypedArray()
+ }
+
+ private fun slashSegments(segments: List): String {
+ val segmentString = StringBuilder()
+ for (segment in segments) {
+ segmentString.append("/").append(segment)
+ }
+ return segmentString.toString()
+ }
+
+ private fun dotHeaders(headers: Headers): String {
+ val builder = StringBuilder()
+ headers.forEach { pair ->
+ builder.append("${pair.first}: ${pair.second}").append(N)
+ }
+ return builder.dropLast(1).toString()
+ }
+
+ private fun logLines(type: Int, tag: String, lines: Array, logger: Logger?,
+ withLineSize: Boolean, useLogHack: Boolean) {
+ for (line in lines) {
+ val lineLength = line.length
+ val maxLogSize = if (withLineSize) 110 else lineLength
+ for (i in 0..lineLength / maxLogSize) {
+ val start = i * maxLogSize
+ var end = (i + 1) * maxLogSize
+ end = if (end > line.length) line.length else end
+ if (logger == null) {
+ I.log(type, tag, DEFAULT_LINE + line.substring(start, end), useLogHack)
+ } else {
+ logger.log(type, tag, line.substring(start, end))
+ }
+ }
+ }
+ }
+
+ private fun bodyToString(requestBody: RequestBody?, headers: Headers): String {
+ return requestBody?.let {
+ return try {
+ when {
+ bodyHasUnknownEncoding(headers) -> {
+ return "encoded body omitted)"
+ }
+ requestBody.isDuplex() -> {
+ return "duplex request body omitted"
+ }
+ requestBody.isOneShot() -> {
+ return "one-shot body omitted"
+ }
+ else -> {
+ val buffer = Buffer()
+ requestBody.writeTo(buffer)
+
+ val contentType = requestBody.contentType()
+ val charset: Charset = contentType?.charset(StandardCharsets.UTF_8)
+ ?: StandardCharsets.UTF_8
+
+ return if (buffer.isProbablyUtf8()) {
+ getJsonString(buffer.readString(charset)) + LINE_SEPARATOR + "${requestBody.contentLength()}-byte body"
+ } else {
+ "binary ${requestBody.contentLength()}-byte body omitted"
+ }
+ }
+ }
+ } catch (e: IOException) {
+ "{\"err\": \"" + e.message + "\"}"
+ }
+ } ?: ""
+ }
+
+ private fun bodyHasUnknownEncoding(headers: Headers): Boolean {
+ val contentEncoding = headers["Content-Encoding"] ?: return false
+ return !contentEncoding.equals("identity", ignoreCase = true) &&
+ !contentEncoding.equals("gzip", ignoreCase = true)
+ }
+
+ private fun getJsonString(msg: String): String {
+ val message: String = try {
+ when {
+ msg.startsWith("{") -> {
+ val jsonObject = JSONObject(msg)
+ jsonObject.toString(JSON_INDENT)
+ }
+ msg.startsWith("[") -> {
+ val jsonArray = JSONArray(msg)
+ jsonArray.toString(JSON_INDENT)
+ }
+ else -> {
+ msg
+ }
+ }
+ } catch (e: JSONException) {
+ msg
+ } catch (e1: OutOfMemoryError) {
+ OOM_OMITTED
+ }
+ return message
+ }
+
+ fun printFailed(tag: String, builder: LoggingInterceptor.Builder) {
+ I.log(builder.type, tag, RESPONSE_UP_LINE, builder.isLogHackEnable)
+ I.log(builder.type, tag, DEFAULT_LINE + "Response failed", builder.isLogHackEnable)
+ I.log(builder.type, tag, END_LINE, builder.isLogHackEnable)
+ }
+ }
+
+ init {
+ throw UnsupportedOperationException()
+ }
+}
+
+/**
+ * @see 'https://github.com/square/okhttp/blob/master/okhttp-logging-interceptor/src/main/java/okhttp3/logging/utf8.kt'
+ * */
+internal fun Buffer.isProbablyUtf8(): Boolean {
+ try {
+ val prefix = Buffer()
+ val byteCount = size.coerceAtMost(64)
+ copyTo(prefix, 0, byteCount)
+ for (i in 0 until 16) {
+ if (prefix.exhausted()) {
+ break
+ }
+ val codePoint = prefix.readUtf8CodePoint()
+ if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
+ return false
+ }
+ }
+ return true
+ } catch (_: EOFException) {
+ return false // Truncated UTF-8 sequence.
+ }
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/GuruAnalyticsDatabase.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/GuruAnalyticsDatabase.kt
new file mode 100644
index 0000000..f93f51e
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/GuruAnalyticsDatabase.kt
@@ -0,0 +1,77 @@
+package guru.core.analytics.data.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import androidx.sqlite.db.SupportSQLiteDatabase
+import guru.core.analytics.data.db.dao.EventDao
+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
+
+@Database(
+ entities = [
+ EventEntity::class,
+ ],
+ version = 1,
+ exportSchema = false,
+)
+@TypeConverters(Converters::class)
+abstract class GuruAnalyticsDatabase : RoomDatabase() {
+ abstract fun eventDao(): EventDao
+
+ companion object {
+
+ private lateinit var appContext: SoftReference
+
+ private val context
+ get() = appContext.get()!!
+
+ private const val dbName = "guru_analytics"
+
+ @Volatile
+ private var INSTANCE: GuruAnalyticsDatabase? = null
+
+ @Synchronized
+ fun initialize(context: Context) {
+ if (INSTANCE == null) {
+ appContext = SoftReference(context.applicationContext)
+ INSTANCE = newInstance()
+ }
+ }
+
+ fun getInstance(): GuruAnalyticsDatabase = INSTANCE!!
+
+ private fun newInstance(): GuruAnalyticsDatabase {
+ return Room.databaseBuilder(context, GuruAnalyticsDatabase::class.java, dbName)
+ .addMigrations(*MIGRATIONS)
+ .addCallback(object : RoomDatabase.Callback() {
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ Timber.d("database onCreate")
+ super.onCreate(db)
+ }
+
+ override fun onOpen(db: SupportSQLiteDatabase) {
+ Timber.d("database onOpen")
+ super.onOpen(db)
+ }
+ })
+ .build()
+ }
+
+ fun runInTransaction(callback: () -> TransactionResult, defVal: T?): Maybe {
+ return INSTANCE?.runInTransactionEx(callback, defVal) ?: Maybe.empty()
+ }
+
+ fun runInTransaction(callback: () -> TransactionResult): Maybe {
+ return INSTANCE?.runInTransactionEx(callback) ?: Maybe.empty()
+ }
+ }
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/dao/EventDao.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/dao/EventDao.kt
new file mode 100644
index 0000000..d521685
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/dao/EventDao.kt
@@ -0,0 +1,91 @@
+package guru.core.analytics.data.db.dao
+
+import androidx.lifecycle.LiveData
+import androidx.room.*
+import guru.core.analytics.data.db.model.EventEntity
+import guru.core.analytics.data.store.EventInfoStore
+
+@Dao
+abstract class EventDao {
+
+ companion object {
+ private const val TAG = "EventDao"
+ }
+
+ fun updateEventUploading(events: List) {
+ val ids = getIds(events)
+ updateEventState(1, EventInfoStore.SESSION, ids)
+ }
+
+ fun updateEventDefault(events: List) {
+ val ids = getIds(events)
+ updateEventState(0, EventInfoStore.SESSION, ids)
+ }
+
+ private fun getIds(events: List) = events.map { it.id }.toTypedArray()
+
+ @Transaction
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ abstract fun addEvent(event: EventEntity)
+
+ @Transaction
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ abstract fun addEvents(events: List)
+
+ @Query("SELECT * FROM Event WHERE status = 0 ORDER BY priority ASC, at ASC LIMIT :limit")
+ abstract fun getEvents(limit: Int): List
+
+ @Query("SELECT * FROM Event WHERE status = 0 ORDER BY priority ASC, at ASC")
+ abstract fun getAllEvents(): List
+
+ @Query("UPDATE Event SET status = :status, session = :session WHERE id in (:keys)")
+ abstract fun updateEventState(status: Int, session: String, keys: Array)
+
+ @Transaction
+ open fun loadAndMarkAllUploadEvents(): List {
+ val events = getAllEvents()
+ updateEventUploading(events)
+ return events
+ }
+
+ @Transaction
+ open fun loadAndMarkUploadEvents(limit: Int): List {
+ 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
+ }
+
+ @Transaction
+ open fun deleteExpiredEvents(timestamp: Long): Pair {
+ val deletedEventsCount = deleteEvents(timestamp)
+ val resetEventsCount = resetEventStateExceptSession(EventInfoStore.SESSION)
+ return deletedEventsCount to resetEventsCount
+ }
+
+ @Query("UPDATE Event SET status = :status")
+ abstract fun updateEventState(status: Int): Int
+
+ @Query("UPDATE Event SET status = 0 WHERE session != :exceptSession")
+ abstract fun resetEventStateExceptSession(exceptSession: String): Int
+
+ @Transaction
+ @Delete
+ abstract fun deleteEvents(events: List)
+
+ @Query("DELETE FROM Event WHERE at < :timestamp")
+ abstract fun deleteEvents(timestamp: Long): Int
+
+ @Query("SELECT count(id) FROM Event WHERE status = 0")
+ abstract fun getEventCount(): LiveData
+
+ @Transaction
+ @Query("DELETE FROM Event WHERE (SELECT count(id) FROM Event) > :max AND at IN (SELECT at FROM Event ORDER BY at DESC LIMIT(SELECT count(id) FROM Event) OFFSET :max)")
+ abstract fun deleteExceedEvents(max: Int)
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/migrations/Migrations.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/migrations/Migrations.kt
new file mode 100644
index 0000000..af9f76f
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/migrations/Migrations.kt
@@ -0,0 +1,16 @@
+package guru.core.analytics.data.db.migrations
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import timber.log.Timber
+
+/**
+ * Created by Haoyi on 2022-11-05.
+ */
+private val MIGRATION_1_TO_2: Migration = object : Migration(1, 2) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ Timber.d("migrate 1 to 2")
+ }
+}
+
+val MIGRATIONS = emptyArray()
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/Event.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/Event.kt
new file mode 100644
index 0000000..6971f65
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/Event.kt
@@ -0,0 +1,41 @@
+package guru.core.analytics.data.db.model
+
+import androidx.annotation.IntDef
+import androidx.annotation.Keep
+import com.google.gson.annotations.SerializedName
+
+
+@Keep
+data class Event(
+ @SerializedName("timestamp") val timestamp: Long, // 客户端中记录此事件的时间(采用世界协调时间,毫秒为单位)
+ @SerializedName("event") val event: String, // 事件名称
+ @SerializedName("info") val info: Map?, // 包含deviceId / uid / adjustId / adId / firebaseId 等
+ @SerializedName("param") val param: Map?, // 事件参数
+ @SerializedName("properties") val properties: Map?, // 用户属性信息
+ @SerializedName("eventId") val eventId: String,
+)
+
+@Keep
+data class ParamValue(
+ @SerializedName("s") val s: String? = null, // 事件参数的字符串值
+ @SerializedName("i") val i: Long? = null, // 事件参数的整数值
+ @SerializedName("d") val d: Double? = null, // 事件参数的小数值。注意:APP序列化成JSON时,注意不要序列化成科学计数法
+)
+
+@IntDef(EventPriority.EMERGENCE, EventPriority.HIGH, EventPriority.DEFAULT, EventPriority.LOW)
+@Retention(AnnotationRetention.BINARY)
+@Target(
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.VALUE_PARAMETER,
+ AnnotationTarget.FIELD,
+ AnnotationTarget.LOCAL_VARIABLE,
+ AnnotationTarget.CLASS
+)
+annotation class EventPriority {
+ companion object {
+ const val EMERGENCE = 0
+ const val HIGH = 5
+ const val DEFAULT = 10
+ const val LOW = 15
+ }
+}
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventEntity.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventEntity.kt
new file mode 100644
index 0000000..b69f90a
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventEntity.kt
@@ -0,0 +1,37 @@
+package guru.core.analytics.data.db.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "Event")
+data class EventEntity(
+ @PrimaryKey
+ @ColumnInfo(name = "id")
+ val id: String,
+
+ @ColumnInfo(name = "session")
+ val session: String,
+
+ @ColumnInfo(name = "json")
+ var json: String,
+
+ @ColumnInfo(name = "ext")
+ val ext: String,
+
+ @ColumnInfo(name = "priority", defaultValue = "${EventPriority.DEFAULT}")
+ val priority: Int,
+
+ @ColumnInfo(name = "status")
+ val status: Int,
+
+ @ColumnInfo(name = "at")
+ val at: Long,
+
+ @ColumnInfo(name = "event")
+ val event: String,
+
+ @ColumnInfo(name = "version")
+ val version: Int,
+
+ )
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventStatistic.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventStatistic.kt
new file mode 100644
index 0000000..058460e
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventStatistic.kt
@@ -0,0 +1,7 @@
+package guru.core.analytics.data.db.model
+
+data class EventStatistic(
+ val eventCountAll: Int = 0,
+ val eventCountDeleted: Int = 0,
+ val eventCountUploaded: Int = 0,
+)
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Converters.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Converters.kt
new file mode 100644
index 0000000..d97e917
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Converters.kt
@@ -0,0 +1,19 @@
+package guru.core.analytics.data.db.utils
+
+import androidx.room.TypeConverter
+import java.util.*
+
+/**
+ * Created by Haoyi on 2019/1/11.
+ */
+class Converters {
+ @TypeConverter
+ fun fromTimestamp(value: Long?): Date? {
+ return value?.let { Date(it) }
+ }
+
+ @TypeConverter
+ fun dateToTimestamp(date: Date?): Long? {
+ return date?.let { it.time }
+ }
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/DatabaseException.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/DatabaseException.kt
new file mode 100644
index 0000000..b313d30
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/DatabaseException.kt
@@ -0,0 +1,7 @@
+package guru.core.analytics.data.db.utils
+
+/**
+ * Created by Haoyi on 2022-11-05.
+ */
+class DatabaseException(message: String, cause: Throwable? = null) : Exception(message, cause) {
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Transactions.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Transactions.kt
new file mode 100644
index 0000000..857b026
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Transactions.kt
@@ -0,0 +1,60 @@
+package guru.core.analytics.data.db.utils
+
+import androidx.room.RoomDatabase
+import io.reactivex.Maybe
+
+/**
+ * Created by Haoyi on 2022-11-05.
+ */
+enum class ResultBehavior {
+ SUCCESS, IGNORE, ERROR
+}
+
+data class TransactionResult(
+ val value: T?,
+ val behavior: ResultBehavior,
+ val cause: Throwable? = null
+) {
+ companion object {
+ fun obtainIgnoreResult(): TransactionResult {
+ return TransactionResult(null, ResultBehavior.IGNORE)
+ }
+
+ fun obtainSuccessResult(value: R? = null): TransactionResult {
+ return TransactionResult(value, ResultBehavior.SUCCESS)
+ }
+
+ fun obtainErrorResult(throwable: Throwable? = null): TransactionResult {
+ return TransactionResult(null, ResultBehavior.ERROR, throwable)
+ }
+ }
+}
+
+fun RoomDatabase.runInTransactionEx(
+ callback: () -> TransactionResult,
+ defVal: T?
+): Maybe {
+ 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 RoomDatabase.runInTransactionEx(callback: () -> TransactionResult): Maybe {
+ return runInTransactionEx(callback, null)
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/Clearable.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/Clearable.kt
new file mode 100644
index 0000000..51ae49e
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/Clearable.kt
@@ -0,0 +1,11 @@
+package guru.core.analytics.data.local
+
+import kotlin.reflect.KProperty
+
+/**
+ * Created by Haoyi on 2017/12/20.
+ */
+interface Clearable {
+ fun clear(thisRef: PreferenceHolder, property: KProperty<*>)
+ fun clearCache()
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceCache.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceCache.kt
new file mode 100644
index 0000000..4b8710c
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceCache.kt
@@ -0,0 +1,10 @@
+package guru.core.analytics.data.local
+
+import android.util.LruCache
+
+/**
+ * Created by Haoyi on 2018/6/5.
+ */
+object PreferenceCache : LruCache(32) {
+ override fun sizeOf(key: String?, value: Any?): Int = 1
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceFieldDelegate.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceFieldDelegate.kt
new file mode 100644
index 0000000..49625b4
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceFieldDelegate.kt
@@ -0,0 +1,91 @@
+package guru.core.analytics.data.local
+
+import android.annotation.SuppressLint
+import android.content.SharedPreferences
+import android.util.Log
+import guru.core.analytics.data.local.PreferenceHolder.Companion.CACHE
+import guru.core.analytics.data.local.PreferenceHolder.Companion.SCHEDULER
+import timber.log.Timber
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty
+
+/**
+ * Created by Haoyi on 2017/12/13.
+ */
+internal class PreferenceFieldDelegate(
+ private val clazz: KClass,
+ private val key: String,
+ private val default: () -> T?
+) : ReadWriteProperty, Clearable {
+ override fun getValue(thisRef: PreferenceHolder, property: KProperty<*>): T? =
+ readValue(thisRef, property).apply { field = this }
+
+ override fun setValue(thisRef: PreferenceHolder, property: KProperty<*>, value: T?) {
+ field = value
+ saveNewValue(thisRef, property, value)
+ }
+
+ override fun clear(thisRef: PreferenceHolder, property: KProperty<*>) {
+ setValue(thisRef, property, null)
+ }
+
+ override fun clearCache() {
+ field = null
+ }
+
+ var field: T? = null
+
+ private fun saveNewValue(thisRef: PreferenceHolder, property: KProperty<*>, value: T?) {
+ if (value != null) CACHE.put(key, value) else CACHE.remove(key)
+ thisRef.getSharedPreferences()
+ .observeOn(SCHEDULER)
+ .subscribe(
+ {
+ if (value == null) {
+ removeValue(thisRef)
+ } else {
+ it.edit().apply { putValue(clazz, value, key) }.apply()
+ }
+ }, {
+ it.printStackTrace()
+ Log.d("Preference", "====> zhy saveNewValue error!", it)
+ }
+ )
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun readValue(thisRef: PreferenceHolder, property: KProperty<*>): T? {
+ val result = CACHE.get(key)
+ return try {
+ when {
+ clazz.isInstance(result) -> result as T
+ else -> thisRef.getSharedPreferencesDirectly().getValue(property).value
+ }
+ } catch (err: Throwable) {
+ default()
+ }
+ }
+
+ private inner class Result(val value: T?)
+
+ private fun SharedPreferences.getValue(property: KProperty<*>): Result {
+ val value = if (contains(key)) {
+ getFromPreference(clazz, default(), key)
+ } else {
+ default()
+ }
+ return Result(value)
+ }
+
+ @SuppressLint("ApplySharedPref")
+ private fun removeValue(thisRef: PreferenceHolder) {
+ CACHE.remove(key)
+ thisRef.getSharedPreferences()
+ .observeOn(SCHEDULER)
+ .subscribe(
+ { preference -> preference.edit().remove(key).commit() },
+ { err -> Timber.d(err, "removeValue error!") }
+ )
+ }
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceHolder.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceHolder.kt
new file mode 100644
index 0000000..d0bb804
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceHolder.kt
@@ -0,0 +1,85 @@
+package guru.core.analytics.data.local
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
+import android.util.LruCache
+import io.reactivex.Single
+import io.reactivex.schedulers.Schedulers
+import timber.log.Timber
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.ThreadPoolExecutor
+import java.util.concurrent.TimeUnit
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KClass
+
+/**
+ * Created by Haoyi on 2017/12/13.
+ */
+abstract class PreferenceHolder(val context: Context,
+ val name: String) {
+
+ val preferences = context.getSharedPreferences(name, Context.MODE_PRIVATE)
+
+ companion object {
+ @JvmStatic
+ val EXECUTOR = ThreadPoolExecutor(0, 1,
+ 60, TimeUnit.SECONDS, LinkedBlockingQueue())
+ @JvmStatic
+ val SCHEDULER = Schedulers.from(EXECUTOR)
+ @JvmStatic
+ val CACHE: LruCache = PreferenceCache
+ }
+
+ protected inline fun bind(key: String, defaultValue: T?): ReadWriteProperty = bind(T::class, key, { defaultValue })
+
+ protected inline fun bind(key: String, noinline default: () -> T?): ReadWriteProperty = bind(T::class, key, default)
+
+ protected fun bind(clazz: KClass, key: String, default: () -> T?): ReadWriteProperty = PreferenceFieldDelegate(clazz, key, default)
+
+ fun getSharedPreferences(): Single = Single.just(preferences)
+
+ fun getSharedPreferencesDirectly(): SharedPreferences = preferences
+
+ inline fun set(key: String, value: T?) {
+ if (value != null) CACHE.put(key, value) else CACHE.remove(key)
+ val clazz = T::class
+ getSharedPreferences()
+ .observeOn(SCHEDULER)
+ .subscribe({
+ if (value == null) {
+ removeValue(key)
+ } else {
+ it.edit().apply { putValue(clazz, value, key) }.apply()
+ }
+ }, {
+ it.printStackTrace()
+ Log.d("Preference", "====> zhy saveNewValue error!", it)
+ })
+ }
+
+ inline fun get(key: String, value: T? = null): T? {
+ val result = CACHE.get(key)
+ val clazz = T::class
+ return try {
+ when {
+ clazz.isInstance(result) -> result as T
+ else -> preferences.getFromPreference(clazz, value, key)
+ }
+ } catch (err: Throwable) {
+ value
+ }
+ }
+
+ @SuppressLint("ApplySharedPref")
+ fun removeValue(key: String) {
+ CACHE.remove(key)
+ getSharedPreferences()
+ .observeOn(SCHEDULER)
+ .subscribe(
+ { preference -> preference.edit().remove(key).commit() },
+ { err -> Timber.d(err, "removeValue error!") }
+ )
+ }
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferencesManager.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferencesManager.kt
new file mode 100644
index 0000000..e3f7ddd
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferencesManager.kt
@@ -0,0 +1,51 @@
+package guru.core.analytics.data.local
+
+import android.annotation.SuppressLint
+import android.content.Context
+
+class PreferencesManager private constructor(
+ context: Context,
+) : PreferenceHolder(context, NAME) {
+
+ companion object {
+ private const val NAME = "guru_analytics"
+ const val KEY_TOTAL_DURATION_FG_EVENT = "total_duration_fg_event"
+
+ @SuppressLint("StaticFieldLeak")
+ @Volatile
+ private var INSTANCE: PreferencesManager? = null
+
+ fun getInstance(context: Context): PreferencesManager =
+ INSTANCE ?: synchronized(this) {
+ INSTANCE ?: PreferencesManager(context.applicationContext).also { INSTANCE = it }
+ }
+ }
+
+ fun getTotalDurationFgEvent(): Long {
+ return try {
+ getLongDirectly(KEY_TOTAL_DURATION_FG_EVENT, 0L)
+ } catch (e: Throwable) {
+ 0L
+ }
+ }
+
+ fun setTotalDurationFgEvent(value: Long) {
+ setLongDirectly(KEY_TOTAL_DURATION_FG_EVENT, value)
+ }
+
+ private fun getLongDirectly(key: String, defValue: Long = 0L): Long {
+ return getSharedPreferencesDirectly().getLong(key, defValue)
+ }
+
+ private fun setLongDirectly(key: String, value: Long) {
+ getSharedPreferencesDirectly().edit().putLong(key, value).commit()
+ }
+
+ var isFirstOpen: Boolean? by bind("is_first_open", true)
+
+ var eventCountAll: Int? by bind("event_count_all", 0)
+ var eventCountDeleted: Int? by bind("event_count_deleted", 0)
+ var eventCountUploaded: Int? by bind("event_count_uploaded", 0)
+ var uploadEventBaseUrl: String? by bind("update_event_base_url", "")
+ var totalDurationFgEvent: Long? by bind("total_duration_fg_event", 0L)
+}
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PutValue.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PutValue.kt
new file mode 100644
index 0000000..b94de30
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PutValue.kt
@@ -0,0 +1,38 @@
+@file:Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
+
+package guru.core.analytics.data.local
+
+import android.content.SharedPreferences
+import kotlin.reflect.KClass
+
+/**
+ * Created by Haoyi on 2017/12/13.
+ */
+
+fun SharedPreferences.Editor.putValue(clazz: KClass<*>, value: Any, key: String) {
+ when (clazz.simpleName) {
+ "Long" -> putLong(key, value as Long)
+ "Int" -> putInt(key, value as Int)
+ "String" -> putString(key, value as String?)
+ "Boolean" -> putBoolean(key, value as Boolean)
+ "Float" -> putFloat(key, value as Float)
+ else -> throw Error("Not found type!")
+ }
+}
+
+fun SharedPreferences.getFromPreference(clazz: KClass, default: T?, key: String): T = when (clazz.simpleName) {
+ "Long" -> getLong(key, (default ?: -1L) as Long) as T
+ "Int" -> getInt(key, (default ?: -1) as Int) as T
+ "String" -> getString(key, (default ?: "") as String) as T
+ "Boolean" -> getBoolean(key, (default ?: false) as Boolean) as T
+ "Float" -> getFloat(key, (default ?: -1.0) as Float) as T
+ else -> throw Error("Not found type!")
+}
+
+private fun getDefault(clazz: KClass): T? = when(clazz.simpleName) {
+ "Long" -> -1L
+ "Int" -> -1
+ "Boolean" -> false
+ "Float" -> -1.0F
+ else -> null
+} as? T
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsInfo.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsInfo.kt
new file mode 100644
index 0000000..fdfe00a
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsInfo.kt
@@ -0,0 +1,17 @@
+package guru.core.analytics.data.model
+
+internal data class AnalyticsInfo(
+ var debug: Boolean = false,
+ var batchLimit: Int? = null,
+ var eventExpiredInDays: Int? = 7,
+ var persistableLog: Boolean = true,
+ var uploadPeriodInSeconds: Long? = null,
+ var startUploadDelayInSecond: Long? = 0L,
+ var eventHandlerCallback: ((Int, String?) -> Unit)? = null,
+ var isInitPeriodicWork: Boolean = true,
+ var uploadEventBaseUrl: String? = null,
+ var fgEventPeriodInSeconds: Long? = null,
+ var xAppId: String? = null,
+ var xDeviceInfo: String? = null,
+ var mainProcess: String? = null,
+)
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsOptions.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsOptions.kt
new file mode 100644
index 0000000..50002d0
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsOptions.kt
@@ -0,0 +1,5 @@
+package guru.core.analytics.data.model
+
+import guru.core.analytics.data.db.model.EventPriority
+
+data class AnalyticsOptions(@EventPriority val priority: Int = EventPriority.DEFAULT)
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/Constants.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/Constants.kt
new file mode 100644
index 0000000..365e936
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/Constants.kt
@@ -0,0 +1,9 @@
+package guru.core.analytics.data.model
+
+import androidx.annotation.IntDef
+import androidx.annotation.StringDef
+
+object GuruDetails {
+ val version: Int = 1
+}
+
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/EventItem.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/EventItem.kt
new file mode 100644
index 0000000..0ed9532
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/EventItem.kt
@@ -0,0 +1,13 @@
+package guru.core.analytics.data.model
+
+import androidx.annotation.Keep
+import com.google.gson.annotations.SerializedName
+
+@Keep
+data class EventItem(
+ @SerializedName("event_name") val eventName: String,
+ @SerializedName("item_category") val itemCategory: String? = null,
+ @SerializedName("item_name") val itemName: String? = null,
+ @SerializedName("value") val value: Number? = null,
+ @SerializedName("params") val params: Map? = null,
+)
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/exceptions/ArgumentException.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/exceptions/ArgumentException.kt
new file mode 100644
index 0000000..dd1f7bc
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/exceptions/ArgumentException.kt
@@ -0,0 +1,4 @@
+package guru.core.analytics.data.model.exceptions
+
+class ArgumentException {
+}
\ No newline at end of file
diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/DeviceInfoStore.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/DeviceInfoStore.kt
new file mode 100644
index 0000000..ea3faf0
--- /dev/null
+++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/DeviceInfoStore.kt
@@ -0,0 +1,43 @@
+package guru.core.analytics.data.store
+
+import android.content.Context
+import android.os.Build
+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 java.util.*
+
+
+object DeviceInfoStore {
+
+ private val deviceInfoSubject: BehaviorSubject