TP-12345 | Added Alfred Updated Module

This commit is contained in:
snehabanka
2023-04-25 13:20:34 +05:30
committed by AMAN SINGH
parent 70913daa53
commit 141da9f70d
38 changed files with 2623 additions and 194 deletions

2
.idea/gradle.xml generated
View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
@@ -10,6 +11,7 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/navi-alfred" />
</set>
</option>
</GradleProjectSettings>

2
.idea/misc.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -5,14 +5,11 @@ plugins {
android {
namespace 'com.alfred.demo'
compileSdk 33
compileSdk 32
defaultConfig {
applicationId "com.alfred.demo"
minSdk 21
targetSdk 33
versionCode 1
versionName "1.0"
targetSdk 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -33,12 +30,6 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.3.2'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
@@ -47,20 +38,13 @@ android {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.5.1'
implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'com.google.code.gson:gson:2.8.9'
testImplementation "junit:junit:4.13.2"
androidTestImplementation "androidx.test.ext:junit:1.1.4"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
}

View File

@@ -1,46 +0,0 @@
package com.alfred.demo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.alfred.demo.ui.theme.AlfredAndroidTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AlfredAndroidTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
AlfredAndroidTheme {
Greeting("Android")
}
}

View File

@@ -1,11 +0,0 @@
package com.alfred.demo.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -1,70 +0,0 @@
package com.alfred.demo.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun AlfredAndroidTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -1,34 +0,0 @@
package com.alfred.demo.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@@ -1,6 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
}
plugins {
id 'com.android.application' version '8.0.0' apply false
id 'com.android.library' version '8.0.0' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.6.21' apply false
}

View File

@@ -1,15 +1,17 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
android {
namespace 'com.navi.alfred'
compileSdk 33
compileSdk 32
defaultConfig {
minSdk 21
targetSdk 33
targetSdk 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -21,6 +23,21 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
flavorDimensions "app"
productFlavors {
qa {
dimension "app"
buildConfigField 'String', 'BASE_URL', formatString('https://qa-alfred-ingester.np.navi-sa.in/')
}
dev {
dimension "app"
buildConfigField 'String', 'BASE_URL', formatString('https://dev-sa.navi.com/')
}
prod{
dimension "app"
buildConfigField 'String', 'BASE_URL', formatString('https://alfred-ingester.prod.navi-sa.in/')
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@@ -29,13 +46,42 @@ android {
jvmTarget = '1.8'
}
}
static def formatString(String value) {
return '"' + value + '"'
}
dependencies {
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation 'androidx.core:core-ktx:1.6.0'
implementation "androidx.appcompat:appcompat:1.5.1"
implementation "com.google.android.material:material:1.7.0"
// Timber for logging
implementation 'com.jakewharton.timber:timber:5.0.1'
// Gson
implementation 'com.google.code.gson:gson:2.9.0'
// Firebase SDK for Push Notification
api 'com.google.firebase:firebase-analytics-ktx'
// Firebase SDK for Google Analytics (Kotlin)
api 'com.google.firebase:firebase-crashlytics:18.3.6'
// Import the BoM for the Firebase platform
api platform('com.google.firebase:firebase-bom:30.1.0')
implementation "androidx.room:room-runtime:2.4.3"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:2.7.1"
// To use Kotlin annotation processing tool (kapt)
kapt "androidx.room:room-compiler:2.4.3"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:2.4.3"
api "com.squareup.retrofit2:retrofit:2.9.0"
api 'com.squareup.retrofit2:converter-gson:2.9.0'
api 'com.squareup.okhttp3:logging-interceptor:4.9.0'
debugImplementation 'com.github.chuckerteam.chucker:library:3.5.2'
releaseImplementation 'com.github.chuckerteam.chucker:library-no-op:3.5.2'
testImplementation "androidx.room:room-testing:2.4.3"
}

View File

@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>

View File

@@ -0,0 +1,244 @@
/*
*
* * Copyright © 2022-2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred
import android.os.Build
import android.provider.Settings
import android.text.TextUtils
import com.navi.alfred.utils.AlfredConstants
import com.navi.alfred.utils.getCarrierName
import com.navi.alfred.utils.getNetworkType
import java.util.*
data class AlfredConfig(
private var appVersionCode: String = "",
private var appVersionName: String = "",
private var appName: String = "",
private var framesInterval: Int = 2000,
private var enable: Boolean = false,
private var eventBatchSize: Int = 20,
private var alfredSessionId: String = "",
private var flavor: String = "",
private var advertisingId: String? = null,
private var latitude: Double? = null,
private var longitude: Double? = null,
private var userId: String? = null,
private var alfredEventId: String = "",
private var eventStartRecordingTime: Long? = null,
private var nextEventStartRecordingTime: Long? = null,
private var sessionStartRecordingTime: Long = 0L,
private var videoQuality: String? = null,
private var enableRecording: Boolean = false,
private var enableAnr: Boolean = false,
private var enableCrash: Boolean = false,
var cpuUsageBeforeEventStart: Float? = null,
var memoryUsageBeforeEventStart: Float? = null,
var storageUsageBeforeEventStart: Float? = null,
private var cpuEnableStatus: Boolean = false,
private var memoryEnableStatus: Boolean = false,
private var metricsApiEnableStatus: Boolean = false,
var batteryPercentageBeforeEventStart: Float? = null,
private var disableScreenList: List<String>? = null,
private var disableModuleList: List<String>? = null,
private var snapshotPerSecond: Int = 1,
private var enableAlfred: Boolean = false,
private var firebaseControlledCruise: Boolean = false,
private var disableDialogScreenShot: Boolean = false
) {
fun getDeviceId(): String = Settings.System.getString(
AlfredManager.applicationContext.contentResolver, Settings.Secure.ANDROID_ID
)
fun setEventStartRecordingTime(setToNext: Boolean? = false) {
if (setToNext == true) {
this.eventStartRecordingTime = this.nextEventStartRecordingTime
} else {
this.eventStartRecordingTime = System.currentTimeMillis()
}
}
fun setNextEventStartRecordingTime() {
this.nextEventStartRecordingTime = System.currentTimeMillis()
}
fun getEventStartRecordingTime(): Long? = this.eventStartRecordingTime
fun setSessionStartRecordingTime() {
this.sessionStartRecordingTime = System.currentTimeMillis()
}
fun getSessionStartRecordingTime(): Long = this.sessionStartRecordingTime
fun getAlfredEventId(): String = this.alfredEventId
fun setAlfredEventId() {
this.alfredEventId = UUID.randomUUID().toString().plus(AlfredConstants.ALFRED_EVENT_ID)
}
fun setAlfredSessionId() {
this.alfredSessionId = UUID.randomUUID().toString().plus(AlfredConstants.ALFRED_SESSION_ID)
}
fun getAlfredSessionId(): String = this.alfredSessionId
fun getVideoQuality(): String? = this.videoQuality
fun setVideoQuality(videoQuality: String?) {
this.videoQuality = videoQuality
}
fun getAlfredStatus(): Boolean = this.enableAlfred
fun setAlfredStatus(enable: Boolean) {
this.enableAlfred = enable
}
fun getFirebaseControlledCruise(): Boolean = this.firebaseControlledCruise
fun setFirebaseControlledCruise(firebaseControlledCruise: Boolean) {
this.firebaseControlledCruise = firebaseControlledCruise
}
fun setEnableRecordingStatus(enableRecording: Boolean) {
this.enableRecording = enableRecording
}
fun getEnableRecordingStatus(): Boolean = this.enableRecording
fun getEventTimeStamp(): Long = System.currentTimeMillis()
fun setUserId(userId: String?) {
this.userId = userId
}
fun setLocation(latitude: Double, longitude: Double) {
this.latitude = latitude
this.longitude = longitude
}
fun getAppVersionCode(): String = appVersionCode
fun getAppVersionName(): String = appVersionName
fun getManufacturer(): String? = Build.MANUFACTURER
fun getDeviceModel(): String? = Build.MODEL
fun getOs(): String = AlfredConstants.OS_ANDROID
fun getOsVersion(): String = Build.VERSION.SDK_INT.toString()
fun getNetworkCarrier(): String? = getCarrierName(AlfredManager.applicationContext)
fun getNetworkType(): String = getNetworkType(AlfredManager.applicationContext)
fun getBatteryPercentage(): Float =
com.navi.alfred.utils.getBatteryPercentage(AlfredManager.applicationContext)
fun getUserId(): String? = userId
fun getEventBatchSize(): Int = eventBatchSize
fun getLatitude(): Double? = latitude
fun getEventsDelayInMilliseconds(): Long =
AlfredConstants.DEFAULT_EVENT_DELAY_IN_SECONDS.toLong() * 1000
fun getLongitude(): Double? = longitude
fun getPostUrl(): String = AlfredConstants.DEFAULT_SEND_EVENT_POST_URL
fun isProd(): Boolean = TextUtils.equals(flavor, AlfredConstants.PROD)
fun isQa(): Boolean = TextUtils.equals(flavor, AlfredConstants.QA)
fun isEnable(): Boolean = enable
fun setCpuUsageBeforeEventStart() {
this.cpuUsageBeforeEventStart = getCpuUsage()
}
fun setMemoryUsageBeforeEventStart() {
this.memoryUsageBeforeEventStart = getMemoryUsage()
}
fun setStorageUsageBeforeEventStart() {
this.storageUsageBeforeEventStart = getStorageUsage()
}
fun setBatteryPercentageBeforeEventStart() {
this.batteryPercentageBeforeEventStart = getBatteryPercentage()
}
fun getCpuUsage(): Float = com.navi.alfred.utils.getCpuUsage()
fun getMemoryUsage(): Float = com.navi.alfred.utils.getMemoryUsage()
fun getStorageUsage(): Float {
val (totalSize, freeSize) = com.navi.alfred.utils.getStorageUsage(context = AlfredManager.applicationContext)
return (totalSize - freeSize).toFloat()
}
fun setDisableScreenList(disableScreenList: List<String>) {
this.disableScreenList = disableScreenList
}
fun setDisableModuleList(disableModuleList: List<String>) {
this.disableModuleList = disableModuleList
}
fun getDisableModuleList(): List<String>? = this.disableModuleList
fun setAnrEnableStatus(status: Boolean) {
this.enableAnr = status
}
fun getAnrEnableStatus(): Boolean = this.enableAnr
fun setSnapshotPerSecond(frequency: Int) {
this.snapshotPerSecond = frequency
}
fun getSnapshotPerSecond(): Int = this.snapshotPerSecond
fun setCrashEnableStatus(status: Boolean) {
this.enableCrash = status
}
fun getCrashEnableStatus(): Boolean = this.enableCrash
fun getDisableScreenList(): List<String>? = this.disableScreenList
fun setCpuEnableStatus(status: Boolean) {
this.cpuEnableStatus = status
}
fun setMemoryEnableStatus(status: Boolean) {
this.memoryEnableStatus = status
}
fun getCpuEnableStatus(): Boolean = this.cpuEnableStatus
fun getMemoryEnableStatus(): Boolean = this.memoryEnableStatus
fun setApiMetricsEnableStatus(status: Boolean) {
this.metricsApiEnableStatus = status
}
fun getMetricsApiEnableStatus(): Boolean = this.metricsApiEnableStatus
fun setDisableDialogScreenShot(status: Boolean){
this.disableDialogScreenShot=status
}
fun getDisableDialogScreenShot(): Boolean {
return this.disableDialogScreenShot
}
}

View File

@@ -0,0 +1,890 @@
/*
*
* * Copyright © 2022-2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred
import android.app.Dialog
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.view.KeyEvent.ACTION_DOWN
import android.view.KeyEvent.ACTION_UP
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.annotation.WorkerThread
import androidx.work.*
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.alfred.utils.ScreenShotStorageHelper
import com.navi.alfred.db.AlfredDatabase
import com.navi.alfred.db.AlfredDatabaseHelper
import com.navi.alfred.db.dao.ApiMetricDao
import com.navi.alfred.db.dao.ScreenShotDao
import com.navi.alfred.db.dao.ZipDetailsDao
import com.navi.alfred.db.model.AnalyticsEvent
import com.navi.alfred.db.model.ApiMetricHelper
import com.navi.alfred.db.model.ScreenShotPathHelper
import com.navi.alfred.db.model.ZipDetailsHelper
import com.navi.alfred.dispatcher.AlfredDispatcher
import com.navi.alfred.model.*
import com.navi.alfred.network.AlfredNetworkRepository
import com.navi.alfred.network.AlfredRetrofitProvider
import com.navi.alfred.network.model.CruiseResponse
import com.navi.alfred.utils.*
import com.navi.alfred.utils.AlfredConstants.API_METRICS
import com.navi.alfred.utils.AlfredConstants.CODE_API_SUCCESS
import com.navi.alfred.utils.AlfredConstants.THIRD_PARTY_MODULE
import com.navi.alfred.utils.AlfredConstants.ZIP_FILE_EXTENSION
import com.navi.alfred.worker.AddEventTask
import com.navi.alfred.worker.AddMetricTask
import com.navi.alfred.worker.UploadFileWorker
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.Response
import okio.Buffer
import java.io.File
import java.lang.reflect.Type
import java.util.*
import java.util.concurrent.Executors
import kotlin.concurrent.fixedRateTimer
object AlfredManager {
lateinit var config: AlfredConfig
lateinit var applicationContext: Context
private var previousTouchEvent: NaviMotionEvent = NaviMotionEvent()
private val mutex = Mutex()
private val coroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val repository = AlfredNetworkRepository()
private val completableJob = Job()
private var hasUploadFlowStarted: Boolean = false
private lateinit var alfredDataBase: AlfredDatabase
private lateinit var screenShotDao: ScreenShotDao
private lateinit var zipDetailsDao: ZipDetailsDao
private lateinit var apiMetricDao: ApiMetricDao
private const val imageThreshHoldValue: Int = 60
private const val imageSecondThreshHoldValue: Int = 100
private var screenShotCaptureDelay: Long = 1000L
private var isAppInBackground: Boolean = false
private var hasRecordingStarted: Boolean = false
var dialog: Dialog? = null
private var timer: Timer? = null
private var screenShotTimer: Timer? = null
private var viewLayoutDelay: Long = 1000
private var currentScreenName: String? = null
private var currentModuleName: String? = null
private var sessionIdForCrash: String? = null
private var sessionStartRecordingTimeForCrash: Long? = null
private var eventStartRecordingTimeForCrash: Long? = null
private lateinit var zipFileDetails: List<ZipDetailsHelper>
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
}
private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob + exceptionHandler)
fun init(config: AlfredConfig, context: Context) {
this.config = config
this.applicationContext = context
AlfredRetrofitProvider.init(applicationContext)
startSyncEvents(context)
alfredDataBase = AlfredDatabaseHelper.getAnalyticsDatabase(applicationContext)
this.screenShotDao = alfredDataBase.screenShotDao()
this.zipDetailsDao = alfredDataBase.zipDetailsDao()
this.apiMetricDao = alfredDataBase.apiMetricDao()
}
fun startRecording(
context: Context,
view: View,
screenName: String? = null,
moduleName: String,
thirdPartyScreenView: View? = null
) {
if (config.getEnableRecordingStatus().not()) {
return
}
if (!hasRecordingStarted) {
checkDbBeforeStartRecording()
config.setAlfredSessionId()
config.setSessionStartRecordingTime()
config.setEventStartRecordingTime()
handleDeviceAttributes()
}
currentScreenName = screenName
currentModuleName = moduleName
hasRecordingStarted = true
screenShotTimer?.cancel()
screenShotTimer = Timer()
var bmpForCanvas: Pair<Canvas, Bitmap>? = null
var bmpForThirdPartySdkScreen: Bitmap? = null
val timerTask: TimerTask = object : TimerTask() {
override fun run() {
coroutineScope.launch(Dispatchers.IO) {
if (moduleName == THIRD_PARTY_MODULE) {
if (bmpForThirdPartySdkScreen == null) {
bmpForThirdPartySdkScreen =
thirdPartyScreenView?.let { captureScreenshotOfCustomView(it) }
}
insertScreenShotPathInDb(
this, applicationContext, bmpForThirdPartySdkScreen
)
} else {
if (bmpForCanvas == null) {
delay(viewLayoutDelay)
bmpForCanvas = createBitmapForView(view)
}
try {
if (dialog != null) {
captureBottomSheet(
view,
context,
screenName,
bmpForCanvas?.first,
rootBmp = bmpForCanvas?.second,
moduleName = moduleName
)
} else {
captureScreen(
view,
context,
screenName = screenName,
scope = coroutineScope,
canvas = bmpForCanvas?.first,
bmp = bmpForCanvas?.second,
moduleName = moduleName
)
}
} catch (e: Exception) {
e.log()
}
}
handleScreenShot()
}
}
}
screenShotCaptureDelay = (1000 / config.getSnapshotPerSecond().toLong())
screenShotTimer?.schedule(timerTask, 0, screenShotCaptureDelay)
}
private suspend fun handleScreenShot() {
if (ScreenShotStorageHelper.images.size == imageThreshHoldValue || ScreenShotStorageHelper.images.size == imageSecondThreshHoldValue) {
toZip(
screenShotDao.fetchScreenShotsPath(imageThreshHoldValue)
)
} else {
if (ScreenShotStorageHelper.images.size == 1) {
checkToStartZipUpload()
}
}
}
private suspend fun checkToStartZipUpload() {
val zipFileName =
config.getAlfredSessionId() + config.getSessionStartRecordingTime().toString()
if (checkFileExists(
zipFileName, applicationContext
) != null
) {
if (!hasUploadFlowStarted) {
hasUploadFlowStarted = true
getPreSignedUrl(zipFileName)
}
}
}
private suspend fun captureBottomSheet(
view: View,
context: Context,
screenName: String? = null,
rootCanvas: Canvas? = null,
rootBmp: Bitmap? = null,
moduleName: String? = null
) {
if(config.getDisableDialogScreenShot()){
return
}
val bottomSheetView = dialog?.window?.decorView?.rootView
if (bottomSheetView != null) {
val bottomSheetCanvasForBitmap = createBitmapForView(bottomSheetView)
val bottomSheetScreenShot = captureScreen(
bottomSheetView,
context,
true,
screenName,
coroutineScope,
bottomSheetCanvasForBitmap?.first,
bottomSheetCanvasForBitmap?.second,
moduleName = moduleName
)
val backgroundScreenShot =
captureScreen(
view,
context,
true,
screenName,
coroutineScope,
rootCanvas,
rootBmp,
moduleName = moduleName
)
if (bottomSheetScreenShot != null && backgroundScreenShot != null) {
combineScreenshots(
backgroundScreenShot,
bottomSheetScreenShot,
context,
screenName,
moduleName = moduleName,
scope = coroutineScope
)
}
}
}
private fun checkDbBeforeStartRecording() {
coroutineScope.launch(Dispatchers.IO) {
if (screenShotDao.getScreenShotCount() > 0) {
clearScreenShot(screenShotDao.fetchAllScreenShotsPath())
screenShotDao.deleteAllScreenShot()
}
if (zipDetailsDao.getZipFilesDetailsCount() > 0) {
zipFileDetails = zipDetailsDao.fetchAllZipFilesDetails()
zipFileDetails.forEachIndexed { index, DumpZipDetailsHelper ->
val zipFileName =
DumpZipDetailsHelper.alfredSessionId + DumpZipDetailsHelper.sessionStartRecordingTime.toString()
if (checkFileExists(fileName = zipFileName, applicationContext) != null) {
getPreSignedUrl(zipFileName, true, index)
} else {
zipDetailsDao.deleteZipFileDetail(DumpZipDetailsHelper.id)
}
}
}
}
}
fun measureInflatedView(view: View) {
view.layoutParams = ViewGroup.LayoutParams(1080, 1920)
view.measure(
View.MeasureSpec.makeMeasureSpec(
view.layoutParams.width, View.MeasureSpec.EXACTLY
), View.MeasureSpec.makeMeasureSpec(
view.layoutParams.height, View.MeasureSpec.EXACTLY
)
)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
suspend fun getCruiseConfig() {
val response = repository.cruiseConfig(
AlfredConstants.DEFAULT_CRUISE_CONFIG_URL,
config.getAppVersionName(),
config.getOsVersion(),
config.getDeviceId()
)
if (response.isSuccessful && response.code() == CODE_API_SUCCESS) {
response.body()?.let { cruiseResponse ->
setCruiseConfig(cruiseResponse)
}
}
}
private fun setCruiseConfig(cruiseResponse: CruiseResponse) {
cruiseResponse.data.let { responseList ->
if (responseList.isNotEmpty()) {
val cruiseConfig = responseList.first()
cruiseConfig.source.let { source ->
source.enable?.let { sourceConfig ->
config.setAlfredStatus(sourceConfig)
}
source.recordingsConfig?.let { recordingConfig ->
recordingConfig.enable?.let { enable ->
config.setEnableRecordingStatus(enable)
}
recordingConfig.videoQuality?.let { videoQuality ->
config.setVideoQuality(videoQuality)
}
recordingConfig.disableAnrRecording?.let { anrStatus ->
config.setAnrEnableStatus(!anrStatus)
}
recordingConfig.disableCrashRecording?.let { crashStatus ->
config.setCrashEnableStatus(!crashStatus)
}
recordingConfig.disableScreens?.let { disableScreen ->
config.setDisableScreenList(disableScreen)
}
recordingConfig.disableModules?.let { disableModuleList ->
config.setDisableModuleList(disableModuleList)
}
recordingConfig.snapshotPerSecond?.let { snapShotPerSecond ->
config.setSnapshotPerSecond(snapShotPerSecond)
}
}
source.metricsConfig?.let { metricsConfig ->
metricsConfig.disableMemoryMonitoring?.let { memory_monitor_status ->
config.setMemoryEnableStatus(!memory_monitor_status)
}
metricsConfig.disableCpuMonitoring?.let { cpu_monitor_status ->
config.setCpuEnableStatus(!cpu_monitor_status)
}
metricsConfig.disableApiPerformance?.let { api_monitor_status ->
config.setApiMetricsEnableStatus(!api_monitor_status)
}
}
}
}
}
}
private suspend fun getPreSignedUrl(
zipFileName: String, dumpFlow: Boolean = false, index: Int? = null
) {
config.setAlfredEventId()
val bucketKey = config.getAlfredEventId().plus(ZIP_FILE_EXTENSION)
val response = repository.getPreSignedUrl(bucketKey)
if (response.isSuccessful && response.code() == CODE_API_SUCCESS) {
checkFileExists(
zipFileName, applicationContext
)?.let { file ->
response.body()?.data?.let { uploadFile(file, it, dumpFlow, index) }
}
} else {
if (!dumpFlow) {
hasUploadFlowStarted = false
config.getEventStartRecordingTime()?.let { eventStartRecordingTime ->
insertZipDetailsToDbForDumpingLater(
config.getAlfredSessionId(),
config.getSessionStartRecordingTime(),
eventStartRecordingTime
)
}
config.setEventStartRecordingTime(true)
}
}
}
private fun checkAndInitiateFileUploadWorkManager() {
val screenShotList = screenShotDao.fetchAllScreenShotsPath()
screenShotDao.deleteAllScreenShot()
val requestData = Data.Builder()
.putString(AlfredConstants.ALFRED_SESSION_ID, config.getAlfredSessionId())
.putLong(
AlfredConstants.SESSION_START_RECORDING_TIME,
config.getSessionStartRecordingTime()
).putLong(
AlfredConstants.EVENT_START_RECORDING_TIME,
config.getEventStartRecordingTime() ?: 0L
).putString(AlfredConstants.SCREENSHOT_LIST, Gson().toJson(screenShotList))
.build()
val constraints = Constraints.Builder().setRequiresBatteryNotLow(false)
.setRequiredNetworkType(NetworkType.CONNECTED).build()
val uniqueWorkName = "YOUR_UNIQUE_WORK_NAME"
val uniqueWorkStatus = WorkManager.getInstance(applicationContext)
.getWorkInfosForUniqueWork(uniqueWorkName).get()
if (uniqueWorkStatus.isNullOrEmpty()) {
val uniqueWorkRequest = OneTimeWorkRequestBuilder<UploadFileWorker>()
.setConstraints(constraints)
.setInputData(requestData)
.build()
WorkManager.getInstance(applicationContext)
.beginUniqueWork(uniqueWorkName, ExistingWorkPolicy.REPLACE, uniqueWorkRequest)
.enqueue()
} else {
val workRequest = OneTimeWorkRequestBuilder<UploadFileWorker>()
.setConstraints(constraints)
.setInputData(requestData)
.build()
WorkManager.getInstance(applicationContext)
.enqueue(workRequest)
}
}
private suspend fun uploadFile(
uploadFile: File, url: String, dumpFlow: Boolean = false, index: Int? = null
) {
val requestBody = uploadFile.asRequestBody("application/zip".toMediaTypeOrNull())
val uploadResponse = repository.uploadZipToS3(
url, requestBody
)
if (uploadResponse.isSuccessful && uploadResponse.code() == CODE_API_SUCCESS) {
uploadFile.delete()
sendAlfredSessionEvent(dumpFlow, index)
} else {
if (!dumpFlow) {
config.getEventStartRecordingTime()?.let { eventStartRecordingTime ->
insertZipDetailsToDbForDumpingLater(
config.getAlfredSessionId(),
config.getSessionStartRecordingTime(),
eventStartRecordingTime
)
}
config.setEventStartRecordingTime(true)
}
}
if (!dumpFlow) {
hasUploadFlowStarted = false
}
}
private fun insertZipDetailsToDbForDumpingLater(
alfredSessionId: String,
sessionStartRecordingTime: Long,
eventStartRecordingTime: Long,
screenShotList: String? = null
) {
zipDetailsDao.insert(
data = ZipDetailsHelper(
alfredSessionId = alfredSessionId,
sessionStartRecordingTime = sessionStartRecordingTime,
eventStartRecordingTime = eventStartRecordingTime,
screenShotList = screenShotList
)
)
}
private fun deleteScreenShot() {
try {
val screenShotPathList: List<ScreenShotPathHelper> =
if (screenShotDao.getScreenShotCount() >= imageThreshHoldValue) {
screenShotDao.fetchScreenShotsPath(imageThreshHoldValue)
} else {
screenShotDao.fetchAllScreenShotsPath()
}
if (clearScreenShot(screenShotPathList)) {
try {
if (isAppInBackground) {
ScreenShotStorageHelper.clearAll()
screenShotDao.deleteAllScreenShot()
isAppInBackground = false
hasRecordingStarted = false
} else {
val idList = screenShotPathList.map { it.id }
screenShotDao.deleteScreenShot(idList)
ScreenShotStorageHelper.deleteKItems(idList.size)
}
} catch (e: Exception) {
e.log()
}
}
} catch (e: Exception) {
e.log()
}
}
suspend fun sendIngestMetric(): Boolean {
if (config.getAlfredStatus() && config.getMetricsApiEnableStatus()) {
mutex.withLock {
val metricEventList = apiMetricDao.fetchApiMetric(config.getEventBatchSize())
if (metricEventList.isNotEmpty()) {
try {
val detailsList = metricEventList.map { it.metric }
val listType: Type =
object : TypeToken<ArrayList<MetricAttribute?>?>() {}.type
val events: ArrayList<MetricAttribute> =
Gson().fromJson(detailsList.toString(), listType)
if (events.size > 0) {
val sessionId = events.first().sessionId
val request = EventMetricRequest(
baseAttribute = BaseAttribute(sessionId = sessionId),
metricsAttribute = events
)
val response = repository.eventMetric(
AlfredConstants.DEFAULT_INGEST_METRIC_URL,
metricRequestBody = request
)
return if (response.isSuccessful && response.code() == CODE_API_SUCCESS) {
apiMetricDao.deleteApiMetric(metricEventList.map { it.id })
true
} else {
false
}
}
} catch (e: Exception) {
return false
}
} else {
return false
}
}
return false
}
return false
}
private fun startSyncEvents(applicationContext: Context) {
timer = fixedRateTimer(
AlfredConstants.TIMER_THREAD_NAME,
false,
AlfredConstants.DEFAULT_INITIAL_DELAY,
config.getEventsDelayInMilliseconds()
) {
runBlocking {
AddMetricTask.resetEventCount()
sendIngestMetric()
AddEventTask.resetEventCount()
sendEventsToServer(applicationContext)
}
}
}
suspend fun sendEventsToServer(
applicationContext: Context
): Boolean {
if (config.getAlfredStatus() && config.getEnableRecordingStatus()) {
mutex.withLock {
val db = AlfredDatabaseHelper.getAnalyticsDatabase(applicationContext)
val analyticsDao = db.analyticsDao()
val analyticsEvents = analyticsDao.fetchEvents(config.getEventBatchSize())
if (analyticsEvents.isNotEmpty()) {
try {
val detailsList = analyticsEvents.map { it.details }
val listType: Type =
object : TypeToken<ArrayList<EventAttribute?>?>() {}.type
val events: ArrayList<EventAttribute> =
Gson().fromJson(detailsList.toString(), listType)
if (events.size > 0) {
val sessionId = events.first().sessionId
val request = AnalyticsRequest(
baseAttribute = BaseAttribute(
sessionId = sessionId
), events = events
)
val response = repository.sendEvents(
config.getPostUrl(), request
)
return if (response.isSuccessful && response.code() == CODE_API_SUCCESS) {
analyticsDao.deleteEvents(analyticsEvents.map { it.eventId })
true
} else {
false
}
} else {
false
}
} catch (e: Exception) {
return false
}
} else {
return false
}
}
return false
}
return false
}
private fun sendAlfredSessionEvent(dumpFlow: Boolean = false, index: Int? = null) {
var request: SessionRequest? = null
if (dumpFlow) {
var clientTs: Long? = null
var sessionTimeStamp: Long? = null
var sessionId: String? = null
if (index != null) {
val zipFileDetail = zipFileDetails[index]
clientTs = zipFileDetail.eventStartRecordingTime
sessionTimeStamp = zipFileDetail.sessionStartRecordingTime
sessionId = zipFileDetail.alfredSessionId
} else {
clientTs = eventStartRecordingTimeForCrash
sessionTimeStamp = sessionStartRecordingTimeForCrash
sessionId = sessionIdForCrash
}
request = SessionRequest(
base_attribute = BaseAttribute(
sessionId = sessionId,
eventTimeStamp = config.getEventTimeStamp(),
clientTs = clientTs,
sessionTimeStamp = sessionTimeStamp
), session_upload_event_attributes = SessionEventAttribute(
beginningDeviceAttributes = DeviceAttributes(),
endDeviceAttributes = DeviceAttributes()
)
)
} else {
request = SessionRequest(
base_attribute = BaseAttribute(
sessionId = config.getAlfredSessionId(),
eventTimeStamp = config.getEventTimeStamp(),
clientTs = config.getEventStartRecordingTime()
), session_upload_event_attributes = SessionEventAttribute(
beginningDeviceAttributes = DeviceAttributes(
battery = config.batteryPercentageBeforeEventStart,
cpu = config.cpuUsageBeforeEventStart,
memory = config.memoryUsageBeforeEventStart,
storage = config.storageUsageBeforeEventStart
), endDeviceAttributes = DeviceAttributes(
battery = config.getBatteryPercentage(),
cpu = config.getCpuUsage(),
memory = config.getMemoryUsage(),
storage = config.getStorageUsage()
)
)
)
}
coroutineScope.launch {
repository.sendSession(
AlfredConstants.DEFAULT_SEND_SESSION_POST_URL, request
)
if (!dumpFlow) {
config.setEventStartRecordingTime(true)
handleDeviceAttributes()
}
}
}
private fun handleDeviceAttributes() {
coroutineScope.launch(Dispatchers.IO) {
if (config.getAlfredStatus()) {
if (config.getCpuEnableStatus()) {
config.setCpuUsageBeforeEventStart()
}
if (config.getMemoryEnableStatus()) {
config.setMemoryUsageBeforeEventStart()
}
}
config.setStorageUsageBeforeEventStart()
config.setBatteryPercentageBeforeEventStart()
}
}
fun saveApiLog(request: Request, response: Response, startTime: Long, endTime: Long) {
var bytesSent = 0L
var byteReceived = 0L
if (request.body != null) {
val buffer = Buffer()
request.body!!.writeTo(buffer)
bytesSent = buffer.size
}
if (response.body != null) {
val responseBody = response.body
val source = responseBody?.source()
source?.request(Long.MAX_VALUE)
val buffer = source?.buffer()
byteReceived = buffer?.size ?: 0
}
var errorMessage: String? = null
val duration: Long = endTime - startTime
val errorType: String? = null
if (response.code != CODE_API_SUCCESS) {
errorMessage = response.message
}
val attributes: HashMap<String, Any> = hashMapOf()
attributes[AlfredConstants.URL] = request.url.toString()
attributes[AlfredConstants.METHOD] = request.method
attributes[AlfredConstants.RESPONSE_CODE] = response.code
attributes[AlfredConstants.ERROR_MESSAGE] = errorMessage.toString()
attributes[AlfredConstants.ERROR_TYPE] = errorType.toString()
attributes[AlfredConstants.START_TIME] = startTime
attributes[AlfredConstants.END_TIME] = endTime
attributes[AlfredConstants.DURATION_IN_MS] = duration.toDouble()
attributes[AlfredConstants.BYTES_RECEIVED] = byteReceived
attributes[AlfredConstants.BYTES_SENT] = bytesSent
coroutineDispatcher.executor.execute {
val appPerformanceEvent = buildAppPerformanceEvent(
AlfredConstants.API_METRIC_EVENT_NAME, API_METRICS, attributes
)
AlfredDispatcher.addTaskToQueue(
AddMetricTask(
appPerformanceEvent, applicationContext
)
)
}
}
fun stopRecording(appBackgroundView: View) {
isAppInBackground = true
hasRecordingStarted = false
screenShotTimer?.cancel()
if (config.getFirebaseControlledCruise()) {
deleteScreenShot()
} else {
if (config.getAlfredStatus() && config.getEnableRecordingStatus()) {
if (ScreenShotStorageHelper.images.size > 0) {
startAnrCrashZipUpload(appBackgroundView)
}
}
}
}
suspend fun toZipForWorkManager(
imagePathList: List<ScreenShotPathHelper>,
zipFileName: String,
sessionStartRecordingTime: Long? = null,
alfredSessionId: String? = null,
eventStartRecordingTime: Long? = null,
index: Int? = null
) {
sessionIdForCrash = alfredSessionId
sessionStartRecordingTimeForCrash = sessionStartRecordingTime
eventStartRecordingTimeForCrash = eventStartRecordingTime
val fileList = ArrayList<String>()
imagePathList.forEach { screenShotPathHelper ->
screenShotPathHelper.screenShotPath?.let { screenShotPath ->
if (!fileList.contains(screenShotPath)) {
fileList.add(screenShotPath)
}
}
}
val zipFilePath = applicationContext.filesDir.path + "/" + zipFileName
if (zip(fileList, zipFilePath) == true) {
clearScreenShot(imagePathList)
getPreSignedUrl(zipFileName, true, index = index)
}
}
private fun toZip(
imagePathList: List<ScreenShotPathHelper>
) {
val zipFilePath: String =
applicationContext.filesDir.path + "/" + config.getAlfredSessionId() + config.getSessionStartRecordingTime()
val fileList = ArrayList<String>()
imagePathList.forEach { screenShotPathHelper ->
screenShotPathHelper.screenShotPath?.let { screenShotPath ->
if (!fileList.contains(screenShotPath)) {
fileList.add(screenShotPath)
}
}
}
if (zip(fileList, zipFilePath) == true) {
deleteScreenShot()
config.setNextEventStartRecordingTime()
}
}
fun handleTouchEvent(
currentTouchEvent: MotionEvent?, screenName: String? = null, moduleName: String? = null
) {
if (config.getAlfredStatus() && config.getEnableRecordingStatus()) {
coroutineDispatcher.executor.execute {
if (currentTouchEvent?.action == ACTION_DOWN) {
previousTouchEvent.action = currentTouchEvent.action
previousTouchEvent.positionX = currentTouchEvent.rawX
previousTouchEvent.positionY = currentTouchEvent.rawY
}
if (currentTouchEvent?.action == ACTION_UP) {
val touchEventData = getTouchEvent(currentTouchEvent, previousTouchEvent)
val touchEventProperties = touchEventData.second
touchEventProperties[AlfredConstants.SCREEN_WIDTH] = getScreenWidth().toString()
touchEventProperties[AlfredConstants.SCREEN_HEIGHT] = getScreenHeight().toString()
val event = buildEvent(
touchEventData.first,
touchEventProperties,
screenName = screenName,
moduleName = moduleName
)
AlfredDispatcher.addTaskToQueue(AddEventTask(event, applicationContext))
}
}
}
}
private fun startAnrCrashZipUpload(view: View) {
if (ScreenShotStorageHelper.images.size > 0) {
coroutineScope.launch(Dispatchers.IO) {
var bmp: Bitmap? = null
val screenShotCapture = async {
bmp = captureScreenshotOfCustomView(
view
)
}
screenShotCapture.await()
val insertScreenShotInDb = async {
insertScreenShotPathInDb(this, applicationContext, bmp)
}
insertScreenShotInDb.await()
checkAndInitiateFileUploadWorkManager()
}
}
}
fun handleAnrEvent(
anrEventProperties: Map<String, String>, anrView: View, screenName: String? = null
) {
startAnrCrashZipUpload(anrView)
coroutineDispatcher.executor.execute {
val event = buildEvent(
AlfredConstants.ANR_EVENT, anrEventProperties as HashMap<String, String>,
screenName = screenName,
moduleName = currentModuleName
)
AlfredDispatcher.addTaskToQueue(AddEventTask(event, this.applicationContext))
}
}
fun handleSWWEvent(
screenName: String? = null, swwEventProperties: Map<String, String>
) {
coroutineDispatcher.executor.execute {
val event = buildEvent(
AlfredConstants.ERROR_LOG,
swwEventProperties as HashMap<String, String>,
screenName = screenName,
moduleName = currentModuleName
)
AlfredDispatcher.addTaskToQueue(AddEventTask(event, applicationContext))
}
}
fun handleCrashEvent(
crashEventProperties: Map<String, String>,
crashView: View,
screenName: String? = null
) {
startAnrCrashZipUpload(crashView)
coroutineDispatcher.executor.execute {
val event = buildEvent(
AlfredConstants.CRASH_ANALYTICS_EVENT,
crashEventProperties as HashMap<String, String>,
screenName = screenName,
moduleName = currentModuleName
)
AlfredDispatcher.addTaskToQueue(AddEventTask(event, applicationContext))
}
}
@WorkerThread
fun buildEvent(
eventName: String,
properties: HashMap<String, String>? = null,
screenName: String? = null,
moduleName: String? = null
): AnalyticsEvent {
val timeStamp = System.currentTimeMillis()
val eventData = EventAttribute(
eventName = eventName,
eventType = eventName,
eventTimestamp = timeStamp,
attributes = properties,
sessionId = config.getAlfredSessionId(),
screenName = screenName,
moduleName = moduleName
)
return AnalyticsEvent(timeStamp, Gson().toJson(eventData))
}
@WorkerThread
fun buildAppPerformanceEvent(
eventName: String, eventType: String, attribute: HashMap<String, Any>? = null
): ApiMetricHelper {
val timeStamp = System.currentTimeMillis()
val metricData = MetricAttribute(
eventId = config.getAlfredEventId(),
eventName = eventName,
eventType = eventType,
sessionId = config.getAlfredSessionId(),
attributes = attribute,
screenName = currentScreenName,
moduleName = currentModuleName
)
return ApiMetricHelper(timeStamp, Gson().toJson(metricData))
}
}

View File

@@ -0,0 +1,31 @@
/*
*
* * Copyright © 2022-2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.db
import androidx.room.Database
import androidx.room.RoomDatabase
import com.navi.alfred.db.dao.AnalyticsDAO
import com.navi.alfred.db.dao.ApiMetricDao
import com.navi.alfred.db.dao.ScreenShotDao
import com.navi.alfred.db.dao.ZipDetailsDao
import com.navi.alfred.db.model.AnalyticsEvent
import com.navi.alfred.db.model.ApiMetricHelper
import com.navi.alfred.db.model.ScreenShotPathHelper
import com.navi.alfred.db.model.ZipDetailsHelper
@Database(
entities = [AnalyticsEvent::class, ScreenShotPathHelper::class, ZipDetailsHelper::class, ApiMetricHelper::class],
version = 2,
exportSchema = true
)
abstract class AlfredDatabase : RoomDatabase() {
abstract fun analyticsDao(): AnalyticsDAO
abstract fun screenShotDao(): ScreenShotDao
abstract fun zipDetailsDao(): ZipDetailsDao
abstract fun apiMetricDao(): ApiMetricDao
}

View File

@@ -0,0 +1,31 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.db
import android.content.Context
import androidx.room.Room
import com.navi.alfred.utils.AlfredConstants.EVENT_DB_NAME
object AlfredDatabaseHelper {
@Volatile private var INSTANCE: AlfredDatabase? = null
fun getAnalyticsDatabase(context: Context): AlfredDatabase {
return INSTANCE
?: synchronized(this) {
if (INSTANCE != null) {
INSTANCE
}
val instance =
Room.databaseBuilder(context, AlfredDatabase::class.java, EVENT_DB_NAME).fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}

View File

@@ -0,0 +1,87 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.navi.alfred.db.model.AnalyticsEvent
import com.navi.alfred.db.model.ApiMetricHelper
import com.navi.alfred.db.model.ScreenShotPathHelper
import com.navi.alfred.db.model.ZipDetailsHelper
@Dao
interface AnalyticsDAO {
@Insert
fun insertEvent(event: AnalyticsEvent)
@Query("SELECT * FROM AnalyticsEvent ORDER BY time ASC LIMIT :thresholdValue")
fun fetchEvents(thresholdValue: Int): List<AnalyticsEvent>
@Query("DELETE FROM AnalyticsEvent WHERE eventId IN (:idList)")
fun deleteEvents(idList: List<Int>)
@Query("SELECT count(*) FROM AnalyticsEvent")
fun getEventCount(): Int
}
@Dao
interface ScreenShotDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertScreenShotPath(event: ScreenShotPathHelper)
@Query("SELECT * FROM ScreenShotPathHelper ORDER BY time ASC LIMIT :thresholdValue")
fun fetchScreenShotsPath(thresholdValue: Int): List<ScreenShotPathHelper>
@Query("SELECT * FROM ScreenShotPathHelper")
fun fetchAllScreenShotsPath(): List<ScreenShotPathHelper>
@Query("DELETE FROM ScreenShotPathHelper WHERE id IN (:idList)")
fun deleteScreenShot(idList: List<Int>)
@Query("DELETE FROM ScreenShotPathHelper")
fun deleteAllScreenShot()
@Query("SELECT count(*) FROM ScreenShotPathHelper")
fun getScreenShotCount(): Int
}
@Dao
interface ZipDetailsDao{
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(data: ZipDetailsHelper)
@Query("SELECT count(*) FROM ZipDetailsHelper")
fun getZipFilesDetailsCount(): Int
@Query("SELECT * FROM ZipDetailsHelper ORDER BY sessionStartRecordingTime")
fun fetchAllZipFilesDetails(): List<ZipDetailsHelper>
@Query("DELETE FROM ZipDetailsHelper WHERE id = :id")
fun deleteZipFileDetail(id: Int)
}
@Dao
interface ApiMetricDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(data: ApiMetricHelper)
@Query("SELECT count(*) FROM ApiMetricHelper")
fun getApiMetricCount(): Int
@Query("SELECT * FROM ApiMetricHelper ORDER BY time ASC LIMIT :thresholdValue")
fun fetchApiMetric(thresholdValue: Int): List<ApiMetricHelper>
@Query("DELETE FROM ApiMetricHelper WHERE id IN (:idList)")
fun deleteApiMetric(idList: List<Int>)
}

View File

@@ -0,0 +1,68 @@
/*
*
* * Copyright © 2022-2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.db.model
import androidx.room.*
import com.google.gson.Gson
import com.navi.alfred.model.DeviceAttributes
@Entity
data class AnalyticsEvent(
@ColumnInfo(name = "time") val time: Long?,
@ColumnInfo(name = "details") val details: String?
) {
@PrimaryKey(autoGenerate = true)
var eventId: Int = 0
}
@Entity
data class ScreenShotPathHelper(
@ColumnInfo(name = "time") val time: Long?,
@ColumnInfo(name = "screenShotPath") val screenShotPath: String?
) {
@PrimaryKey(autoGenerate = true)
var id: Int = 0
}
@Entity
@TypeConverters(Converter::class)
data class ZipDetailsHelper(
@ColumnInfo(name = "alfredSessionId") val alfredSessionId: String,
@ColumnInfo(name = "sessionStartRecordingTime") val sessionStartRecordingTime: Long,
@ColumnInfo(name = "eventStartRecordingTime") val eventStartRecordingTime: Long,
@ColumnInfo(name = "screenShotList") val screenShotList: String?
) {
@PrimaryKey(autoGenerate = true)
var id: Int = 0
}
@Entity
data class ApiMetricHelper(
@ColumnInfo(name = "time") val time: Long?,
@ColumnInfo(name = "metric") val metric: String?
) {
@PrimaryKey(autoGenerate = true)
var id: Int = 0
}
class Converter {
@TypeConverter
fun fromDeviceAttributes(data: DeviceAttributes): String {
return Gson().toJson(data)
}
@TypeConverter
fun toDeviceAttributes(data: String): DeviceAttributes {
return Gson().fromJson(data, DeviceAttributes::class.java)
}
}

View File

@@ -0,0 +1,18 @@
package com.navi.alfred.deserializer
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.navi.alfred.model.AnalyticsRequest
import java.lang.reflect.Type
class AnalyticsDataDeserializer : JsonDeserializer<AnalyticsRequest> {
override fun deserialize(
json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?
): AnalyticsRequest? {
json?.let {
return context?.deserialize(json, AnalyticsRequest::class.java)
}
return null
}
}

View File

@@ -0,0 +1,18 @@
package com.navi.alfred.deserializer
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.navi.alfred.model.EventMetricRequest
import java.lang.reflect.Type
class MetricsDataDeserializer : JsonDeserializer<EventMetricRequest> {
override fun deserialize(
json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?
): EventMetricRequest? {
json?.let {
return context?.deserialize(json, EventMetricRequest::class.java)
}
return null
}
}

View File

@@ -0,0 +1,19 @@
package com.navi.alfred.deserializer
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.navi.alfred.model.EventMetricRequest
import com.navi.alfred.model.SessionRequest
import java.lang.reflect.Type
class SessionDataDeserializer : JsonDeserializer<SessionRequest> {
override fun deserialize(
json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?
): SessionRequest? {
json?.let {
return context?.deserialize(json, EventMetricRequest::class.java)
}
return null
}
}

View File

@@ -0,0 +1,28 @@
/*
*
* * Copyright © 2019-2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.dispatcher
import com.navi.alfred.worker.AnalyticsTask
import com.navi.alfred.worker.AnalyticsTaskProcessor
/**
* handles task of putting events into DB and syncing data with backend
*/
object AlfredDispatcher {
private val taskProcessor: AnalyticsTaskProcessor = AnalyticsTaskProcessor()
fun addTaskToQueue(task: AnalyticsTask) {
taskProcessor.addTask(task)
}
/**
* Executes task in a different thread than the Queue
*/
fun executeInNewThread(task: AnalyticsTask) {
taskProcessor.executeInNewThread(task)
}
}

View File

@@ -0,0 +1,20 @@
package com.navi.alfred.model
import com.google.gson.annotations.SerializedName
data class EventMetricRequest(
@SerializedName("base_attributes") val baseAttribute: BaseAttribute,
@SerializedName("metrics_attributes") val metricsAttribute: List<MetricAttribute>
)
data class MetricAttribute(
@SerializedName("event_id") val eventId: String? = null,
@SerializedName("event_name") val eventName: String? = null,
@SerializedName("session_id") val sessionId: String? = null,
@SerializedName("event_type") val eventType: String? = null,
@SerializedName("screen_name") val screenName: String? = null,
@SerializedName("module_name") val moduleName: String? = null,
@SerializedName("attributes") val attributes: HashMap<String, Any>? = null
)

View File

@@ -0,0 +1,14 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.model
data class NaviMotionEvent(
var action: Int? = null,
var positionX: Float? = null,
var positionY: Float? = null
)

View File

@@ -0,0 +1,58 @@
package com.navi.alfred.model
import com.google.gson.annotations.SerializedName
import com.navi.alfred.AlfredManager
data class AnalyticsRequest(
@SerializedName("base_attributes") val baseAttribute: BaseAttribute? = null,
@SerializedName("events") val events: List<EventAttribute>? = null
)
data class EventAttribute(
@SerializedName("eventId") val eventId: String? = null,
@SerializedName("screen_name") val screenName: String? = null,
@SerializedName("event_name") val eventName: String? = null,
@SerializedName("event_timestamp") val eventTimestamp: Long? = null,
@SerializedName("event_type") val eventType: String? = null,
@SerializedName("attributes") val attributes: Map<String, String>? = null,
@SerializedName("session_id") val sessionId: String? = null,
@SerializedName("module_name") val moduleName: String? = null
)
data class SessionRequest(
@SerializedName("base_attributes") val base_attribute: BaseAttribute,
@SerializedName("session_upload_event_attributes") val session_upload_event_attributes: SessionEventAttribute
)
data class BaseAttribute(
@SerializedName("app_version_code") val appVersionCode: String = AlfredManager.config.getAppVersionCode(),
@SerializedName("app_version_name") val appVersionName: String? = AlfredManager.config.getAppVersionName(),
@SerializedName("client_ts") val clientTs: Long? = AlfredManager.config.getEventStartRecordingTime(),
@SerializedName("device_id") val deviceId: String? = AlfredManager.config.getDeviceId(),
@SerializedName("device_model") val deviceModel: String? = AlfredManager.config.getDeviceModel(),
@SerializedName("device_manufacturer") val deviceManufacturer: String? = AlfredManager.config.getManufacturer(),
@SerializedName("app_os") val appOs: String? = AlfredManager.config.getOs(),
@SerializedName("os_version") val osVersion: String? = AlfredManager.config.getOsVersion(),
@SerializedName("latitude") val latitude: Double? = AlfredManager.config.getLatitude(),
@SerializedName("longitude") val longitude: Double? = AlfredManager.config.getLongitude(),
@SerializedName("customer_id") val customerId: String? = AlfredManager.config.getUserId(),
@SerializedName("up_time") val upTime: Long? = null,
@SerializedName("carrier_name") val carrierName: String? = AlfredManager.config.getNetworkCarrier(),
@SerializedName("parent_session_id") val parentSessionId: String? = null,
@SerializedName("event_timestamp") val eventTimeStamp: Long? = AlfredManager.config.getEventTimeStamp(),
@SerializedName("session_id") val sessionId: String? = AlfredManager.config.getAlfredSessionId(),
@SerializedName("session_time_stamp") val sessionTimeStamp: Long? = AlfredManager.config.getSessionStartRecordingTime(),
)
data class SessionEventAttribute(
@SerializedName("event_id") val eventId: String? = AlfredManager.config.getAlfredEventId(),
@SerializedName("beginning_device_attributes") val beginningDeviceAttributes: DeviceAttributes? = null,
@SerializedName("end_device_attributes") val endDeviceAttributes: DeviceAttributes? = null
)
data class DeviceAttributes(
@SerializedName("battery") val battery: Float? = null,
@SerializedName("cpu") val cpu: Float? = null,
@SerializedName("storage") val storage: Float? = null,
@SerializedName("memory") val memory: Float? = null,
)

View File

@@ -0,0 +1,63 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.network
import com.navi.alfred.model.EventMetricRequest
import com.navi.alfred.model.SessionRequest
import com.navi.alfred.network.model.CruiseResponse
import com.navi.alfred.network.model.PreSignedUrlResponse
import com.navi.alfred.utils.AlfredConstants.ALFRED
import okhttp3.RequestBody
import retrofit2.Response
class AlfredNetworkRepository {
suspend fun sendEvents(
url: String, analyticsRequest: com.navi.alfred.model.AnalyticsRequest
): Response<Unit> {
return AlfredRetrofitProvider.getApiService()
.sendEvents(url, "application/json", ALFRED, analyticsRequest)
}
suspend fun getPreSignedUrl(sessionId: String): Response<PreSignedUrlResponse> {
return AlfredRetrofitProvider.getApiService().getPreSignedUrl(sessionId, ALFRED)
}
suspend fun sendSession(
url: String,
sessionRequest: SessionRequest
): Response<Unit> {
return AlfredRetrofitProvider.getApiService()
.sendSession(url, "application/json", ALFRED, sessionRequest)
}
suspend fun uploadZipToS3(
preSignedUrl: String, request: RequestBody
): Response<Unit> {
return AlfredRetrofitProvider.getApiService().uploadToS3(preSignedUrl, request, ALFRED)
}
suspend fun eventMetric(
url: String, metricRequestBody: EventMetricRequest
): Response<Unit> {
return AlfredRetrofitProvider.getApiService()
.sendMetric(url, ALFRED, "application/json", metricRequestBody)
}
suspend fun cruiseConfig(
url: String, appVersionName: String, osVersionCode: String, deviceId: String
): Response<CruiseResponse> {
return AlfredRetrofitProvider.getApiService().getCruiseConfig(
url,
ALFRED,
contentType = "application/json",
appVersionName = appVersionName,
osVersion = osVersionCode,
deviceId = deviceId
)
}
}

View File

@@ -0,0 +1,73 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.network
import android.content.Context
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.google.gson.GsonBuilder
import com.navi.alfred.AlfredManager
import com.navi.alfred.BuildConfig
import com.navi.alfred.deserializer.AnalyticsDataDeserializer
import com.navi.alfred.deserializer.MetricsDataDeserializer
import com.navi.alfred.deserializer.SessionDataDeserializer
import com.navi.alfred.model.EventMetricRequest
import com.navi.alfred.model.SessionRequest
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object AlfredRetrofitProvider {
private const val BASE_URL_DEBUG = "https://dev-sa.navi.com/"
private const val BASE_URL_PROD = "https://alfred-ingester.prod.navi-sa.in/"
private const val BASE_URL_QA = "https://qa-alfred-ingester.np.navi-sa.in/"
private lateinit var apiService: AlfredRetrofitService
private lateinit var okHttpClient: OkHttpClient
fun init(context: Context) {
okHttpClient = OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS)
if (BuildConfig.DEBUG) {
addInterceptor(loggingInterceptor())
addInterceptor(
ChuckerInterceptor.Builder(context).collector(ChuckerCollector(context))
.alwaysReadResponseBody(false).build()
)
}
}.build()
apiService = getRetrofit().create(AlfredRetrofitService::class.java)
}
private fun getRetrofit(): Retrofit {
val baseUrl = if (AlfredManager.config.isQa()) {
BASE_URL_QA
} else if (AlfredManager.config.isProd()) {
BASE_URL_PROD
} else {
BASE_URL_DEBUG
}
val providesDeserializer = GsonBuilder().registerTypeAdapter(
com.navi.alfred.model.AnalyticsRequest::class.java,
AnalyticsDataDeserializer()
).registerTypeAdapter(SessionRequest::class.java, SessionDataDeserializer())
.registerTypeAdapter(EventMetricRequest::class.java, MetricsDataDeserializer()).create()
return Retrofit.Builder().baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(providesDeserializer))
.client(okHttpClient).build()
}
fun getApiService(): AlfredRetrofitService {
return apiService
}
private fun loggingInterceptor() = HttpLoggingInterceptor().apply {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
}

View File

@@ -0,0 +1,71 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.network
import com.navi.alfred.model.EventMetricRequest
import com.navi.alfred.model.SessionRequest
import com.navi.alfred.network.model.CruiseResponse
import com.navi.alfred.network.model.PreSignedUrlResponse
import com.navi.alfred.utils.AlfredConstants.APP_VERSION_NAME
import com.navi.alfred.utils.AlfredConstants.CONTENT_TYPE
import com.navi.alfred.utils.AlfredConstants.DEVICE_ID
import com.navi.alfred.utils.AlfredConstants.OS_VERSION
import com.navi.alfred.utils.AlfredConstants.SESSION_ID
import com.navi.alfred.utils.AlfredConstants.X_TARGET
import okhttp3.RequestBody
import retrofit2.Response
import retrofit2.http.*
interface AlfredRetrofitService {
@POST
suspend fun sendEvents(
@Url url: String,
@Header(CONTENT_TYPE) contentType: String,
@Header(X_TARGET) target: String,
@Body analyticsRequest: com.navi.alfred.model.AnalyticsRequest
): Response<Unit>
@GET("ingest/session/pre-sign/{sessionId}")
suspend fun getPreSignedUrl(
@Path(SESSION_ID) sessionId: String,
@Header(X_TARGET) target: String,
): Response<PreSignedUrlResponse>
@PUT
suspend fun uploadToS3(
@Url url: String,
@Body file: RequestBody,
@Header(X_TARGET) target: String,
): Response<Unit>
@POST
suspend fun sendSession(
@Url url: String,
@Header(CONTENT_TYPE) contentType: String,
@Header(X_TARGET) target: String,
@Body sessionRequest: SessionRequest
): Response<Unit>
@POST
suspend fun sendMetric(
@Url url: String,
@Header(X_TARGET) target: String,
@Header(CONTENT_TYPE) contentType: String,
@Body metricRequestBody: EventMetricRequest
): Response<Unit>
@GET
suspend fun getCruiseConfig(
@Url url: String,
@Header(X_TARGET) target: String,
@Header(APP_VERSION_NAME) appVersionName: String,
@Header(OS_VERSION) osVersion: String,
@Header(DEVICE_ID) deviceId: String,
@Header(CONTENT_TYPE) contentType: String,
): Response<CruiseResponse>
}

View File

@@ -0,0 +1,15 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.network.model
import com.google.gson.annotations.SerializedName
data class PreSignedUrlResponse(
@SerializedName("data") val data: String? = null,
@SerializedName("status") val status: Int? = null
)

View File

@@ -0,0 +1,44 @@
package com.navi.alfred.network.model
import com.google.gson.annotations.SerializedName
data class CruiseResponse(
@SerializedName("data") val data: List<CruiseConfig>, @SerializedName("status") val status: Int
)
data class CruiseConfig(
@SerializedName("_id") val id: String, @SerializedName("_source") val source: Source
)
data class Source(
@SerializedName("enable") val enable: Boolean? = false,
@SerializedName("metrics_config") val metricsConfig: MetricsConfig? = null,
@SerializedName("os_config") val osConfig: OsConfig? = null,
@SerializedName("recordings_config") val recordingsConfig: RecordingsConfig? = null,
@SerializedName("type") val type: String? = null
)
data class RecordingsConfig(
@SerializedName("disable_anr_recording") val disableAnrRecording: Boolean? = null,
@SerializedName("disable_crash_recording") val disableCrashRecording: Boolean? = null,
@SerializedName("enable") val enable: Boolean? = null,
@SerializedName("snapshot_per_second") val snapshotPerSecond: Int? = null,
@SerializedName("video_quality") val videoQuality: String? = null,
@SerializedName("video_recording_policy") val videoRecordingPolicy: String? = null,
@SerializedName("disable_screens") val disableScreens: List<String>? = null,
@SerializedName("disable_modules") val disableModules: List<String>? = null
)
data class OsConfig(
@SerializedName("app_version") val appVersion: String? = null,
@SerializedName("app_version_code") val appVersionCode: String? = null,
@SerializedName("os_version") val osVersion: String? = null,
)
data class MetricsConfig(
@SerializedName("disable_api_performance") val disableApiPerformance: Boolean? = null,
@SerializedName("disable_cpu_monitoring") val disableCpuMonitoring: Boolean? = null,
@SerializedName("disable_memory_monitoring") val disableMemoryMonitoring: Boolean? = null,
@SerializedName("disable_remote_logging") val disableRemoteLogging: Boolean? = null,
@SerializedName("enable") val enable: Boolean? = null
)

View File

@@ -0,0 +1,72 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.utils
object AlfredConstants {
const val CODE_API_SUCCESS = 200
const val UNDERSCORE = "_"
const val ADD_EVENT_TASK = "AddEventTask"
const val ADD_API_METRIC_TASK = "AddMetricTask"
const val OS_ANDROID = "Android"
const val DEFAULT_SEND_EVENT_POST_URL = "/ingest/event"
const val DEFAULT_SEND_SESSION_POST_URL = "/ingest/session"
const val DEFAULT_CRUISE_CONFIG_URL = "/cruise"
const val DEFAULT_INGEST_METRIC_URL = "/ingest/metrics"
const val TIMER_THREAD_NAME = "SyncTimer"
const val DEFAULT_INITIAL_DELAY = 5000L
const val DEFAULT_EVENT_DELAY_IN_SECONDS = 30
const val PROD = "prod"
const val QA = "qa"
const val TOUCH_EVENT = "TOUCH_EVENT"
const val SCROLL_EVENT = "SCROLL_EVENT"
const val SCREEN_WIDTH = "SCREEN_WIDTH"
const val SCREEN_HEIGHT = "SCREEN_HEIGHT"
const val START_X = "START_X"
const val START_Y = "START_Y"
const val END_X = "END_X"
const val END_Y = "END_Y"
const val ANR_EVENT = "ANR_EVENT"
const val CRASH_ANALYTICS_EVENT = "CRASH_ANALYTICS_EVENT"
const val ERROR_LOG = "ERROR_LOG"
const val ALFRED_EVENT_ID = "ALFRED_EVENT_ID"
const val ALFRED_SESSION_ID = "ALFRED_SESSION_ID"
const val API_METRIC_EVENT_NAME = "API_METRIC_EVENT"
const val API_METRICS = "API_METRICS"
const val SESSION_START_RECORDING_TIME = "SESSION_START_RECORDING_TIME"
const val EVENT_START_RECORDING_TIME = "EVENT_START_RECORDING_TIME"
const val URL = "url"
const val METHOD = "method"
const val RESPONSE_CODE = "response_code"
const val ERROR_MESSAGE = "error_message"
const val ERROR_TYPE = "error_type"
const val START_TIME = "start_time"
const val END_TIME = "end_time"
const val DURATION_IN_MS = "duration_in_ms"
const val BYTES_RECEIVED = "bytes_received"
const val BYTES_SENT = "bytes_sent"
const val ALFRED = "ALFRED"
const val CONTENT_TYPE = "Content-Type"
const val X_TARGET = "X-Target"
const val APP_VERSION_NAME = "appVersionName"
const val OS_VERSION = "osVersion"
const val DEVICE_ID = "deviceId"
const val SESSION_ID = "sessionId"
const val THIRD_PARTY_SCREEN = "THIRD_PARTY_SCREEN"
const val THIRD_PARTY_MODULE = "THIRD_PARTY_MODULE"
const val INSURANCE_MODULE = "INSURANCE_MODULE"
const val CHAT_MODULE = "CHAT_MODULE"
const val SCREENSHOT_LIST = "SCREENSHOT_LIST"
const val REASON = "REASON"
const val CODE = "CODE"
const val STATUS_CODE = "STATUS_CODE"
const val EVENT_DB_NAME = "navi-analytics"
const val ZIP_FILE_EXTENSION = ".zip"
const val IMAGE_FILE_EXTENSION = ".jpeg"
const val SYNC_EVENT_TASK = "SyncEventTask"
}

View File

@@ -0,0 +1,10 @@
package com.navi.alfred.utils
import com.google.firebase.crashlytics.FirebaseCrashlytics
object AlfredHelper {
fun recordException(e: Throwable) {
FirebaseCrashlytics.getInstance().recordException(e)
}
}

View File

@@ -0,0 +1,29 @@
/*
*
* * Copyright © 2022-2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.utils
// stores screenshot path
object ScreenShotStorageHelper {
var images = ArrayList<String>()
fun addImage(image: String) {
images.add(image)
}
fun clearAll() {
images.clear()
}
fun deleteKItems(k: Int) {
if (k <= images.size) {
images = ArrayList(images.subList(k, images.size))
} else {
images.clear()
}
}
}

View File

@@ -0,0 +1,384 @@
/*
*
* * Copyright © 2022-2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.utils
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.graphics.Canvas
import android.net.ConnectivityManager
import android.os.BatteryManager
import android.os.StatFs
import android.telephony.TelephonyManager
import android.view.MotionEvent
import android.view.View
import com.navi.alfred.utils.ScreenShotStorageHelper
import com.navi.alfred.AlfredManager
import com.navi.alfred.db.AlfredDatabaseHelper
import com.navi.alfred.db.model.ScreenShotPathHelper
import com.navi.alfred.model.NaviMotionEvent
import com.navi.alfred.utils.AlfredConstants.IMAGE_FILE_EXTENSION
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.*
import java.lang.Math.subtractExact
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.math.abs
suspend fun captureScreen(
v: View,
context: Context,
bottomSheetFlow: Boolean? = false,
screenName: String? = null,
scope: CoroutineScope,
canvas: Canvas? = null,
bmp: Bitmap? = null,
moduleName: String? = null
): Bitmap? {
if (isScreenDisabled(screenName, moduleName)) {
return null
}
if (canvas != null && bmp != null) {
withContext(Dispatchers.Main) {
try {
v.draw(canvas)
} catch (e: Exception) {
e.log()
}
}
insertScreenShotPathInDb(scope, context, bmp, bottomSheetFlow)
}
return bmp
}
fun combineScreenshots(
backgroundScreenshot: Bitmap?,
bottomSheetScreenshot: Bitmap?,
context: Context,
screenName: String? = null,
moduleName: String? = null,
scope: CoroutineScope
): Bitmap? {
if (backgroundScreenshot == null || bottomSheetScreenshot == null || isScreenDisabled(
screenName, moduleName
)
) {
return null
}
var screenWidth = Resources.getSystem().displayMetrics.widthPixels / 2
var screenHeight = Resources.getSystem().displayMetrics.heightPixels / 2
if (!isResolutionEven(screenWidth, screenHeight)) {
screenHeight += 1
screenWidth += 1
}
val combinedBitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(combinedBitmap)
canvas.drawBitmap(backgroundScreenshot, 0f, 0f, null)
var bottomSheetLeft = (screenWidth - bottomSheetScreenshot.width) / 2f
var bottomSheetTop = screenHeight - bottomSheetScreenshot.height.toFloat()
if (!isResolutionEven(bottomSheetLeft.toInt(), bottomSheetTop.toInt())) {
bottomSheetLeft = (bottomSheetLeft.toInt() + 1).toFloat()
bottomSheetTop = (bottomSheetTop.toInt() + 1).toFloat()
}
canvas.drawBitmap(bottomSheetScreenshot, bottomSheetLeft, bottomSheetTop, null)
insertScreenShotPathInDb(scope, context, combinedBitmap)
return combinedBitmap
}
fun insertScreenShotPathInDb(
scope: CoroutineScope, context: Context, bitmap: Bitmap?, bottomSheetFlow: Boolean? = false
) {
scope.launch(Dispatchers.IO) {
try {
val fileDir = context.filesDir
val fileName = System.currentTimeMillis().toString() + IMAGE_FILE_EXTENSION
val path = fileDir.path.plus("/").plus(fileName)
val imageUrl = File(path)
val fos = FileOutputStream(
imageUrl
)
val videoQuality: Int = when (AlfredManager.config.getVideoQuality()) {
VideoQuality.HIGH.name -> {
10
}
VideoQuality.LOW.name -> {
6
}
VideoQuality.MEDIUM.name -> {
8
}
else -> {
10
}
}
bitmap?.compress(CompressFormat.JPEG, videoQuality, fos)
fos.flush()
fos.close()
if (bottomSheetFlow == false) {
val db = AlfredDatabaseHelper.getAnalyticsDatabase(context)
val screenShotDao = db.screenShotDao()
try {
screenShotDao.insertScreenShotPath(
ScreenShotPathHelper(
System.currentTimeMillis(), path
)
)
ScreenShotStorageHelper.addImage(path)
} catch (e: Exception) {
e.printStackTrace()
}
}
} catch (e: Exception) {
e.log()
} catch (e: IOException) {
e.log()
}
}
}
fun captureScreenshotOfCustomView(
view: View
): Bitmap? {
view.draw(Canvas())
val bitmapForCanvas = createBitmapForView(view)
try {
view.draw(bitmapForCanvas?.first)
} catch (e: Exception) {
e.log()
}
return bitmapForCanvas?.second
}
fun createBitmapForView(view: View): Pair<Canvas, Bitmap>? {
var width = view.width / 2
var height = view.height / 2
if (width > 0 && height > 0) {
if (!isResolutionEven(width, height)) {
width += 1
height += 1
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.scale(0.5f, 0.5f)
return Pair(canvas, bitmap)
}
return null
}
fun zip(_files: ArrayList<String>, zipFilePath: String?): Boolean? {
val buffer = 1000
try {
zipFilePath?.let { zipFile ->
File(zipFile)
}
var origin: BufferedInputStream? = null
val dest = FileOutputStream(zipFilePath)
val out = ZipOutputStream(BufferedOutputStream(dest))
val data = ByteArray(buffer)
for (i in _files.indices) {
try {
val fi = FileInputStream(_files[i])
origin = BufferedInputStream(fi, buffer)
val entry = ZipEntry(_files[i].substring(_files[i].lastIndexOf("/") + 1))
out.putNextEntry(entry)
var count: Int
while (origin.read(data, 0, buffer).also { count = it } != -1) {
out.write(data, 0, count)
}
origin.close()
} catch (e: Exception) {
e.log()
}
}
out.close()
return true
} catch (e: java.lang.Exception) {
e.log()
}
return null
}
fun clearScreenShot(path: List<ScreenShotPathHelper>): Boolean {
path.forEach { data ->
val file = data.screenShotPath?.let { File(it) }
if (file?.exists() == true) {
file.delete()
}
}
return true
}
fun isScreenDisabled(screenName: String? = null, moduleName: String? = null): Boolean {
if (moduleName != null && AlfredManager.config.getDisableModuleList()
?.contains(moduleName) == true
) {
return true
} else {
if (screenName != null && AlfredManager.config.getDisableScreenList()
?.contains(screenName) == true
) {
return true
}
}
return false
}
fun checkFileExists(fileName: String, context: Context): File? {
val file = File(context.filesDir, fileName)
return if (file.exists()) {
file
} else {
null
}
}
fun getTouchEvent(
currentTouchEvent: MotionEvent?, previousTouchEvent: NaviMotionEvent?
): Pair<String, HashMap<String, String>> {
val properties = java.util.HashMap<String, String>()
val eventName = if (difference(currentTouchEvent?.rawX, previousTouchEvent?.positionX) && difference(
currentTouchEvent?.rawY, previousTouchEvent?.positionY
)
) {
properties[AlfredConstants.START_X] = previousTouchEvent?.positionX.toString()
properties[AlfredConstants.START_Y] = previousTouchEvent?.positionY.toString()
AlfredConstants.TOUCH_EVENT
} else {
properties[AlfredConstants.START_X] = previousTouchEvent?.positionX.toString()
properties[AlfredConstants.START_Y] = previousTouchEvent?.positionY.toString()
properties[AlfredConstants.END_X] = currentTouchEvent?.rawX.toString()
properties[AlfredConstants.END_Y] = currentTouchEvent?.rawY.toString()
AlfredConstants.SCROLL_EVENT
}
return Pair(eventName, properties)
}
fun difference(value1: Float? = 0F, value2: Float? = 0F, threshold: Int = 100): Boolean {
return abs(subtractExact(value1?.toInt().orZero(), value2?.toInt().orZero())) <= threshold
}
@SuppressLint("MissingPermission")
fun getNetworkType(context: Context): String {
try {
val connManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val mWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
if (mWifi != null && mWifi.isConnected) return "Wifi"
val mTelephonyManager =
context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
return when (mTelephonyManager.networkType) {
TelephonyManager.NETWORK_TYPE_GPRS, TelephonyManager.NETWORK_TYPE_EDGE, TelephonyManager.NETWORK_TYPE_CDMA, TelephonyManager.NETWORK_TYPE_1xRTT, TelephonyManager.NETWORK_TYPE_IDEN -> "2G"
TelephonyManager.NETWORK_TYPE_UMTS, TelephonyManager.NETWORK_TYPE_EVDO_0, TelephonyManager.NETWORK_TYPE_EVDO_A, TelephonyManager.NETWORK_TYPE_HSDPA, TelephonyManager.NETWORK_TYPE_HSUPA, TelephonyManager.NETWORK_TYPE_HSPA, TelephonyManager.NETWORK_TYPE_EVDO_B, TelephonyManager.NETWORK_TYPE_EHRPD, TelephonyManager.NETWORK_TYPE_HSPAP -> "3G"
TelephonyManager.NETWORK_TYPE_LTE -> "4G"
TelephonyManager.NETWORK_TYPE_NR -> "5G"
else -> "Unknown" + AlfredConstants.UNDERSCORE + mTelephonyManager.networkType
}
} catch (e: Exception) {
}
return "Unknown"
}
fun getCarrierName(context: Context): String? {
return try {
(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager).networkOperatorName
} catch (e: Exception) {
null
}
}
fun Int?.orZero() = this ?: 0
fun Float?.orZero() = this ?: 0F
fun getBatteryPercentage(context: Context): Float {
val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY).toFloat()
}
fun getMemoryUsage(): Float {
val runtime = Runtime.getRuntime()
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
return usedMemory / (1024f * 1024f)
}
fun getCpuUsage(): Float {
val command = "top -n 1 -d 1"
val process = Runtime.getRuntime().exec(command)
val reader = BufferedReader(InputStreamReader(process.inputStream))
var line: String? = reader.readLine()
var cpuUsage = 0f
while (line != null) {
if (line.contains("% cpu")) {
val regex = "\\s+".toRegex()
val tokens = regex.split(line)
cpuUsage = tokens[0].toFloat()
break
}
line = reader.readLine()
}
reader.close()
return cpuUsage
}
fun getStorageUsage(context: Context): Pair<Long, Long> {
val path = context.filesDir.path
val stat = StatFs(path)
val blockSize = stat.blockSizeLong
val totalBlocks = stat.blockCountLong
val freeBlocks = stat.availableBlocksLong
val totalSize = blockSize * totalBlocks
val freeSize = blockSize * freeBlocks
return Pair(totalSize, freeSize)
}
enum class VideoQuality {
LOW, HIGH, MEDIUM
}
private fun Any.tag(): String {
return try {
this::class.java.simpleName.ifEmpty { "Empty" }
} catch (e: Exception) {
"EmptyTag"
}
}
private fun isResolutionEven(width: Int, height: Int): Boolean {
if (width.mod(2) == 0 && height.mod(2) == 0) {
return true
}
return false
}
fun java.lang.Exception.log() {
if (AlfredManager.config.isQa()) {
Timber.tag(tag()).e(this)
}
AlfredHelper.recordException(this)
}
fun getScreenWidth(): Int {
return Resources.getSystem().displayMetrics.widthPixels
}
fun getScreenHeight(): Int {
return Resources.getSystem().displayMetrics.heightPixels
}

View File

@@ -0,0 +1,54 @@
/*
*
* * Copyright © 2022-2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.worker
import android.content.Context
import androidx.annotation.WorkerThread
import com.navi.alfred.AlfredManager
import com.navi.alfred.db.AlfredDatabaseHelper
import com.navi.alfred.db.model.AnalyticsEvent
import com.navi.alfred.dispatcher.AlfredDispatcher
import com.navi.alfred.utils.AlfredConstants
import java.util.concurrent.atomic.AtomicInteger
class AddEventTask(
private val event: AnalyticsEvent,
private val context: Context
) : AnalyticsTask {
companion object {
private var eventCount: AtomicInteger = AtomicInteger(0)
fun resetEventCount() {
eventCount = AtomicInteger(0)
}
}
@WorkerThread
override fun execute(): Boolean {
val db = AlfredDatabaseHelper.getAnalyticsDatabase(context)
val analyticsDao = db.analyticsDao()
return try {
analyticsDao.insertEvent(event)
eventCount.incrementAndGet()
checkIfSyncIsNeeded()
true
} catch (e: Exception) {
false
}
}
private fun checkIfSyncIsNeeded() {
if (eventCount.get() >= AlfredManager.config.getEventBatchSize()) {
resetEventCount()
AlfredDispatcher.executeInNewThread(SyncDataTask(context))
}
}
override fun getTaskName(): String {
return AlfredConstants.ADD_EVENT_TASK
}
}

View File

@@ -0,0 +1,47 @@
package com.navi.alfred.worker
import android.content.Context
import androidx.annotation.WorkerThread
import com.navi.alfred.AlfredManager
import com.navi.alfred.db.AlfredDatabaseHelper
import com.navi.alfred.db.model.ApiMetricHelper
import com.navi.alfred.dispatcher.AlfredDispatcher
import com.navi.alfred.utils.AlfredConstants
import java.util.concurrent.atomic.AtomicInteger
class AddMetricTask(
private val event: ApiMetricHelper, private val context: Context
) : AnalyticsTask {
companion object {
private var eventCount: AtomicInteger = AtomicInteger(0)
fun resetEventCount() {
eventCount = AtomicInteger(0)
}
}
@WorkerThread
override fun execute(): Boolean {
val db = AlfredDatabaseHelper.getAnalyticsDatabase(context)
val analyticsDao = db.apiMetricDao()
return try {
analyticsDao.insert(event)
eventCount.incrementAndGet()
checkIfSyncIsNeeded()
true
} catch (e: Exception) {
false
}
}
private fun checkIfSyncIsNeeded() {
if (eventCount.get() >= AlfredManager.config.getEventBatchSize()) {
resetEventCount()
AlfredDispatcher.executeInNewThread(SyncDataTask(context))
}
}
override fun getTaskName(): String {
return AlfredConstants.ADD_API_METRIC_TASK
}
}

View File

@@ -0,0 +1,17 @@
/*
*
* * Copyright © 2019-2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.worker
import androidx.annotation.WorkerThread
interface AnalyticsTask {
@WorkerThread
fun execute(): Boolean
fun getTaskName(): String
}

View File

@@ -0,0 +1,65 @@
/*
*
* * Copyright © 2019-2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.worker
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.BlockingDeque
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingDeque
class AnalyticsTaskProcessor {
private val taskQueue: BlockingDeque<AnalyticsTask> = LinkedBlockingDeque<AnalyticsTask>()
private val coroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val singleThreadExecutorForSync = Executors.newSingleThreadExecutor()
private var active: AnalyticsTask? = null
private var isSyncEventTaskExecuted: Boolean = true
fun addTask(task: AnalyticsTask?) {
if (task != null) {
taskQueue.add(task)
startExecution()
}
}
fun addTaskToFront(task: AnalyticsTask?) {
if (task != null) {
taskQueue.addFirst(task)
startExecution()
}
}
private fun startExecution() {
if (active == null) {
scheduleNext()
}
}
private fun scheduleNext() {
if (taskQueue.poll().also { active = it } != null) {
coroutineDispatcher.executor.execute {
active?.let {
executeTask(it)
scheduleNext()
}
}
}
}
private fun executeTask(task: AnalyticsTask) {
task.execute()
}
fun executeInNewThread(task: AnalyticsTask) {
if (isSyncEventTaskExecuted) {
isSyncEventTaskExecuted = false
singleThreadExecutorForSync.execute {
task.execute()
isSyncEventTaskExecuted = true
}
}
}
}

View File

@@ -0,0 +1,28 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.alfred.worker
import android.content.Context
import androidx.annotation.WorkerThread
import com.navi.alfred.AlfredManager
import com.navi.alfred.utils.AlfredConstants.SYNC_EVENT_TASK
import kotlinx.coroutines.runBlocking
class SyncDataTask(private val context: Context) : AnalyticsTask {
@WorkerThread
override fun execute(): Boolean {
return runBlocking {
AlfredManager.sendIngestMetric()
AlfredManager.sendEventsToServer(context)
}
}
override fun getTaskName(): String {
return SYNC_EVENT_TASK
}
}

View File

@@ -0,0 +1,52 @@
package com.navi.alfred.worker
import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.alfred.AlfredManager
import com.navi.alfred.db.model.ScreenShotPathHelper
import com.navi.alfred.utils.AlfredConstants
import com.navi.alfred.utils.checkFileExists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.lang.reflect.Type
class UploadFileWorker(
context: Context, workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
val alfredSessionId = inputData.getString(AlfredConstants.ALFRED_SESSION_ID)
val sessionStartRecordingTime =
inputData.getLong(AlfredConstants.SESSION_START_RECORDING_TIME, 0L)
val eventStartRecordingTime =
inputData.getLong(AlfredConstants.EVENT_START_RECORDING_TIME, 0L)
val screenShotList = inputData.getString(AlfredConstants.SCREENSHOT_LIST)
val listType: Type = object : TypeToken<List<ScreenShotPathHelper?>?>() {}.type
val screenShots: List<ScreenShotPathHelper> =
Gson().fromJson(screenShotList.toString(), listType)
val zipFileName = alfredSessionId + sessionStartRecordingTime.toString()
try {
if (alfredSessionId == null || sessionStartRecordingTime == 0L || eventStartRecordingTime == 0L) {
handleWorkFailure(zipFileName)
Result.failure()
} else {
AlfredManager.toZipForWorkManager(screenShots, zipFileName,sessionStartRecordingTime,alfredSessionId, eventStartRecordingTime)
Result.success()
}
} catch (e: Exception) {
e.printStackTrace()
handleWorkFailure(zipFileName)
Result.failure()
}
}
private fun handleWorkFailure(zipFileName:String) {
checkFileExists(zipFileName, context = applicationContext)?.delete()
}
}