Implement sending logs to developer (#190)
* Save logs to a file * Send logs via email * Enable network logs in release builds * Remove useless chooser title * Append to logs file and ignore I/O errors * Ensure email and password are not logged * Ensure base URL is never logged * Add logs disclaimer
This commit is contained in:
@@ -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>
|
||||
@@ -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()
|
||||
|
||||
@@ -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>")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -31,4 +34,8 @@ interface AuthModule {
|
||||
|
||||
@Binds
|
||||
fun bindShoppingListsAuthRepo(impl: AuthRepoImpl): ShoppingListsAuthRepo
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindCredentialsLogRedactor(impl: CredentialsLogRedactor): LogRedactor
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -23,4 +26,8 @@ interface BaseURLModule {
|
||||
|
||||
@Binds
|
||||
fun bindServerUrlProvider(serverInfoRepoImpl: ServerInfoRepoImpl): ServerUrlProvider
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindBaseUrlLogRedactor(impl: BaseUrlLogRedactor): LogRedactor
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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
|
||||
@@ -10,6 +13,7 @@ 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
|
||||
@@ -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,
|
||||
@@ -87,6 +94,12 @@ 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
|
||||
@@ -94,6 +107,39 @@ class MainActivity : BaseActivity<MainActivityBinding>(
|
||||
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) {
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.impl.BaseUrlLogRedactor
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.datasource.CertificateCombinedException
|
||||
import gq.kirmanak.mealient.datasource.NetworkError
|
||||
@@ -27,6 +28,7 @@ class BaseURLViewModel @Inject constructor(
|
||||
private val recipeRepo: RecipeRepo,
|
||||
private val logger: Logger,
|
||||
private val trustedCertificatesStore: TrustedCertificatesStore,
|
||||
private val baseUrlLogRedactor: BaseUrlLogRedactor,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
|
||||
@@ -36,18 +38,20 @@ class BaseURLViewModel @Inject constructor(
|
||||
val invalidCertificatesFlow = invalidCertificatesChannel.receiveAsFlow()
|
||||
|
||||
fun saveBaseUrl(baseURL: String) {
|
||||
logger.v { "saveBaseUrl() called with: baseURL = $baseURL" }
|
||||
logger.v { "saveBaseUrl() called" }
|
||||
_uiState.value = OperationUiState.Progress()
|
||||
viewModelScope.launch { checkBaseURL(baseURL) }
|
||||
}
|
||||
|
||||
private suspend fun checkBaseURL(baseURL: String) {
|
||||
logger.v { "checkBaseURL() called with: baseURL = $baseURL" }
|
||||
logger.v { "checkBaseURL() called" }
|
||||
|
||||
val hasPrefix = listOf("http://", "https://").any { baseURL.startsWith(it) }
|
||||
val urlWithPrefix = baseURL.takeIf { hasPrefix } ?: "https://%s".format(baseURL)
|
||||
val url = urlWithPrefix.trimEnd { it == '/' }
|
||||
|
||||
baseUrlLogRedactor.set(baseUrl = url)
|
||||
|
||||
logger.d { "checkBaseURL: Created URL = \"$url\", with prefix = \"$urlWithPrefix\"" }
|
||||
if (url == serverInfoRepo.getUrl()) {
|
||||
logger.d { "checkBaseURL: new URL matches current" }
|
||||
|
||||
11
app/src/main/res/drawable/ic_send.xml
Normal file
11
app/src/main/res/drawable/ic_send.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M120,800L120,160L880,480L120,800ZM200,680L674,480L200,280L200,420L440,480L200,540L200,680ZM200,680L200,480L200,280L200,420L200,420L200,540L200,540L200,680Z" />
|
||||
</vector>
|
||||
@@ -1,46 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer"
|
||||
style="?drawerLayoutStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.activity.MainActivity">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer"
|
||||
style="?drawerLayoutStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.activity.MainActivity">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<gq.kirmanak.mealient.ui.activity.ToolbarView
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_medium"
|
||||
android:background="@drawable/bg_toolbar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
<gq.kirmanak.mealient.ui.activity.ToolbarView
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_medium"
|
||||
android:background="@drawable/bg_toolbar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
tools:layout="@layout/fragment_recipes_list" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/navigation_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
app:headerLayout="@layout/view_navigation_drawer_header"
|
||||
app:menu="@menu/navigation_menu" />
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/navigation_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
app:headerLayout="@layout/view_navigation_drawer_header"
|
||||
app:menu="@menu/navigation_menu" />
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
||||
@@ -1,38 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/recipes_list"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_list"
|
||||
android:title="@string/menu_navigation_drawer_recipes_list" />
|
||||
<item
|
||||
android:id="@+id/recipes_list"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_list"
|
||||
android:title="@string/menu_navigation_drawer_recipes_list" />
|
||||
|
||||
<item
|
||||
android:id="@+id/add_recipe"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_add"
|
||||
android:title="@string/menu_navigation_drawer_add_recipe" />
|
||||
<item
|
||||
android:id="@+id/add_recipe"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_add"
|
||||
android:title="@string/menu_navigation_drawer_add_recipe" />
|
||||
|
||||
<item
|
||||
android:id="@+id/shopping_lists"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_shopping_cart"
|
||||
android:title="@string/menu_navigation_drawer_shopping_lists" />
|
||||
<item
|
||||
android:id="@+id/shopping_lists"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_shopping_cart"
|
||||
android:title="@string/menu_navigation_drawer_shopping_lists" />
|
||||
|
||||
<item
|
||||
android:id="@+id/change_url"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_change"
|
||||
android:title="@string/menu_navigation_drawer_change_url" />
|
||||
<item
|
||||
android:id="@+id/change_url"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_change"
|
||||
android:title="@string/menu_navigation_drawer_change_url" />
|
||||
|
||||
<item
|
||||
android:id="@+id/login"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_login"
|
||||
android:title="@string/menu_navigation_drawer_login" />
|
||||
<item
|
||||
android:id="@+id/login"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_login"
|
||||
android:title="@string/menu_navigation_drawer_login" />
|
||||
|
||||
<item
|
||||
android:id="@+id/logout"
|
||||
android:icon="@drawable/ic_logout"
|
||||
android:title="@string/menu_navigation_drawer_logout" />
|
||||
<item
|
||||
android:id="@+id/logout"
|
||||
android:icon="@drawable/ic_logout"
|
||||
android:title="@string/menu_navigation_drawer_logout" />
|
||||
|
||||
<item
|
||||
android:id="@+id/email_logs"
|
||||
android:icon="@drawable/ic_send"
|
||||
android:title="@string/menu_navigation_drawer_email_logs" />
|
||||
|
||||
</menu>
|
||||
@@ -73,4 +73,10 @@
|
||||
<string name="fragment_recipes_favorite_added">Added %1$s to favorites</string>
|
||||
<string name="fragment_recipes_favorite_removed">Removed %1$s from favorites</string>
|
||||
<string name="menu_navigation_drawer_shopping_lists">Shopping lists</string>
|
||||
<string name="menu_navigation_drawer_email_logs">Email logs</string>
|
||||
<string name="activity_main_email_logs_subject">Mealient logs</string>
|
||||
<string name="activity_main_email_logs_confirmation_message">The logs contain sensitive data such as API token, shopping lists, and recipes. API tokens can be revoked using web client. The file can be viewed and edited if you send it to yourself instead.</string>
|
||||
<string name="activity_main_email_logs_confirmation_title">Sending sensitive data</string>
|
||||
<string name="activity_main_email_logs_confirmation_positive">Choose how to send</string>
|
||||
<string name="activity_main_email_logs_confirmation_negative">Cancel</string>
|
||||
</resources>
|
||||
6
app/src/main/res/xml/file_provider_paths.xml
Normal file
6
app/src/main/res/xml/file_provider_paths.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<files-path
|
||||
path="."
|
||||
name="log.txt" />
|
||||
</paths>
|
||||
Reference in New Issue
Block a user