Co-authored-by: Aman <amankasyapp@gmail.com>
Co-authored-by: Shivam Goyal <shivam.goyal@navi.com>
This commit is contained in:
Aman S
2024-01-19 16:53:06 +05:30
committed by GitHub
parent b897f11dd4
commit 44c311e2e4
18 changed files with 1307 additions and 422 deletions

View File

@@ -26,8 +26,7 @@ import com.navi.alfred.utils.log
import com.navi.analytics.utils.NaviTrackEvent
import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.AppLaunchUtils
import com.navi.base.utils.QaReleaseLogUtil
import com.navi.base.utils.QaReleaseLogUtil.buildQaReleaseLogMessage
import com.navi.base.utils.NetWatchManger
import com.navi.chat.base.ChatBaseActivity
import com.navi.common.ui.activity.BaseActivity
import com.navi.common.uitron.util.UiTronDependencyProvider
@@ -50,8 +49,10 @@ import com.naviapp.analytics.utils.NaviSDKHelper
import com.naviapp.common.transformer.AppLoadTimerMapper
import com.naviapp.home.activity.NewDashboardActivity
import com.naviapp.home.common.setup.NotificationManager
import com.naviapp.releaselog.utils.NetWatchUtil
import com.naviapp.utils.Constants
import com.naviapp.utils.DEV
import com.naviapp.utils.QA
import com.naviapp.utils.isDifferentPackage
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
@@ -103,8 +104,12 @@ open class NaviApplication : MultiDexApplication(), Application.ActivityLifecycl
PreferenceManager.init(this)
NaviSDKHelper.init(naviApplication = this)
registerActivityLifecycleCallbacks(this)
QaReleaseLogUtil.init(context = applicationContext, flavour = BuildConfig.FLAVOR)
if (TextUtils.equals(BuildConfig.FLAVOR, QA) && !BuildConfig.DEBUG) {
NetWatchManger.init(
context = applicationContext,
flavour = BuildConfig.FLAVOR
)
}
// Dumping anr data to click stream
ANRWatchDog(WATCHDOG_ANR_TIMEOUT).setIgnoreDebugger(true).setReportMainThreadOnly().setANRListener {
if (it.cause?.stackTrace.isNullOrEmpty()) {
@@ -127,10 +132,6 @@ open class NaviApplication : MultiDexApplication(), Application.ActivityLifecycl
anrEventProperties
)
buildQaReleaseLogMessage(
data = anrEventProperties,
logType = QaReleaseLogUtil.ReleaseLogType.ANR_LOG.name
)
if (isDifferentPackage.not()) {
anrEventProperties[STACK_TRACE] = it.cause?.stackTrace?.get(0).toString()
AlfredManager.handleAnrEvent(anrEventProperties)
@@ -155,10 +156,6 @@ open class NaviApplication : MultiDexApplication(), Application.ActivityLifecycl
NaviTrackEvent.trackEventOnClickStream(
GLOBAL_APP_CRASH, crashEventProperties
)
buildQaReleaseLogMessage(
data = crashEventProperties,
logType = QaReleaseLogUtil.ReleaseLogType.CRASH_LOG.name
)
if (isDifferentPackage.not()) {
exception.stackTrace[0]?.let { stackTraceElement ->
crashEventProperties[STACK_TRACE] = stackTraceElement.toString()
@@ -196,6 +193,9 @@ open class NaviApplication : MultiDexApplication(), Application.ActivityLifecycl
if (appForegroundCounter > 0) {
startAlfredRecording(activity)
if (TextUtils.equals(BuildConfig.FLAVOR, QA) && !BuildConfig.DEBUG){
NetWatchUtil.addUniversalButtonToActivity(activity, applicationContext)
}
}
}

View File

@@ -62,7 +62,6 @@ import com.navi.ap.utils.constants.APP_PLATFORM_VERTICAL_TYPE
import com.navi.ap.utils.constants.CUSTOMER_CAPITAL
import com.navi.ap.utils.constants.REDIRECT_STATUS
import com.navi.base.deeplink.util.DeeplinkConstants
import com.navi.base.deeplink.util.DeeplinkConstants.RELEASE_LOG
import com.navi.base.model.ActionData
import com.navi.base.model.CtaData
import com.navi.base.model.GenericAnalytics
@@ -76,7 +75,6 @@ import com.navi.base.utils.BaseUtils
import com.navi.base.utils.BaseUtils.isUserLoggedIn
import com.navi.base.utils.ConnectivityObserver
import com.navi.base.utils.DateUtils
import com.navi.base.utils.QaReleaseLogUtil.isQaRelease
import com.navi.base.utils.isNotNull
import com.navi.base.utils.isNotNullAndNotEmpty
import com.navi.base.utils.isNull
@@ -1751,19 +1749,6 @@ class NewDashboardActivity :
screen = intent.getStringExtra(Constants.REDIRECT_STATUS) ?: HomeFragment.TAG,
bundle = intent.extras
)
if (isQaRelease) {
binding.profileScreen.btnLogs.apply {
setVisibilityState(View.VISIBLE)
setOnClickListener {
NaviDeepLinkNavigator.navigate(
activity = this@NewDashboardActivity,
ctaData = CtaData(url = RELEASE_LOG)
)
}
}
} else {
binding.profileScreen.btnLogs.apply { setVisibilityState(View.GONE) }
}
}
private fun fetchBottomNavigationBar() {

View File

@@ -8,13 +8,14 @@
package com.naviapp.network.retrofit
import com.navi.common.R as CommonR
import android.text.TextUtils
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.analytics.utils.NaviTrackEvent
import com.navi.base.utils.BaseUtils
import com.navi.base.utils.QaReleaseLogUtil
import com.navi.base.utils.QaReleaseLogUtil.buildQaReleaseLogMessage
import com.navi.base.utils.NetWatchManger
import com.navi.base.utils.NetWatchManger.buildLogMessage
import com.navi.common.CommonLibManager
import com.navi.common.network.ApiConstants
import com.navi.common.network.ApiConstants.API_CODE_ERROR
@@ -29,6 +30,7 @@ import com.navi.common.network.models.ErrorMessage
import com.navi.common.network.models.GenericErrorResponse
import com.navi.common.network.models.GenericResponse
import com.navi.common.network.models.RepoResult
import com.naviapp.BuildConfig
import com.naviapp.R
import com.naviapp.app.NaviApplication
import com.naviapp.models.RedirectPageStatus
@@ -38,9 +40,12 @@ import com.naviapp.network.ApiConstants.API_SUCCESS_CODE
import com.naviapp.network.ApiConstants.API_SUCCESS_CODE_201
import com.naviapp.network.ApiConstants.API_SUCCESS_CODE_204
import com.naviapp.network.ApiConstants.E_OFFER_EXPIRED
import com.naviapp.releaselog.utils.ErrorLog
import com.naviapp.utils.Constants
import com.naviapp.utils.Constants.API_401
import com.naviapp.utils.Constants.LOAN_OFFER_EXPIRED
import com.naviapp.utils.Constants.SYSTEM_UNDER_MAINTENANCE
import com.naviapp.utils.QA
import com.naviapp.utils.handleError
import com.naviapp.utils.isNetworkAvailable
import java.net.ConnectException
@@ -62,14 +67,18 @@ abstract class ResponseCallback {
private fun <T> handleResponse(response: Response<GenericResponse<T>>): RepoResult<T> {
addApiUrlInErrorResponse(response.body()?.errors, response.raw().request.url.toString())
buildQaReleaseLogMessage(
responseData = response.body()?.data,
statusCode = response.body()?.statusCode.toString(),
request = response.raw().request,
logType = QaReleaseLogUtil.ReleaseLogType.NETWORK_LOG.name,
response = response.raw(),
requestMethod = response.raw().request.method
)
if (TextUtils.equals(BuildConfig.FLAVOR, QA) && !BuildConfig.DEBUG) {
buildLogMessage(
responseData = response.body()?.data,
request = response.raw().request,
logType = NetWatchManger.ReleaseLogType.NETWORK_LOG.name,
response = response.raw(),
errorMessage = ErrorLog(
statusCode = response.code().toString(),
message = response.message()
)
)
}
response.body()?.let {
if (it.errors?.firstOrNull()?.code == E_OFFER_EXPIRED) {
handleError(it.errors, RedirectPageStatus(rejectReason = LOAN_OFFER_EXPIRED))

View File

@@ -1,12 +1,21 @@
package com.naviapp.releaselog.activity
import android.os.Build
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.navi.common.model.ModuleNameV2
import com.navi.common.ui.activity.BaseActivity
import com.naviapp.releaselog.screens.ReleaseLogScreen
import com.navi.design.theme.NaviMaterialTheme
import com.naviapp.R
import com.naviapp.releaselog.screens.LogListScreen
import com.naviapp.releaselog.screens.TabScreen
import com.naviapp.releaselog.viewmodel.ReleaseLogViewModel
import dagger.hilt.android.AndroidEntryPoint
@@ -15,10 +24,26 @@ class ReleaseLogActivity : BaseActivity() {
private val viewModel by viewModels<ReleaseLogViewModel>()
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ReleaseLogScreen(LocalContext.current, viewModel)
window.statusBarColor = ContextCompat.getColor(this, R.color.colorBlueSeekbar)
NaviMaterialTheme {
val navController = rememberNavController()
NavHost(
navController = navController, startDestination = "listScreen"
) {
composable("listScreen") {
LogListScreen(LocalContext.current, viewModel, navController)
}
composable(
"tabScreen"
) {
TabScreen(viewModel, navController, context = LocalContext.current)
}
}
}
}
}
@@ -26,6 +51,6 @@ class ReleaseLogActivity : BaseActivity() {
override val moduleName: ModuleNameV2 get() = ModuleNameV2.COMMON
companion object {
const val TAG = "RELEASE_LOG"
const val TAG = "NET_WATCH_LOG"
}
}

View File

@@ -1,15 +1,12 @@
package com.naviapp.releaselog.screens
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -20,41 +17,72 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.input.ImeAction
import android.content.ClipboardManager
import androidx.compose.material.Checkbox
import androidx.compose.material.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat.getSystemService
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.base.db.model.LogsEvent
import com.navi.base.utils.isNotNullAndNotEmpty
import com.navi.design.font.FontWeightEnum
import com.navi.design.theme.getFontWeight
import com.navi.design.theme.ttComposeFontFamily
import com.navi.pay.common.theme.color.NaviPayColor
import com.navi.naviwidgets.R as WidgetsR
import com.naviapp.dashboard.menu.customersupport.screens.NaviErrorScreen
import com.naviapp.dashboard.menu.customersupport.screens.NaviLoadingScreen
import com.naviapp.releaselog.utils.NetWatchUtil.colorKeysAndValues
import com.naviapp.releaselog.viewmodel.ReleaseLogViewModel
import org.json.JSONObject
@Composable
fun ReleaseLogScreen(context: Context, viewModel: ReleaseLogViewModel) {
fun LogListScreen(
context: Context, viewModel: ReleaseLogViewModel, navHostController: NavController
) {
val logsDataState by viewModel.logsData.collectAsState()
val isLogDeleted by viewModel.logsDataDeleted.collectAsState()
LaunchedEffect(Unit) {
viewModel.readLogFile(context = context)
@@ -71,60 +99,69 @@ fun ReleaseLogScreen(context: Context, viewModel: ReleaseLogViewModel) {
is ReleaseLogViewModel.ReleaseLogState.Success -> {
val data = (logsDataState as ReleaseLogViewModel.ReleaseLogState.Success).data
ShowUi(data = data, context = context, viewModel = viewModel)
ShowUi(data, context, viewModel, navHostController)
}
}
if (isLogDeleted) {
ShowUi(emptyList(), context, viewModel, navHostController)
}
}
@Composable
fun ShowUi(
data: List<String>, context: Context, viewModel: ReleaseLogViewModel
data: List<LogsEvent>,
context: Context,
viewModel: ReleaseLogViewModel,
navHostController: NavController
) {
val clipboardManager =
context.getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager
var searchTextState by remember { mutableStateOf("") }
var logsData by remember { mutableStateOf(data) }
Column(
modifier = Modifier
.fillMaxHeight()
.padding(16.dp)
modifier = Modifier.fillMaxHeight()
) {
SearchBar(searchText = searchTextState,
viewModel = viewModel,
context = context,
onSearchTextChanged = { searchQuery ->
searchTextState = searchQuery
if (searchQuery.isEmpty()) {
viewModel.showOriginalLogs()
}
})
TopHeader(onDeleteClick = {
viewModel.deleteLogs(context = context)
logsData = emptyList()
viewModel.rawLogs = emptyList()
}, onSearchClick = { searchText ->
if (searchText.isEmpty()) {
viewModel.readLogFile(context = context)
logsData = emptyList()
}
if (searchText.length >= 3) {
val searchedLogs = viewModel.filterLogs(viewModel.rawLogs, searchText)
viewModel.showSearchedLogs(searchedLogs)
}
}, onRefreshClick = {
Toast.makeText(context, "Refreshing...", Toast.LENGTH_SHORT).show()
viewModel.readLogFile(context = context)
logsData = emptyList()
},
onFilterClick = {
viewModel.readFilteredLogs(context = context, it)
logsData = emptyList()
}
)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(8.dp))
if (data.isNotEmpty()) {
if (logsData.isNotEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Column(
modifier = Modifier.fillMaxHeight()
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
) {
data.forEach { log ->
LogItem(log = log, onLongPress = {
if (log.isNotEmpty()) {
val clipData: ClipData = ClipData.newPlainText("Logs", log)
clipboardManager.setPrimaryClip(clipData)
Toast.makeText(
context, "Copied to Clipboard", Toast.LENGTH_SHORT
).show()
}
})
LogItem(log = log, navHostController, viewModel)
}
}
}
Spacer(modifier = Modifier.height(2.dp))
} else {
Box(
modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center
@@ -135,99 +172,654 @@ fun ShowUi(
}
}
@Composable
fun SearchBar(
searchText: String,
onSearchTextChanged: (String) -> Unit,
viewModel: ReleaseLogViewModel,
context: Context
fun LogItem(log: LogsEvent, navController: NavController, viewModel: ReleaseLogViewModel) {
val statusCodeColor = when {
log.statusCode == "200" -> NaviPayColor.textPrimary
log.statusCode?.startsWith("4") == true -> Color(0xFFECBD48)
log.statusCode?.startsWith("5") == true -> Color.Red
log.statusCode?.startsWith("2") == true && log.statusCode?.length!! < 3 -> Color(0xFFECBD48)
else -> NaviPayColor.textPrimary
}
Row(modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp, horizontal = 16.dp)
.clickable {
viewModel.setSelectedLog(log)
navController.navigate("tabScreen")
}) {
Text(
text = if (log.statusCode != "null") log.statusCode.toString() else " !!! ",
fontSize = 16.sp,
fontFamily = ttComposeFontFamily,
fontWeight = getFontWeight(FontWeightEnum.TT_MEDIUM),
color = statusCodeColor,
modifier = Modifier
.width(40.dp)
.align(Alignment.CenterVertically),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.size(16.dp))
Column(
modifier = Modifier.weight(3f)
) {
Text(
text = log.method.toString() + " " + log.endPoint.toString(),
fontSize = 16.sp,
fontFamily = ttComposeFontFamily,
fontWeight = getFontWeight(FontWeightEnum.TT_MEDIUM),
color = statusCodeColor
)
Text(
modifier = Modifier.fillMaxWidth(),
text = log.baseUrl.toString(),
fontSize = 14.sp,
fontFamily = ttComposeFontFamily,
fontWeight = getFontWeight(FontWeightEnum.TT_REGULAR),
color = statusCodeColor
)
Row {
Text(
modifier = Modifier.weight(1f),
text = log.requestTime.toString(),
fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontFamily = ttComposeFontFamily,
fontWeight = getFontWeight(FontWeightEnum.TT_REGULAR),
color = statusCodeColor
)
Text(
modifier = Modifier.weight(1f),
text = log.latency.toString(),
fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontFamily = ttComposeFontFamily,
fontWeight = getFontWeight(FontWeightEnum.TT_REGULAR),
color = statusCodeColor
)
Text(
modifier = Modifier.weight(1f),
text = log.requestSize.toString(),
fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontFamily = ttComposeFontFamily,
fontWeight = getFontWeight(FontWeightEnum.TT_REGULAR),
color = statusCodeColor
)
}
}
}
Divider(
Modifier
.fillMaxWidth()
.width(1.dp)
)
}
@Composable
fun TabScreenHeader(
title: String,
onBackClick: () -> Unit,
onShareCurlIconClick: () -> Unit,
onShareAsTextIconClick: () -> Unit
) {
Box(
var expanded by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color.Gray)
.padding(8.dp)
.height(30.dp)
.clip(RoundedCornerShape(8.dp))
.height(60.dp)
.background(color = Color(0xFF3A8EE7)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
verticalAlignment = Alignment.CenterVertically
) {
BasicTextField(
value = searchText,
onValueChange = {
if (it != searchText) {
onSearchTextChanged(it)
}
},
textStyle = LocalTextStyle.current.copy(
color = Color.Black, fontSize = 16.sp, letterSpacing = 0.15.sp
),
modifier = Modifier
.weight(1f)
.background(Color.White)
.padding(4.dp),
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = {
if (searchText.length < 3) {
if (searchText.isEmpty()) {
viewModel.showOriginalLogs()
}
Toast.makeText(context, "Min Three Characters", Toast.LENGTH_SHORT).show()
} else {
viewModel.scrollToMatchedText(searchText = searchText)
}
})
IconButton(onClick = {
onBackClick()
}) {
Icon(
painter = painterResource(id = WidgetsR.drawable.ic_black_back_arrow_svg),
contentDescription = "Back"
)
SearchIcon(
imageVector = Icons.Default.Search,
contentDescription = "Search",
onClick = {
if (searchText.length < 3) {
if (searchText.isEmpty()) {
viewModel.showOriginalLogs()
}
Toast.makeText(context, "Min Three Characters", Toast.LENGTH_SHORT).show()
} else {
viewModel.scrollToMatchedText(searchText = searchText)
}
})
}
Text(
text = title,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = Color.White,
textAlign = TextAlign.Center,
maxLines = 2,
modifier = Modifier.weight(1f)
)
Box(
modifier = Modifier
.size(40.dp)
.padding(end = 8.dp),
contentAlignment = Alignment.Center
) {
Icon(painter = painterResource(id = WidgetsR.drawable.ic_share),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.clickable {
expanded = !expanded
})
if (expanded) {
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(onClick = {
expanded = false
onShareCurlIconClick()
}, text = { Text("Share Curl Command") })
DropdownMenuItem(onClick = {
expanded = false
onShareAsTextIconClick()
}, text = { Text("Share as Text") })
}
}
}
}
}
@Composable
fun SearchIcon(
imageVector: ImageVector, contentDescription: String?, onClick: () -> Unit
) {
val density = LocalDensity.current.density
Image(imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier
.clickable { onClick() }
.padding(8.dp)
.size(8.dp * density))
fun TabScreen(viewModel: ReleaseLogViewModel, navController: NavHostController, context: Context) {
val tabTitles = listOf("OVERVIEW", "REQUEST", "RESPONSE")
var selectedTab by remember { mutableIntStateOf(0) }
val logsData by viewModel.selectedLog.collectAsState()
val scrollState = rememberScrollState()
val overViewData = setOverViewFields(logsData)
Column(
modifier = Modifier.fillMaxSize()
) {
TabScreenHeader(title = logsData?.method + " " + logsData?.endPoint,
onBackClick = { navController.popBackStack() },
onShareCurlIconClick = {
viewModel.generateCurlCommand(
logsData?.method.toString(),
logsData?.apiUrl.toString(),
logsData?.requestHeader.toString(),
logsData?.requestBody
).let {
viewModel.copyToClipboard(it, context)
Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show()
}
},
onShareAsTextIconClick = {
copyContentsToClipboard(
overViewData.toString(),
logsData?.responseHeader.toString(),
logsData?.responseBody.toString(),
context = context
)
Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show()
}
)
TabRow(
selectedTabIndex = selectedTab,
backgroundColor = Color(0xFF3A8EE7),
) {
tabTitles.forEachIndexed { index, title ->
Tab(text = { Text(text = title) },
selected = selectedTab == index,
onClick = { selectedTab = index })
}
}
when (selectedTab) {
0 -> {
DisplayOverView(overViewData)
}
1 -> {
Column(modifier = Modifier.verticalScroll(scrollState)) {
DisplayHeader(logsData?.requestHeader.toString())
logsData?.requestBody.let {
if (!it.isNullOrEmpty()) {
DisplayJson(it)
} else {
Text(
text = "(Empty Body)",
color = Color.Gray,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
2 -> {
Column(modifier = Modifier.verticalScroll(scrollState)) {
DisplayHeader(logsData?.responseHeader.toString())
logsData?.responseBody.let {
if (!it.isNullOrEmpty() && it != "null") {
DisplayJson(it)
} else {
if (logsData?.errorMessage.isNotNullAndNotEmpty() && logsData?.errorMessage != "null") {
Text(
text = logsData?.errorMessage.toString(),
color = Color.Red,
modifier = Modifier.padding(start = 16.dp)
)
} else {
Text(
text = "(Empty Body)",
color = Color.Gray,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
}
}
}
}
fun copyContentsToClipboard(overview: String?, header: String?, body: String?, context: Context) {
val clipboardManager = getSystemService(context, ClipboardManager::class.java)
val combinedText = buildAnnotatedString {
overview?.let { overview ->
append("$overview\n\n")
}
header?.let { header ->
if (header != "null" && header.isNotEmpty()) {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append("------------------Request Body --------------------:\n")
}
append("$header\n\n")
}
}
body?.let { body ->
if (body != "null" && body.isNotEmpty()) {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append("------------------Response Body------------------:\n")
}
append(body)
}
}
}
val clip: ClipData = ClipData.newPlainText("json", combinedText)
clipboardManager?.setPrimaryClip(clip)
Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show()
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LogItem(log: String, onLongPress: () -> Unit) {
Box(
fun DisplayJson(jsonString: String) {
if (jsonString.isNotNullAndNotEmpty() && jsonString != "null") {
val jsonObject = JSONObject(jsonString)
val formattedJson = jsonObject.toString(4)
val annotatedString = buildAnnotatedString {
append(formattedJson)
colorKeysAndValues(formattedJson)
}
Text(text = annotatedString, modifier = Modifier.padding(horizontal = 16.dp))
}
}
fun determineValueType(valueString: String): JsonValueType {
return when {
valueString.matches("\".*\"".toRegex()) -> JsonValueType.STRING
valueString.equals("true", ignoreCase = true) || valueString.equals(
"false", ignoreCase = true
) -> JsonValueType.BOOLEAN
try {
valueString.toInt()
true
} catch (e: NumberFormatException) {
try {
valueString.toDouble()
true
} catch (e: NumberFormatException) {
false
}
} -> JsonValueType.NUMBER
else -> JsonValueType.UNKNOWN
}
}
enum class JsonValueType {
STRING, BOOLEAN, NUMBER, UNKNOWN
}
fun setOverViewFields(logsEvent: LogsEvent?): HashMap<String, Any> {
val dataMap = HashMap<String, Any>()
logsEvent?.let {
dataMap["URL"] = logsEvent.apiUrl.toString()
dataMap["Method"] = logsEvent.method.toString()
dataMap["Status"] =
if (logsEvent.statusCode.toString() != "null") logsEvent.statusCode.toString() else "Failed"
dataMap["Request size"] = logsEvent.requestSize.toString()
dataMap["Response size"] = logsEvent.responseSize.toString()
dataMap["Total size"] = logsEvent.totalSize.toString()
dataMap["Request time"] = logsEvent.requestTime.toString()
dataMap["Response time"] = logsEvent.responseTime.toString()
dataMap["Duration"] = logsEvent.latency.toString() + ""
dataMap["Protocol"] = logsEvent.protocol.toString()
}
return dataMap
}
@Composable
fun DisplayOverView(data: HashMap<String, Any>) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp)) {
for ((key, value) in data.entries) {
Row {
Text(
text = key, color = Color.DarkGray, modifier = Modifier.width(130.dp)
)
Text(
text = value.toString(), color = Color.Gray
)
}
Spacer(modifier = Modifier.size(1.dp))
}
}
}
@Composable
fun TopHeader(
onDeleteClick: () -> Unit = {},
onSearchClick: (String) -> Unit = {},
onRefreshClick: () -> Unit = {},
onFilterClick: (List<String>) -> Unit = {}
) {
var showDeleteConfirmationDialog by remember { mutableStateOf(false) }
var isSearchMode by remember { mutableStateOf(false) }
var searchText by remember { mutableStateOf("") }
var showFilterDialog by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 10.dp)
.combinedClickable(onClick = {}, onLongClick = onLongPress, onDoubleClick = {})
.height(60.dp)
.background(Color(0xFF3A8EE7)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = log, modifier = Modifier, maxLines = 1, overflow = TextOverflow.Ellipsis
if (isSearchMode) {
Box(modifier = Modifier
.size(32.dp)
.padding(start = 8.dp)
.clickable {
isSearchMode = false
searchText = ""
}
.padding(vertical = 8.dp)) {
Icon(painter = painterResource(id = WidgetsR.drawable.ic_black_back_arrow_svg),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.clickable {
isSearchMode = false
searchText = ""
})
}
Spacer(modifier = Modifier.size(16.dp))
Box(
modifier = Modifier
.weight(1f)
.padding(vertical = 4.dp)
) {
TextField(
value = searchText,
onValueChange = {
searchText = it
onSearchClick(searchText)
},
placeholder = { Text("Search") },
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
textColor = Color.White,
),
maxLines = 1,
modifier = Modifier.fillMaxWidth(),
keyboardActions = KeyboardActions(onDone = {
if (searchText.length > 2) {
onSearchClick(searchText)
}
})
)
}
if (searchText.length > 1) {
Icon(painter = painterResource(id = WidgetsR.drawable.ic_cross),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.clickable {
searchText = ""
onSearchClick(searchText)
})
}
Spacer(modifier = Modifier.size(8.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(end = 16.dp)
) {
Icon(
painter = painterResource(id = WidgetsR.drawable.ic_bold_search_svg),
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Icon(painter = painterResource(id = WidgetsR.drawable.ic_delete_outlined_24dp),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.clickable {
showDeleteConfirmationDialog = true
})
}
} else {
Column(modifier = Modifier.padding(start = 16.dp)) {
Text(
text = "NetWatch",
fontWeight = FontWeight.Bold,
color = Color.White,
fontSize = 18.sp
)
Spacer(modifier = Modifier.size(2.dp))
Text(text = "Navi QA", color = Color.White, fontSize = 14.sp)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(end = 16.dp)
) {
Icon(painter = painterResource(id = WidgetsR.drawable.ic_bold_search_svg),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.clickable {
isSearchMode = true
})
Icon(painter = painterResource(id = WidgetsR.drawable.ic_filter),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.clickable {
showFilterDialog = true
})
Icon(painter = painterResource(id = WidgetsR.drawable.ic_refresh_black),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.clickable {
onRefreshClick()
})
Icon(painter = painterResource(id = WidgetsR.drawable.ic_delete_outlined_24dp),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.clickable {
showDeleteConfirmationDialog = true
})
}
}
}
if (showFilterDialog) {
FilterDialog(
filterItems = listOf("Pulse"),
onFilterSelected = {
onFilterClick(it)
},
onDismiss = {
showFilterDialog = false
}
)
}
if (showDeleteConfirmationDialog) {
AlertDialog(onDismissRequest = {
showDeleteConfirmationDialog = false
},
title = { Text("Confirmation") },
text = { Text("Are you sure you want to delete logs?") },
confirmButton = {
Button(onClick = {
onDeleteClick()
showDeleteConfirmationDialog = false
}) {
Text("Yes")
}
},
dismissButton = {
Button(onClick = {
showDeleteConfirmationDialog = false
}) {
Text("No")
}
})
}
}
@Composable
fun FilterDialog(
filterItems: List<String>,
onFilterSelected: (List<String>) -> Unit,
onDismiss: () -> Unit
) {
var selectedFilters by remember { mutableStateOf(emptyList<String>()) }
Dialog(
onDismissRequest = {
onDismiss()
},
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true),
content = {
Box(
modifier = Modifier
.width(300.dp)
.height(200.dp)
.background(MaterialTheme.colorScheme.surface)
.padding(16.dp)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
Text("Select Filters", fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
filterItems.forEach { filter ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
Checkbox(
checked = selectedFilters.contains(filter),
onCheckedChange = {
selectedFilters = if (it) {
selectedFilters + filter
} else {
selectedFilters - filter
}
},
modifier = Modifier.padding(end = 8.dp)
)
Text(text = filter)
}
}
Spacer(modifier = Modifier.height(26.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
onClick = {
onDismiss()
},
modifier = Modifier.weight(1f)
) {
Text("Cancel")
}
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
onFilterSelected(selectedFilters)
onDismiss()
},
modifier = Modifier.weight(1f)
) {
Text("Apply")
}
}
}
}
}
)
}
@Composable
fun DisplayHeader(data: String) {
val map: HashMap<String, Any> = Gson().fromJson(
data, object : TypeToken<HashMap<String, Any>>() {}.type
)
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp)) {
for ((key, value) in map) {
Row {
Text(
text = key, color = Color.DarkGray, modifier = Modifier.width(180.dp)
)
Text(
text = value.toString(), color = Color.Gray
)
}
Spacer(modifier = Modifier.size(1.dp))
}
}
}

View File

@@ -0,0 +1,6 @@
package com.naviapp.releaselog.utils
data class ErrorLog(
val statusCode: String? = null,
val message: String? = null
)

View File

@@ -0,0 +1,102 @@
package com.naviapp.releaselog.utils
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.graphics.PixelFormat
import android.view.Gravity
import android.view.WindowManager
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.core.content.res.ResourcesCompat
import com.navi.base.deeplink.util.DeeplinkConstants
import com.navi.base.model.CtaData
import com.navi.design.utils.dpToPxInInt
import com.navi.naviwidgets.extensions.hexToColor
import com.navi.naviwidgets.R as WidgetsR
import com.naviapp.common.navigator.NaviDeepLinkNavigator
import com.naviapp.releaselog.activity.ReleaseLogActivity
import com.naviapp.releaselog.screens.JsonValueType
import com.naviapp.releaselog.screens.determineValueType
import org.json.JSONObject
object NetWatchUtil {
fun addUniversalButtonToActivity(
activity: Activity, context: Context
) {
if (activity is ReleaseLogActivity) {
return
}
val button = UniversalButton(context, activity)
val layoutParams = WindowManager.LayoutParams(
dpToPxInInt(30),
dpToPxInInt(30),
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT
)
layoutParams.gravity = Gravity.TOP or Gravity.END
layoutParams.x = dpToPxInInt(25)
layoutParams.y = dpToPxInInt(46)
button.layoutParams = layoutParams
val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.addView(button, layoutParams)
}
@SuppressLint("ViewConstructor")
class UniversalButton(
context: Context,
activity: Activity
) :
androidx.appcompat.widget.AppCompatImageView(context) {
init {
background = ResourcesCompat.getDrawable(resources, WidgetsR.drawable.glowing_bulb, null)
isClickable = true
setOnClickListener {
NaviDeepLinkNavigator.navigate(
activity = activity,
ctaData = CtaData(url = DeeplinkConstants.RELEASE_LOG),
finish = false
)
}
}
}
fun AnnotatedString.Builder.colorKeysAndValues(jsonString: String) {
val jsonObject = JSONObject(jsonString)
val keys = jsonObject.keys()
for (key in keys) {
val keyIndex = jsonString.indexOf("\"$key\"")
val valueIndex = jsonString.indexOf(':', keyIndex + key.length + 1)
if (keyIndex != -1 && valueIndex != -1) {
addStyle(
style = SpanStyle(color = Color.Red, fontWeight = FontWeight.SemiBold),
start = keyIndex,
end = keyIndex + key.length + 2
)
val valueStartIndex = valueIndex + 1
val valueEndIndex = jsonString.indexOfAny(charArrayOf(',', '}'), valueStartIndex)
val valueString = jsonString.substring(valueStartIndex, valueEndIndex)
val valueType = determineValueType(valueString)
val color = when (valueType) {
JsonValueType.STRING -> Color.Blue
JsonValueType.BOOLEAN -> Color.Green
JsonValueType.NUMBER -> hexToColor("#8FFF5732")
else -> Color.Gray
}
addStyle(
style = SpanStyle(color = color), start = valueStartIndex, end = valueEndIndex
)
}
}
}
}

View File

@@ -1,17 +1,18 @@
package com.naviapp.releaselog.viewmodel
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewModelScope
import com.navi.base.utils.QA_RELEASE_LOGS_FILE_NAME
import com.navi.base.utils.QA_RELEASE_LOGS_FOLDER_NAME
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.base.db.NetWatchDatabaseHelper
import com.navi.base.db.model.LogsEvent
import com.navi.common.utils.log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.io.BufferedReader
import java.io.File
import java.io.FileInputStream
import java.io.InputStreamReader
import com.navi.common.viewmodel.BaseVM
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -24,39 +25,25 @@ class ReleaseLogViewModel @Inject constructor() : BaseVM() {
ReleaseLogState.Loading
)
val logsData = _logsData.asStateFlow()
private var rawLogs: List<String> = emptyList()
private val _logsDataDeleted = MutableStateFlow(false)
val logsDataDeleted = _logsDataDeleted.asStateFlow()
var rawLogs: List<LogsEvent> = emptyList()
private val _selectedLog = MutableStateFlow<LogsEvent?>(null)
val selectedLog = _selectedLog.asStateFlow()
fun readLogFile(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
_logsData.emit(ReleaseLogState.Loading)
val logDirectoryName = QA_RELEASE_LOGS_FOLDER_NAME
val logFileName = QA_RELEASE_LOGS_FILE_NAME
val logFile = File(context.filesDir, "$logDirectoryName/$logFileName")
val logs = mutableListOf<String>()
if (logFile.exists()) {
val fileInputStream = FileInputStream(logFile)
val bufferedReader = BufferedReader(InputStreamReader(fileInputStream))
var line: String?
val lines = mutableListOf<String>()
val linesToReadFromBottom = 150
while (bufferedReader.readLine().also { line = it } != null) {
line?.let {
if (it.isNotEmpty()) {
lines.add(it)
}
}
}
val startIndex = maxOf(0, lines.size - linesToReadFromBottom)
for (i in lines.size - 2 downTo startIndex) {
logs.add(lines[i])
}
bufferedReader.close()
fileInputStream.close()
rawLogs = logs
_logsData.emit(ReleaseLogState.Success(logs))
}
rawLogs = emptyList()
val db = NetWatchDatabaseHelper.getLogsDatabase(context)
val logDao = db.logDao()
val logEvents = logDao.fetchLogs(THRESHOLD_VALUE)
rawLogs = logEvents
_logsData.emit(ReleaseLogState.Success(logEvents))
} catch (e: Exception) {
e.log()
_logsData.emit(ReleaseLogState.Error)
@@ -64,31 +51,87 @@ class ReleaseLogViewModel @Inject constructor() : BaseVM() {
}
}
fun showOriginalLogs() {
fun readFilteredLogs(context: Context, moduleName: List<String> = emptyList()) {
viewModelScope.launch(Dispatchers.IO) {
_logsData.emit(ReleaseLogState.Success(rawLogs))
try {
_logsData.emit(ReleaseLogState.Loading)
val db = NetWatchDatabaseHelper.getLogsDatabase(context)
val logDao = db.logDao()
val logEvents = logDao.fetchFilteredLogs(moduleName, THRESHOLD_VALUE)
_logsData.emit(ReleaseLogState.Success(logEvents))
} catch (e: Exception) {
e.log()
_logsData.emit(ReleaseLogState.Error)
}
}
}
fun setSelectedLog(log: LogsEvent) {
_selectedLog.value = log
}
fun scrollToMatchedText(
searchText: String
) {
fun showSearchedLogs(searchedLogs: List<LogsEvent>) {
viewModelScope.launch(Dispatchers.IO) {
_logsData.emit(ReleaseLogState.Success(rawLogs.filter {
it.contains(
searchText, ignoreCase = true
)
}))
_logsData.emit(ReleaseLogState.Success(searchedLogs))
}
}
fun deleteLogs(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
val db = NetWatchDatabaseHelper.getLogsDatabase(context)
val logDao = db.logDao()
logDao.deleteLogs()
_logsDataDeleted.emit(true)
} catch (e: Exception) {
e.log()
_logsDataDeleted.emit(false)
}
}
}
fun generateCurlCommand(
method: String, url: String, headers: String, requestBody: String?
): String {
val curlCommand = StringBuilder("curl -X $method '$url'")
val headerMap: Map<String, String> = Gson().fromJson(
headers, object : TypeToken<Map<String, String>>() {}.type
)
for ((key, value) in headerMap) {
curlCommand.append(" -H '$key: $value'")
}
requestBody?.let {
val escapedBody = it.replace("'", "'\\''")
curlCommand.append(" -d '$escapedBody'")
}
return curlCommand.toString()
}
fun copyToClipboard(text: String, context: Context) {
val clipboard = ContextCompat.getSystemService(context, ClipboardManager::class.java)
val clip: ClipData = ClipData.newPlainText("json", text)
clipboard?.setPrimaryClip(clip)
}
fun filterLogs(logs: List<LogsEvent>, searchText: String): List<LogsEvent> {
return logs.filter { log ->
log.endPoint.toString().contains(searchText, ignoreCase = true)
}
}
sealed class ReleaseLogState {
object Loading : ReleaseLogState()
data class Success(val data: List<String>) : ReleaseLogState()
object Error : ReleaseLogState()
data object Loading : ReleaseLogState()
data class Success(val data: List<LogsEvent>) : ReleaseLogState()
data object Error : ReleaseLogState()
}
companion object{
private const val THRESHOLD_VALUE:Int = 150
}

View File

@@ -38,21 +38,6 @@
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnLogs"
style="@style/ActionButtonText6Style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/_10dp"
android:clickable="true"
android:focusable="true"
android:text="@string/check_logs"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/my_toolbar"
tools:visibility="visible" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/shimmer_layout"
android:layout_width="match_parent"

View File

@@ -0,0 +1,18 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.base.db
import androidx.room.Database
import androidx.room.RoomDatabase
import com.navi.base.db.dao.LogDao
import com.navi.base.db.model.LogsEvent
@Database(entities = [LogsEvent::class], version = 1, exportSchema = false)
abstract class NetWatchDatabase : RoomDatabase() {
abstract fun logDao(): LogDao
}

View File

@@ -0,0 +1,30 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.base.db
import android.content.Context
import androidx.room.Room
object NetWatchDatabaseHelper {
@Volatile private var INSTANCE: NetWatchDatabase? = null
fun getLogsDatabase(context: Context): NetWatchDatabase {
return INSTANCE
?: synchronized(this) {
if (INSTANCE != null) {
INSTANCE
}
val instance =
Room.databaseBuilder(context, NetWatchDatabase::class.java, "NET_WATCH_DB")
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}

View File

@@ -0,0 +1,26 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.base.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.navi.base.db.model.LogsEvent
@Dao
interface LogDao {
@Insert fun insertLogs(logs: LogsEvent)
@Query("SELECT * FROM LogsEvent ORDER BY time DESC LIMIT :thresholdValue")
fun fetchLogs(thresholdValue: Int): List<LogsEvent>
@Query("SELECT * FROM LogsEvent WHERE moduleName not in (:moduleNames) LIMIT :thresholdValue")
fun fetchFilteredLogs(moduleNames: List<String>, thresholdValue: Int): List<LogsEvent>
@Query("DELETE FROM LogsEvent ") fun deleteLogs()
}

View File

@@ -0,0 +1,38 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.base.db.model
import androidx.room.*
import java.io.Serializable
@Entity
data class LogsEvent(
@ColumnInfo(name = "time") val time: Long?,
@ColumnInfo(name = "statusCode") val statusCode: String?,
@ColumnInfo(name = "endPoint") val endPoint: String?,
@ColumnInfo(name = "baseUrl") val baseUrl: String?,
@ColumnInfo(name = "method") val method: String?,
@ColumnInfo(name = "latency") val latency: String?,
@ColumnInfo(name = "requestSize") val requestSize: String?,
@ColumnInfo(name = "requestHeader") val requestHeader: String?,
@ColumnInfo(name = "requestBody") val requestBody: String?,
@ColumnInfo(name = "responseHeader") val responseHeader: String?,
@ColumnInfo(name = "responseBody") val responseBody: String?,
@ColumnInfo(name = "responseSize") val responseSize: String?,
@ColumnInfo(name = "moduleName") val moduleName: String?,
@ColumnInfo(name = "logType") val logType: String?,
@ColumnInfo(name = "protocol") val protocol: String?,
@ColumnInfo(name = "requestTime") val requestTime: String?,
@ColumnInfo(name = "responseTime") val responseTime: String?,
@ColumnInfo(name = "totalSize") val totalSize: String?,
@ColumnInfo(name = "scheme") val scheme: String?,
@ColumnInfo(name = "errorMessage") val errorMessage: String?,
@ColumnInfo(name = "apiUrl") val apiUrl: String?,
) : Serializable {
@PrimaryKey(autoGenerate = true) var id: Int = 0
}

View File

@@ -34,5 +34,3 @@ const val SKIP_LOADER = "skipLoader"
const val PAN_VERIFY_POLLING_FAIL = "PAN_VERIFY_POLLING_FAIL"
const val APPLICATION_JSON = "application/json"
const val EXCLUDE_FROM_HASH_ENCRYPTION = "excludeFromHashEncryption"
const val QA_RELEASE_LOGS_FILE_NAME = "NAVI_QA_RELEASE_LOGS.txt"
const val QA_RELEASE_LOGS_FOLDER_NAME = "NAVI_QA_RELEASE_LOGS_FOLDER"

View File

@@ -0,0 +1,204 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.base.utils
import android.content.Context
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.navi.base.BuildConfig
import com.navi.base.db.NetWatchDatabase
import com.navi.base.db.NetWatchDatabaseHelper
import com.navi.base.db.dao.LogDao
import com.navi.base.db.model.LogsEvent
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.Executors
import kotlinx.coroutines.asCoroutineDispatcher
import okhttp3.Request
import okhttp3.Response
import okio.Buffer
object NetWatchManger {
private lateinit var applicationContext: Context
private val coroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private var isQaRelease: Boolean = false
private lateinit var netWatchDatabase: NetWatchDatabase
private lateinit var logDao: LogDao
fun init(context: Context, flavour: String) {
applicationContext = context
isQaRelease = flavour == QA && !BuildConfig.DEBUG
netWatchDatabase = NetWatchDatabaseHelper.getLogsDatabase(applicationContext)
this.logDao = netWatchDatabase.logDao()
}
fun buildLogMessage(
responseData: Any? = null,
logType: String,
request: Request? = null,
response: Response? = null,
moduleName: String = EMPTY,
errorMessage: Any? = EMPTY
) {
if (!isQaRelease) {
return
}
generateAndStoreLogMessage(
responseData = responseData,
logType = logType,
request = request,
response = response,
moduleName = moduleName,
errorMessage = errorMessage
)
}
private fun generateAndStoreLogMessage(
responseData: Any? = null,
logType: String,
request: Request? = null,
response: Response? = null,
moduleName: String = EMPTY,
errorMessage: Any? = EMPTY
) {
coroutineDispatcher.executor.execute {
val baseUrl = request?.url?.toUrl()?.host
val endPoint = request?.url?.toUrl()?.path
val scheme = request?.url?.scheme
val requestMethod = request?.method
val responseJsonString =
try {
GsonBuilder().setPrettyPrinting().create().toJson(responseData)
} catch (e: Exception) {
""
}
val requestJsonString = bodyToString(request)
val requestHeaderMap: HashMap<String, Any> = hashMapOf()
request?.headers?.forEach { requestHeaderMap[it.first] = it.second }
val requestHeaderJsonString =
try {
Gson().toJson(requestHeaderMap)
} catch (e: Exception) {
EMPTY
}
val responseHeaderMap: HashMap<String, Any> = hashMapOf()
response?.headers?.forEach { responseHeaderMap[it.first] = it.second }
val responseHeaderJsonString =
try {
Gson().toJson(responseHeaderMap)
} catch (e: Exception) {
EMPTY
}
val latency =
response
?.receivedResponseAtMillis
.orZero()
.minus(response?.sentRequestAtMillis.orZero())
val latencyWithUnit =
if (latency < 1000) {
"$latency ms"
} else {
"${String.format("%.2f", latency / 1000.0).toDouble()} sec"
}
val requestTime = response?.sentRequestAtMillis?.let { convertMillisToDateTime(it) }
val responseTime =
response?.receivedResponseAtMillis?.let { convertMillisToDateTime(it) }
val requestSize = request?.body?.contentLength() ?: 0
val requestSizeWithUnit =
when {
requestSize < 1024 -> "$requestSize B"
else -> "${String.format("%.2f", requestSize / 1024.0)} kB"
}
val responseSize = response?.body?.contentLength() ?: 0
val isChunked =
"chunked".equals(response?.headers?.get("Transfer-Encoding"), ignoreCase = true)
val responseSizeWithUnit =
when {
isChunked -> "chunked"
responseSize >= 0 -> {
when {
responseSize < 1024 -> "$responseSize B"
else -> "${String.format("%.2f", responseSize / 1024.0)} kB"
}
}
else -> "unknown"
}
val totalSize = requestSize + responseSize
val totalSizeWithUnit =
when {
isChunked -> "Chunked Encoding"
totalSize >= 0 -> {
when {
totalSize < 1024 -> "$totalSize B"
else -> "${String.format("%.2f", totalSize / 1024.0)} kB"
}
}
else -> "Unknown Size"
}
val logs =
LogsEvent(
time = System.currentTimeMillis(),
statusCode = response?.code.toString(),
endPoint = endPoint,
baseUrl = baseUrl,
logType = logType,
method = requestMethod.toString(),
responseBody = responseJsonString,
responseHeader = responseHeaderJsonString,
requestHeader = requestHeaderJsonString,
moduleName = moduleName,
latency = latencyWithUnit,
requestBody = requestJsonString,
requestSize = requestSizeWithUnit,
responseSize = responseSizeWithUnit,
protocol = response?.protocol?.name.toString(),
requestTime = requestTime,
responseTime = responseTime,
totalSize = totalSizeWithUnit,
scheme = scheme,
apiUrl = request?.url.toString(),
errorMessage = errorMessage.toString()
)
logDao.insertLogs(logs = logs)
}
}
private fun bodyToString(request: Request?): String {
return try {
val copy = request?.newBuilder()?.build()
val buffer = Buffer()
copy?.body?.writeTo(buffer)
buffer.readUtf8()
} catch (e: IOException) {
"did not work"
}
}
private fun convertMillisToDateTime(time: Long): String {
val dateFormat = SimpleDateFormat("hh:mm:ss a", Locale.getDefault())
val date = Date(time)
return dateFormat.format(date)
}
enum class ReleaseLogType {
NETWORK_LOG
}
}

View File

@@ -1,189 +0,0 @@
/*
*
* * Copyright © 2023-2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.base.utils
import android.content.Context
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.navi.base.BuildConfig
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStreamWriter
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.Executors
import kotlinx.coroutines.asCoroutineDispatcher
import okhttp3.Request
import okhttp3.Response
object QaReleaseLogUtil {
private lateinit var applicationContext: Context
private val coroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
var isQaRelease: Boolean = false
fun init(context: Context, flavour: String) {
applicationContext = context
isQaRelease = flavour == QA && !BuildConfig.DEBUG
}
fun buildQaReleaseLogMessage(
statusCode: String? = null,
responseData: Any? = null,
logType: String,
request: Request? = null,
data: Map<String, String>? = null,
response: Response? = null,
requestMethod: String? = null
) {
if (!isQaRelease) {
return
}
coroutineDispatcher.executor.execute {
val logData = hashMapOf<String, Any>()
val timestamp =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
.format(Date())
.toString()
var endPoint = ""
when (logType) {
ReleaseLogType.NETWORK_LOG.name -> {
val networkData: HashMap<String, Any> = hashMapOf()
endPoint = URL(request?.url.toString()).path
val responseJsonString =
try {
Gson().toJson(responseData)
} catch (e: Exception) {
""
}
val responseObject =
try {
JsonParser.parseString(responseJsonString).asJsonObject
} catch (e: Exception) {
JsonObject()
}
networkData["RESPONSE_BODY"] = responseObject
networkData["RESPONSE_CODE"] = statusCode.toString()
val requestJsonString =
try {
Gson().toJson(responseData)
} catch (e: Exception) {
""
}
val requestObject =
try {
JsonParser.parseString(requestJsonString).asJsonObject
} catch (e: Exception) {
JsonObject()
}
networkData["REQUEST_BODY"] = requestObject
networkData["URL"] = request?.url.toString()
val requestHeaderMap: HashMap<String, Any> = hashMapOf()
request?.headers?.forEach { requestHeaderMap[it.first] = it.second }
val requestHeaderJsonString =
try {
Gson().toJson(requestHeaderMap)
} catch (e: Exception) {
EMPTY
}
val requestHeaderObject =
try {
JsonParser.parseString(requestHeaderJsonString).asJsonObject
} catch (e: Exception) {
JsonObject()
}
networkData["REQUEST_HEADER"] = requestHeaderObject
val responseHeaderMap: HashMap<String, Any> = hashMapOf()
response?.headers?.forEach { responseHeaderMap[it.first] = it.second }
val responseHeaderJsonString =
try {
Gson().toJson(responseHeaderMap)
} catch (e: Exception) {
EMPTY
}
val responseHeaderObject =
try {
JsonParser.parseString(responseHeaderJsonString).asJsonObject
} catch (e: Exception) {
JsonObject()
}
networkData["RESPONSE_HEADER"] = responseHeaderObject
networkData["REQUEST_METHOD"] = requestMethod.toString()
logData[timestamp] = networkData
}
ReleaseLogType.ANR_LOG.name,
ReleaseLogType.CRASH_LOG.name -> {
logData[timestamp] = data as HashMap<String, String>
}
else -> {}
}
val logMessage = Gson().toJson(logData)
writeLogToFile(
logMessage = logMessage,
logType,
timestamp,
endPoint,
statusCode,
requestMethod
)
}
}
private fun writeLogToFile(
logMessage: String,
logType: String,
timeStamp: String,
url: String,
statusCode: String?,
requestMethod: String?
) {
val logDirectoryName = QA_RELEASE_LOGS_FOLDER_NAME
val logFileName = QA_RELEASE_LOGS_FILE_NAME
val appInternalStorageDir = applicationContext.filesDir
val logDirectory = File(appInternalStorageDir, logDirectoryName)
if (!logDirectory.exists()) {
logDirectory.mkdirs()
}
val logFile = File(logDirectory, logFileName)
try {
val fileOutputStream = FileOutputStream(logFile, true)
val outputStreamWriter = OutputStreamWriter(fileOutputStream)
outputStreamWriter.append("\r\n")
if (logType == ReleaseLogType.NETWORK_LOG.name) {
outputStreamWriter.append("\r\n---$statusCode $requestMethod $url $timeStamp------")
} else {
outputStreamWriter.append("\r\n--- $logType $timeStamp------")
}
outputStreamWriter.append("\r\n")
outputStreamWriter.append(logMessage)
outputStreamWriter.flush()
outputStreamWriter.close()
fileOutputStream.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
enum class ReleaseLogType {
NETWORK_LOG,
CRASH_LOG,
ANR_LOG
}
}

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M14.667,2H1.334L6.667,8.307V12.667L9.333,14V8.307L14.667,2Z"
android:strokeLineJoin="round"
android:strokeWidth="1.33333"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>

View File

@@ -7,20 +7,20 @@
package com.navi.pulse.network
import com.navi.base.utils.QaReleaseLogUtil
import com.navi.base.utils.QaReleaseLogUtil.buildQaReleaseLogMessage
import com.navi.base.utils.NetWatchManger
import com.navi.base.utils.NetWatchManger.buildLogMessage
import retrofit2.Response
class PulseNetworkRepository {
suspend fun sendEvents(url: String, pulseRequest: PulseRequest): Response<PulseResponse> {
val response = PulseRetrofitProvider.getApiService().sendEvents(url, pulseRequest)
buildQaReleaseLogMessage(
buildLogMessage(
responseData = response.body(),
statusCode = response.body()?.code.toString(),
request = response.raw().request,
logType = QaReleaseLogUtil.ReleaseLogType.NETWORK_LOG.name,
logType = NetWatchManger.ReleaseLogType.NETWORK_LOG.name,
response = response.raw(),
requestMethod = response.raw().request.method
moduleName = "Pulse",
errorMessage = response.errorBody().toString()
)
return response
}