diff --git a/.idea/gradle.xml b/.idea/gradle.xml index a9f4e52..89a8f68 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,5 +1,6 @@ + diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cb..9f71c83 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 5808e47..39a737d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } \ No newline at end of file diff --git a/app/src/main/java/com/alfred/demo/MainActivity.kt b/app/src/main/java/com/alfred/demo/MainActivity.kt deleted file mode 100644 index 59334d4..0000000 --- a/app/src/main/java/com/alfred/demo/MainActivity.kt +++ /dev/null @@ -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") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/alfred/demo/ui/theme/Color.kt b/app/src/main/java/com/alfred/demo/ui/theme/Color.kt deleted file mode 100644 index 6f8b2f4..0000000 --- a/app/src/main/java/com/alfred/demo/ui/theme/Color.kt +++ /dev/null @@ -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) \ No newline at end of file diff --git a/app/src/main/java/com/alfred/demo/ui/theme/Theme.kt b/app/src/main/java/com/alfred/demo/ui/theme/Theme.kt deleted file mode 100644 index 6d83842..0000000 --- a/app/src/main/java/com/alfred/demo/ui/theme/Theme.kt +++ /dev/null @@ -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 - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/alfred/demo/ui/theme/Type.kt b/app/src/main/java/com/alfred/demo/ui/theme/Type.kt deleted file mode 100644 index 8260204..0000000 --- a/app/src/main/java/com/alfred/demo/ui/theme/Type.kt +++ /dev/null @@ -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 - ) - */ -) \ No newline at end of file diff --git a/build.gradle b/build.gradle index 906770c..644234d 100644 --- a/build.gradle +++ b/build.gradle @@ -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 } \ No newline at end of file diff --git a/navi-alfred/build.gradle b/navi-alfred/build.gradle index c775571..2e4bf8e 100644 --- a/navi-alfred/build.gradle +++ b/navi-alfred/build.gradle @@ -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" } \ No newline at end of file diff --git a/navi-alfred/src/main/AndroidManifest.xml b/navi-alfred/src/main/AndroidManifest.xml index a5918e6..4f26cc5 100644 --- a/navi-alfred/src/main/AndroidManifest.xml +++ b/navi-alfred/src/main/AndroidManifest.xml @@ -1,4 +1,9 @@ + + + + + \ No newline at end of file diff --git a/navi-alfred/src/main/java/com/navi/alfred/AlfredConfig.kt b/navi-alfred/src/main/java/com/navi/alfred/AlfredConfig.kt new file mode 100644 index 0000000..3d13eda --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/AlfredConfig.kt @@ -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? = null, + private var disableModuleList: List? = 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) { + this.disableScreenList = disableScreenList + } + + fun setDisableModuleList(disableModuleList: List) { + this.disableModuleList = disableModuleList + } + + fun getDisableModuleList(): List? = 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? = 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 + } + +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/AlfredManager.kt b/navi-alfred/src/main/java/com/navi/alfred/AlfredManager.kt new file mode 100644 index 0000000..24221ac --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/AlfredManager.kt @@ -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 + 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? = 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() + .setConstraints(constraints) + .setInputData(requestData) + .build() + WorkManager.getInstance(applicationContext) + .beginUniqueWork(uniqueWorkName, ExistingWorkPolicy.REPLACE, uniqueWorkRequest) + .enqueue() + } else { + val workRequest = OneTimeWorkRequestBuilder() + .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 = + 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?>() {}.type + val events: ArrayList = + 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?>() {}.type + val events: ArrayList = + 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 = 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, + zipFileName: String, + sessionStartRecordingTime: Long? = null, + alfredSessionId: String? = null, + eventStartRecordingTime: Long? = null, + index: Int? = null + ) { + sessionIdForCrash = alfredSessionId + sessionStartRecordingTimeForCrash = sessionStartRecordingTime + eventStartRecordingTimeForCrash = eventStartRecordingTime + val fileList = ArrayList() + 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 + ) { + val zipFilePath: String = + applicationContext.filesDir.path + "/" + config.getAlfredSessionId() + config.getSessionStartRecordingTime() + val fileList = ArrayList() + 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, anrView: View, screenName: String? = null + ) { + startAnrCrashZipUpload(anrView) + coroutineDispatcher.executor.execute { + val event = buildEvent( + AlfredConstants.ANR_EVENT, anrEventProperties as HashMap, + screenName = screenName, + moduleName = currentModuleName + ) + AlfredDispatcher.addTaskToQueue(AddEventTask(event, this.applicationContext)) + } + } + + fun handleSWWEvent( + screenName: String? = null, swwEventProperties: Map + ) { + coroutineDispatcher.executor.execute { + val event = buildEvent( + AlfredConstants.ERROR_LOG, + swwEventProperties as HashMap, + screenName = screenName, + moduleName = currentModuleName + ) + AlfredDispatcher.addTaskToQueue(AddEventTask(event, applicationContext)) + } + } + + fun handleCrashEvent( + crashEventProperties: Map, + crashView: View, + screenName: String? = null + ) { + startAnrCrashZipUpload(crashView) + coroutineDispatcher.executor.execute { + val event = buildEvent( + AlfredConstants.CRASH_ANALYTICS_EVENT, + crashEventProperties as HashMap, + screenName = screenName, + moduleName = currentModuleName + ) + AlfredDispatcher.addTaskToQueue(AddEventTask(event, applicationContext)) + } + } + + @WorkerThread + fun buildEvent( + eventName: String, + properties: HashMap? = 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? = 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)) + } + +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/db/AlfredDatabase.kt b/navi-alfred/src/main/java/com/navi/alfred/db/AlfredDatabase.kt new file mode 100644 index 0000000..9f6b06a --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/db/AlfredDatabase.kt @@ -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 +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/db/AlfredDatabaseHelper.kt b/navi-alfred/src/main/java/com/navi/alfred/db/AlfredDatabaseHelper.kt new file mode 100644 index 0000000..c480370 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/db/AlfredDatabaseHelper.kt @@ -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 + } + } + +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/db/dao/AlfredDao.kt b/navi-alfred/src/main/java/com/navi/alfred/db/dao/AlfredDao.kt new file mode 100644 index 0000000..40e80ce --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/db/dao/AlfredDao.kt @@ -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 + + @Query("DELETE FROM AnalyticsEvent WHERE eventId IN (:idList)") + fun deleteEvents(idList: List) + + @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 + + @Query("SELECT * FROM ScreenShotPathHelper") + fun fetchAllScreenShotsPath(): List + + @Query("DELETE FROM ScreenShotPathHelper WHERE id IN (:idList)") + fun deleteScreenShot(idList: List) + + @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 + + @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 + + @Query("DELETE FROM ApiMetricHelper WHERE id IN (:idList)") + fun deleteApiMetric(idList: List) +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/db/model/AlfredEntity.kt b/navi-alfred/src/main/java/com/navi/alfred/db/model/AlfredEntity.kt new file mode 100644 index 0000000..e4f5a99 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/db/model/AlfredEntity.kt @@ -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) + } + +} + + + diff --git a/navi-alfred/src/main/java/com/navi/alfred/deserializer/AnalyticsDataDeserializer.kt b/navi-alfred/src/main/java/com/navi/alfred/deserializer/AnalyticsDataDeserializer.kt new file mode 100644 index 0000000..4fb67b0 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/deserializer/AnalyticsDataDeserializer.kt @@ -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 { + override fun deserialize( + json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext? + ): AnalyticsRequest? { + json?.let { + return context?.deserialize(json, AnalyticsRequest::class.java) + } + return null + } +} \ No newline at end of file diff --git a/navi-alfred/src/main/java/com/navi/alfred/deserializer/MetricsDataDeserializer.kt b/navi-alfred/src/main/java/com/navi/alfred/deserializer/MetricsDataDeserializer.kt new file mode 100644 index 0000000..4cbe4cb --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/deserializer/MetricsDataDeserializer.kt @@ -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 { + override fun deserialize( + json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext? + ): EventMetricRequest? { + json?.let { + return context?.deserialize(json, EventMetricRequest::class.java) + } + return null + } +} \ No newline at end of file diff --git a/navi-alfred/src/main/java/com/navi/alfred/deserializer/SessionDataDeserializer.kt b/navi-alfred/src/main/java/com/navi/alfred/deserializer/SessionDataDeserializer.kt new file mode 100644 index 0000000..fb1d72a --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/deserializer/SessionDataDeserializer.kt @@ -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 { + override fun deserialize( + json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext? + ): SessionRequest? { + json?.let { + return context?.deserialize(json, EventMetricRequest::class.java) + } + return null + } +} \ No newline at end of file diff --git a/navi-alfred/src/main/java/com/navi/alfred/dispatcher/AlfredDispatcher.kt b/navi-alfred/src/main/java/com/navi/alfred/dispatcher/AlfredDispatcher.kt new file mode 100644 index 0000000..a55efcf --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/dispatcher/AlfredDispatcher.kt @@ -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) + } +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/model/EventMetricRequest.kt b/navi-alfred/src/main/java/com/navi/alfred/model/EventMetricRequest.kt new file mode 100644 index 0000000..e322086 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/model/EventMetricRequest.kt @@ -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 +) + +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? = null +) + + diff --git a/navi-alfred/src/main/java/com/navi/alfred/model/NaviMotionEvent.kt b/navi-alfred/src/main/java/com/navi/alfred/model/NaviMotionEvent.kt new file mode 100644 index 0000000..e2bf4ec --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/model/NaviMotionEvent.kt @@ -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 +) diff --git a/navi-alfred/src/main/java/com/navi/alfred/model/SessionRequest.kt b/navi-alfred/src/main/java/com/navi/alfred/model/SessionRequest.kt new file mode 100644 index 0000000..2f5fca8 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/model/SessionRequest.kt @@ -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? = 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? = 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, +) \ No newline at end of file diff --git a/navi-alfred/src/main/java/com/navi/alfred/network/AlfredNetworkRepository.kt b/navi-alfred/src/main/java/com/navi/alfred/network/AlfredNetworkRepository.kt new file mode 100644 index 0000000..b66c439 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/network/AlfredNetworkRepository.kt @@ -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 { + return AlfredRetrofitProvider.getApiService() + .sendEvents(url, "application/json", ALFRED, analyticsRequest) + } + + suspend fun getPreSignedUrl(sessionId: String): Response { + return AlfredRetrofitProvider.getApiService().getPreSignedUrl(sessionId, ALFRED) + } + + suspend fun sendSession( + url: String, + sessionRequest: SessionRequest + ): Response { + return AlfredRetrofitProvider.getApiService() + .sendSession(url, "application/json", ALFRED, sessionRequest) + } + + suspend fun uploadZipToS3( + preSignedUrl: String, request: RequestBody + ): Response { + return AlfredRetrofitProvider.getApiService().uploadToS3(preSignedUrl, request, ALFRED) + } + + suspend fun eventMetric( + url: String, metricRequestBody: EventMetricRequest + ): Response { + return AlfredRetrofitProvider.getApiService() + .sendMetric(url, ALFRED, "application/json", metricRequestBody) + } + + suspend fun cruiseConfig( + url: String, appVersionName: String, osVersionCode: String, deviceId: String + ): Response { + return AlfredRetrofitProvider.getApiService().getCruiseConfig( + url, + ALFRED, + contentType = "application/json", + appVersionName = appVersionName, + osVersion = osVersionCode, + deviceId = deviceId + ) + } +} \ No newline at end of file diff --git a/navi-alfred/src/main/java/com/navi/alfred/network/AlfredRetrofitProvider.kt b/navi-alfred/src/main/java/com/navi/alfred/network/AlfredRetrofitProvider.kt new file mode 100644 index 0000000..4505feb --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/network/AlfredRetrofitProvider.kt @@ -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) + } +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/network/AlfredRetrofitService.kt b/navi-alfred/src/main/java/com/navi/alfred/network/AlfredRetrofitService.kt new file mode 100644 index 0000000..9b82778 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/network/AlfredRetrofitService.kt @@ -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 + + @GET("ingest/session/pre-sign/{sessionId}") + suspend fun getPreSignedUrl( + @Path(SESSION_ID) sessionId: String, + @Header(X_TARGET) target: String, + ): Response + + @PUT + suspend fun uploadToS3( + @Url url: String, + @Body file: RequestBody, + @Header(X_TARGET) target: String, + ): Response + + @POST + suspend fun sendSession( + @Url url: String, + @Header(CONTENT_TYPE) contentType: String, + @Header(X_TARGET) target: String, + @Body sessionRequest: SessionRequest + ): Response + + @POST + suspend fun sendMetric( + @Url url: String, + @Header(X_TARGET) target: String, + @Header(CONTENT_TYPE) contentType: String, + @Body metricRequestBody: EventMetricRequest + ): Response + + @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 +} \ No newline at end of file diff --git a/navi-alfred/src/main/java/com/navi/alfred/network/model/AnalyticsResponse.kt b/navi-alfred/src/main/java/com/navi/alfred/network/model/AnalyticsResponse.kt new file mode 100644 index 0000000..d69745e --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/network/model/AnalyticsResponse.kt @@ -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 +) diff --git a/navi-alfred/src/main/java/com/navi/alfred/network/model/CruiseResponse.kt b/navi-alfred/src/main/java/com/navi/alfred/network/model/CruiseResponse.kt new file mode 100644 index 0000000..6bade16 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/network/model/CruiseResponse.kt @@ -0,0 +1,44 @@ +package com.navi.alfred.network.model + +import com.google.gson.annotations.SerializedName + +data class CruiseResponse( + @SerializedName("data") val data: List, @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? = null, + @SerializedName("disable_modules") val disableModules: List? = 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 +) \ No newline at end of file diff --git a/navi-alfred/src/main/java/com/navi/alfred/utils/AlfredConstants.kt b/navi-alfred/src/main/java/com/navi/alfred/utils/AlfredConstants.kt new file mode 100644 index 0000000..cdc0099 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/utils/AlfredConstants.kt @@ -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" + +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/utils/AlfredHelper.kt b/navi-alfred/src/main/java/com/navi/alfred/utils/AlfredHelper.kt new file mode 100644 index 0000000..c7f2584 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/utils/AlfredHelper.kt @@ -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) + } +} \ No newline at end of file diff --git a/navi-alfred/src/main/java/com/navi/alfred/utils/ScreenShotStorageHelper.kt b/navi-alfred/src/main/java/com/navi/alfred/utils/ScreenShotStorageHelper.kt new file mode 100644 index 0000000..3d9a16e --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/utils/ScreenShotStorageHelper.kt @@ -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() + + 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() + } + } +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/utils/Utils.kt b/navi-alfred/src/main/java/com/navi/alfred/utils/Utils.kt new file mode 100644 index 0000000..d2e6a77 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/utils/Utils.kt @@ -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? { + 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, 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): 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> { + val properties = java.util.HashMap() + 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 { + 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 +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/worker/AddEventTask.kt b/navi-alfred/src/main/java/com/navi/alfred/worker/AddEventTask.kt new file mode 100644 index 0000000..7a6947d --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/worker/AddEventTask.kt @@ -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 + } +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/worker/AddMetricTask.kt b/navi-alfred/src/main/java/com/navi/alfred/worker/AddMetricTask.kt new file mode 100644 index 0000000..d053682 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/worker/AddMetricTask.kt @@ -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 + } +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/worker/AnalyticsTask.kt b/navi-alfred/src/main/java/com/navi/alfred/worker/AnalyticsTask.kt new file mode 100644 index 0000000..72db335 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/worker/AnalyticsTask.kt @@ -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 +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/worker/AnalyticsTaskProcessor.kt b/navi-alfred/src/main/java/com/navi/alfred/worker/AnalyticsTaskProcessor.kt new file mode 100644 index 0000000..aaec494 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/worker/AnalyticsTaskProcessor.kt @@ -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 = LinkedBlockingDeque() + 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 + } + } + } +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/worker/SyncDataTask.kt b/navi-alfred/src/main/java/com/navi/alfred/worker/SyncDataTask.kt new file mode 100644 index 0000000..de4c279 --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/worker/SyncDataTask.kt @@ -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 + } +} diff --git a/navi-alfred/src/main/java/com/navi/alfred/worker/UploadFileWorker.kt b/navi-alfred/src/main/java/com/navi/alfred/worker/UploadFileWorker.kt new file mode 100644 index 0000000..95888fc --- /dev/null +++ b/navi-alfred/src/main/java/com/navi/alfred/worker/UploadFileWorker.kt @@ -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?>() {}.type + val screenShots: List = + 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() + } +} +