TP-53503 (#9417)
Co-authored-by: Aman <amankasyapp@gmail.com> Co-authored-by: Shivam Goyal <shivam.goyal@navi.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.naviapp.releaselog.utils
|
||||
|
||||
data class ErrorLog(
|
||||
val statusCode: String? = null,
|
||||
val message: String? = null
|
||||
)
|
||||
102
app/src/main/java/com/naviapp/releaselog/utils/NetWatchUtil.kt
Normal file
102
app/src/main/java/com/naviapp/releaselog/utils/NetWatchUtil.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
18
navi-base/src/main/java/com/navi/base/db/NetWatchDatabase.kt
Normal file
18
navi-base/src/main/java/com/navi/base/db/NetWatchDatabase.kt
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
26
navi-base/src/main/java/com/navi/base/db/dao/LogDao.kt
Normal file
26
navi-base/src/main/java/com/navi/base/db/dao/LogDao.kt
Normal 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()
|
||||
}
|
||||
38
navi-base/src/main/java/com/navi/base/db/model/LogEntity.kt
Normal file
38
navi-base/src/main/java/com/navi/base/db/model/LogEntity.kt
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
204
navi-base/src/main/java/com/navi/base/utils/NetWatchManger.kt
Normal file
204
navi-base/src/main/java/com/navi/base/utils/NetWatchManger.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
13
navi-widgets/src/main/res/drawable/ic_filter.xml
Normal file
13
navi-widgets/src/main/res/drawable/ic_filter.xml
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user