Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement sending logs to developer #190

Merged
merged 10 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 49 additions & 39 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,46 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application
android:name="gq.kirmanak.mealient.App"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/full_backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.App.Starting"
android:localeConfig="@xml/locales_config"
tools:ignore="UnusedAttribute">
<activity
android:name=".ui.activity.MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<application
android:name="gq.kirmanak.mealient.App"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/full_backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.App.Starting"
tools:ignore="UnusedAttribute">
<activity
android:name=".ui.activity.MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.share.ShareRecipeActivity"
android:exported="true"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
</activity>
</application>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.share.ShareRecipeActivity"
android:exported="true"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
</activity>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@ class AuthRepoImpl @Inject constructor(
private val authDataSource: AuthDataSource,
private val logger: Logger,
private val signOutHandler: SignOutHandler,
private val credentialsLogRedactor: CredentialsLogRedactor,
) : AuthRepo, AuthenticationProvider {

override val isAuthorizedFlow: Flow<Boolean>
get() = authStorage.authTokenFlow.map { it != null }

override suspend fun authenticate(email: String, password: String) {
logger.v { "authenticate() called with: email = $email, password = $password" }
logger.v { "authenticate() called" }
credentialsLogRedactor.set(email, password)
val token = authDataSource.authenticate(email, password)
authStorage.setAuthToken(token)
val apiToken = authDataSource.createApiToken(API_TOKEN_NAME)
authStorage.setAuthToken(apiToken)
credentialsLogRedactor.clear()
}

override suspend fun getAuthToken(): String? = authStorage.getAuthToken()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gq.kirmanak.mealient.data.auth.impl

import gq.kirmanak.mealient.logging.LogRedactor
import kotlinx.coroutines.flow.MutableStateFlow
import java.net.URLEncoder
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class CredentialsLogRedactor @Inject constructor() : LogRedactor {

private data class Credentials(
val login: String,
val password: String,
val urlEncodedLogin: String = URLEncoder.encode(login, Charsets.UTF_8.name()),
val urlEncodedPassword: String = URLEncoder.encode(password, Charsets.UTF_8.name()),
)

private val credentialsState = MutableStateFlow<Credentials?>(null)

fun set(login: String, password: String) {
credentialsState.value = Credentials(login, password)
}

fun clear() {
credentialsState.value = null
}

override fun redact(message: String): String {
val credentials = credentialsState.value ?: return message

return message
.replace(credentials.login, "<login>")
.replace(credentials.urlEncodedLogin, "<login>")
.replace(credentials.password, "<password>")
.replace(credentials.urlEncodedPassword, "<password>")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package gq.kirmanak.mealient.data.baseurl.impl

import androidx.core.net.toUri
import gq.kirmanak.mealient.architecture.configuration.AppDispatchers
import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
import gq.kirmanak.mealient.logging.LogRedactor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton

@Singleton
class BaseUrlLogRedactor @Inject constructor(
private val serverInfoStorageProvider: Provider<ServerInfoStorage>,
private val dispatchers: AppDispatchers,
) : LogRedactor {

private val hostState = MutableStateFlow<String?>(null)

init {
setInitialBaseUrl()
}

private fun setInitialBaseUrl() {
val scope = CoroutineScope(dispatchers.default + SupervisorJob())
scope.launch {
val serverInfoStorage = serverInfoStorageProvider.get()
hostState.compareAndSet(
expect = null,
update = serverInfoStorage.getBaseURL()?.extractHost(),
)
}
}

fun set(baseUrl: String) {
hostState.value = baseUrl.extractHost()
}


override fun redact(message: String): String {
val host = hostState.value ?: return message
return message.replace(host, "<host>")
}
}

private fun String.extractHost() = runCatching { toUri() }.getOrNull()?.host
7 changes: 7 additions & 0 deletions app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import gq.kirmanak.mealient.data.auth.AuthDataSource
import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.auth.AuthStorage
import gq.kirmanak.mealient.data.auth.impl.AuthDataSourceImpl
import gq.kirmanak.mealient.data.auth.impl.AuthRepoImpl
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl
import gq.kirmanak.mealient.data.auth.impl.CredentialsLogRedactor
import gq.kirmanak.mealient.datasource.AuthenticationProvider
import gq.kirmanak.mealient.logging.LogRedactor
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo

@Module
Expand All @@ -31,4 +34,8 @@ interface AuthModule {

@Binds
fun bindShoppingListsAuthRepo(impl: AuthRepoImpl): ShoppingListsAuthRepo

@Binds
@IntoSet
fun bindCredentialsLogRedactor(impl: CredentialsLogRedactor): LogRedactor
}
7 changes: 7 additions & 0 deletions app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import gq.kirmanak.mealient.data.baseurl.*
import gq.kirmanak.mealient.data.baseurl.impl.BaseUrlLogRedactor
import gq.kirmanak.mealient.data.baseurl.impl.ServerInfoStorageImpl
import gq.kirmanak.mealient.datasource.ServerUrlProvider
import gq.kirmanak.mealient.logging.LogRedactor

@Module
@InstallIn(SingletonComponent::class)
Expand All @@ -23,4 +26,8 @@ interface BaseURLModule {

@Binds
fun bindServerUrlProvider(serverInfoRepoImpl: ServerInfoRepoImpl): ServerUrlProvider

@Binds
@IntoSet
fun bindBaseUrlLogRedactor(impl: BaseUrlLogRedactor): LogRedactor
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ fun EditText.checkIfInputIsEmpty(
): String? {
val input = if (trim) text?.trim() else text
val text = input?.toString().orEmpty()
logger.d { "Input text is \"$text\"" }
return text.ifEmpty {
inputLayout.error = resources.getString(stringId)
val textChangesLiveData = textChangesLiveData(logger)
Expand Down
46 changes: 46 additions & 0 deletions app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package gq.kirmanak.mealient.ui.activity

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.core.content.FileProvider
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.isVisible
import androidx.core.view.iterator
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAddRecipeFragment
import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAuthenticationFragment
Expand All @@ -20,10 +24,13 @@ import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.MainActivityBinding
import gq.kirmanak.mealient.extensions.collectWhenResumed
import gq.kirmanak.mealient.extensions.observeOnce
import gq.kirmanak.mealient.logging.getLogFile
import gq.kirmanak.mealient.ui.ActivityUiState
import gq.kirmanak.mealient.ui.BaseActivity
import gq.kirmanak.mealient.ui.CheckableMenuItem

private const val EMAIL_FOR_LOGS = "mealient@gmail.com"

@AndroidEntryPoint
class MainActivity : BaseActivity<MainActivityBinding>(
binder = MainActivityBinding::bind,
Expand Down Expand Up @@ -87,13 +94,52 @@ class MainActivity : BaseActivity<MainActivityBinding>(
viewModel.logout()
return true
}

R.id.email_logs -> {
emailLogs()
return true
}

else -> throw IllegalArgumentException("Unknown menu item id: ${menuItem.itemId}")
}
menuItem.isChecked = true
navigateTo(directions)
return true
}

private fun emailLogs() {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.activity_main_email_logs_confirmation_message)
.setTitle(R.string.activity_main_email_logs_confirmation_title)
.setPositiveButton(R.string.activity_main_email_logs_confirmation_positive) { _, _ -> doEmailLogs() }
.setNegativeButton(R.string.activity_main_email_logs_confirmation_negative, null)
.show()
}

private fun doEmailLogs() {
val logFileUri = try {
FileProvider.getUriForFile(this, "$packageName.provider", getLogFile())
} catch (e: Exception) {
return
}
val emailIntent = buildIntent(logFileUri)
val chooserIntent = Intent.createChooser(emailIntent, null)
startActivity(chooserIntent)
}

private fun buildIntent(logFileUri: Uri?): Intent {
val emailIntent = Intent(Intent.ACTION_SEND)
val to = arrayOf(EMAIL_FOR_LOGS)
emailIntent.setType("text/plain")
emailIntent.putExtra(Intent.EXTRA_EMAIL, to)
emailIntent.putExtra(Intent.EXTRA_STREAM, logFileUri)
emailIntent.putExtra(
Intent.EXTRA_SUBJECT,
getString(R.string.activity_main_email_logs_subject)
)
return emailIntent
}

private fun onUiStateChange(uiState: ActivityUiState) {
logger.v { "onUiStateChange() called with: uiState = $uiState" }
val checkedMenuItem = when (uiState.checkedMenuItem) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class AuthenticationViewModel @Inject constructor(
val uiState: LiveData<OperationUiState<Unit>> get() = _uiState

fun authenticate(email: String, password: String) {
logger.v { "authenticate() called with: email = $email, password = $password" }
logger.v { "authenticate() called" }
_uiState.value = OperationUiState.Progress()
viewModelScope.launch {
val result = runCatchingExceptCancel { authRepo.authenticate(email, password) }
Expand Down
Loading