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

Share log file #133

Merged
merged 4 commits into from
Oct 4, 2024
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
7 changes: 5 additions & 2 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ kotlin {
iosSimulatorArm64()

cocoapods {
ios.deploymentTarget = "15.3"
ios.deploymentTarget = "14.0"

version = "1.0"
summary = "Compose App"
Expand Down Expand Up @@ -202,7 +202,10 @@ android {
),
)
sourceSets["main"].res.setSrcDirs(
listOf("src/commonMain/res"),
listOf(
"src/androidMain/res",
"src/commonMain/res",
),
)
dependencies {
debugImplementation(compose.uiTooling)
Expand Down
2 changes: 1 addition & 1 deletion composeApp/composeApp.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Pod::Spec.new do |spec|
spec.summary = 'Compose App'
spec.vendored_frameworks = 'build/cocoapods/framework/composeApp.framework'
spec.libraries = 'c++'
spec.ios.deployment_target = '15.3'
spec.ios.deployment_target = '14.0'
spec.dependency 'Sentry', '~> 8.25'

if !Dir.exist?('build/cocoapods/framework/composeApp.framework') || Dir.empty?('build/cocoapods/framework/composeApp.framework')
Expand Down
11 changes: 11 additions & 0 deletions composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@
android:scheme="ooni" />
</intent-filter>
</activity-alias>

<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/provider_paths" />
</provider>

</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.net.Uri
import android.os.BatteryManager
import android.os.Build
import android.os.LocaleList
import androidx.core.content.FileProvider
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.Preferences
Expand All @@ -20,9 +21,11 @@ import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import okio.Path.Companion.toPath
import org.ooni.engine.AndroidNetworkTypeFinder
import org.ooni.engine.AndroidOonimkallBridge
import org.ooni.probe.background.AppWorkerManager
import org.ooni.probe.data.models.FileSharing
import org.ooni.probe.di.Dependencies
import org.ooni.probe.shared.Platform
import org.ooni.probe.shared.PlatformInfo
Expand All @@ -49,6 +52,7 @@ class AndroidApplication : Application() {
openVpnSettings = ::openVpnSettings,
configureDescriptorAutoUpdate = appWorkerManager::configureDescriptorAutoUpdate,
fetchDescriptorUpdate = appWorkerManager::fetchDescriptorUpdate,
shareFile = ::shareFile,
)
}

Expand Down Expand Up @@ -159,4 +163,35 @@ class AndroidApplication : Application() {
Logger.e("Could not open VPN Settings", e)
false
}

private fun shareFile(fileSharing: FileSharing): Boolean {
val file = filesDir.absolutePath.toPath().resolve(fileSharing.filePath).toFile()
if (!file.exists()) {
Logger.w("File to share does not exist: $file")
return false
}

val uri = try {
FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider", file)
} catch (e: IllegalArgumentException) {
Logger.w("Could not generate file uri to share", e)
return false
}

return try {
startActivity(
Intent.createChooser(
Intent(Intent.ACTION_SEND)
.setType("*/*")
.putExtra(Intent.EXTRA_STREAM, uri)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION),
fileSharing.title,
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
)
true
} catch (e: ActivityNotFoundException) {
Logger.e("Could not share file", e)
false
}
}
}
6 changes: 6 additions & 0 deletions composeApp/src/androidMain/res/xml/provider_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path
name="files"
path="."/>
</paths>
Original file line number Diff line number Diff line change
Expand Up @@ -267,5 +267,6 @@
<string name="quiz_answer_incorrect">Incorrect answer</string>
<string name="logs">Logs</string>
<string name="share_logs">Share Logs</string>
<string name="share_logs_error">Error sharing logs</string>
<string name="filter_logs">Filter Logs</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.ooni.probe.data.models

import okio.Path

data class FileSharing(
val title: String,
val filePath: Path,
)
33 changes: 16 additions & 17 deletions composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.ooni.probe.data.disk.ReadFileOkio
import org.ooni.probe.data.disk.WriteFile
import org.ooni.probe.data.disk.WriteFileOkio
import org.ooni.probe.data.models.AutoRunParameters
import org.ooni.probe.data.models.FileSharing
import org.ooni.probe.data.models.InstalledTestDescriptorModel
import org.ooni.probe.data.models.MeasurementModel
import org.ooni.probe.data.models.PreferenceCategoryKey
Expand Down Expand Up @@ -64,6 +65,7 @@ import org.ooni.probe.domain.RunDescriptors
import org.ooni.probe.domain.RunNetTest
import org.ooni.probe.domain.SaveTestDescriptors
import org.ooni.probe.domain.SendSupportEmail
import org.ooni.probe.domain.ShareLogFile
import org.ooni.probe.domain.ShouldShowVpnWarning
import org.ooni.probe.domain.TestRunStateManager
import org.ooni.probe.domain.UploadMissingMeasurements
Expand Down Expand Up @@ -103,6 +105,7 @@ class Dependencies(
val configureDescriptorAutoUpdate: suspend () -> Boolean,
val fetchDescriptorUpdate: suspend (List<InstalledTestDescriptorModel>?) -> Unit,
val localeDirection: (() -> LayoutDirection)? = null,
private val shareFile: (FileSharing) -> Boolean,
) {
// Common

Expand Down Expand Up @@ -182,22 +185,31 @@ class Dependencies(
private val deleteAllResults by lazy {
DeleteAllResults(resultRepository::deleteAll, deleteFiles::invoke)
}
private val deleteTestDescriptor by lazy {
DeleteTestDescriptor(
preferencesRepository = preferenceRepository,
deleteByRunId = testDescriptorRepository::deleteByRunId,
deleteMeasurementByResultRunId = measurementRepository::deleteByResultRunId,
selectMeasurementsByResultRunId = measurementRepository::selectByResultRunId,
deleteResultByRunId = resultRepository::deleteByRunId,
deleteFile = deleteFiles::invoke,
)
}
private val fetchDescriptor by lazy {
FetchDescriptor(
engineHttpDo = engine::httpDo,
json = json,
)
}
val finishInProgressData by lazy { FinishInProgressData(resultRepository::markAllAsDone) }

val getDescriptorUpdate by lazy {
FetchDescriptorUpdate(
fetchDescriptor = fetchDescriptor::invoke,
createOrUpdateTestDescriptors = testDescriptorRepository::createOrUpdate,
listInstalledTestDescriptors = testDescriptorRepository::list,
)
}

val getAutoRunSettings by lazy { GetAutoRunSettings(preferenceRepository::allSettings) }
val getAutoRunSpecification by lazy {
GetAutoRunSpecification(getTestDescriptors, preferenceRepository)
}
Expand Down Expand Up @@ -239,11 +251,6 @@ class Dependencies(
getAutoRunSettings = getAutoRunSettings::invoke,
)
}
val getAutoRunSettings by lazy {
GetAutoRunSettings(
observeSettings = preferenceRepository::allSettings,
)
}
val runDescriptors by lazy {
RunDescriptors(
getTestDescriptorsBySpec = getTestDescriptorsBySpec::invoke,
Expand All @@ -266,17 +273,8 @@ class Dependencies(
storeUrlsByUrl = urlRepository::createOrUpdateByUrl,
)
}
private val deleteTestDescriptor by lazy {
DeleteTestDescriptor(
preferencesRepository = preferenceRepository,
deleteByRunId = testDescriptorRepository::deleteByRunId,
deleteMeasurementByResultRunId = measurementRepository::deleteByResultRunId,
selectMeasurementsByResultRunId = measurementRepository::selectByResultRunId,
deleteResultByRunId = resultRepository::deleteByRunId,
deleteFile = deleteFiles::invoke,
)
}
private val sendSupportEmail by lazy { SendSupportEmail(platformInfo, launchUrl) }
private val shareLogFile by lazy { ShareLogFile(shareFile, appLogger::getLogFilePath) }
private val shouldShowVpnWarning by lazy {
ShouldShowVpnWarning(preferenceRepository, networkTypeFinder::invoke)
}
Expand Down Expand Up @@ -370,6 +368,7 @@ class Dependencies(
onBack = onBack,
readLog = appLogger::read,
clearLog = appLogger::clear,
shareLogFile = shareLogFile::invoke,
)

fun onboardingViewModel(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.ooni.probe.domain

import okio.Path
import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.logs
import org.jetbrains.compose.resources.getString
import org.ooni.probe.data.models.FileSharing

class ShareLogFile(
private val shareFile: (FileSharing) -> Boolean,
private val getAppLoggerFile: () -> Path,
) {
suspend operator fun invoke(): Boolean =
shareFile(
FileSharing(
title = getString(Res.string.logs),
filePath = getAppLoggerFile(),
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class AppLogger(
}
}

fun getLogFilePath() = FILE_PATH

val logWriter = object : LogWriter() {
override fun isLoggable(
tag: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
Expand All @@ -24,6 +25,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -38,8 +40,12 @@ import ooniprobe.composeapp.generated.resources.back
import ooniprobe.composeapp.generated.resources.filter_logs
import ooniprobe.composeapp.generated.resources.ic_delete_all
import ooniprobe.composeapp.generated.resources.logs
import ooniprobe.composeapp.generated.resources.share_logs
import ooniprobe.composeapp.generated.resources.share_logs_error
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.ooni.probe.LocalSnackbarHostState
import org.ooni.probe.ui.shared.CustomFilterChip
import org.ooni.probe.ui.theme.LocalCustomColors

Expand All @@ -66,6 +72,12 @@ fun LogScreen(
contentDescription = stringResource(Res.string.Settings_Storage_Delete),
)
}
IconButton(onClick = { onEvent(LogViewModel.Event.ShareClicked) }) {
Icon(
Icons.Default.Share,
contentDescription = stringResource(Res.string.share_logs),
)
}
},
)

Expand Down Expand Up @@ -109,6 +121,19 @@ fun LogScreen(
}
}
}

val snackbarHostState = LocalSnackbarHostState.current ?: return
LaunchedEffect(state.errors) {
val error = state.errors.firstOrNull() ?: return@LaunchedEffect
snackbarHostState.showSnackbar(
getString(
when (error) {
LogViewModel.Error.Share -> Res.string.share_logs_error
},
),
)
onEvent(LogViewModel.Event.ErrorShown(error))
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class LogViewModel(
onBack: () -> Unit,
readLog: (Severity?) -> Flow<List<String>>,
clearLog: suspend () -> Unit,
shareLogFile: suspend () -> Boolean,
) : ViewModel() {
private val events = MutableSharedFlow<Event>(extraBufferCapacity = 1)

Expand All @@ -41,10 +42,24 @@ class LogViewModel(
.onEach { clearLog() }
.launchIn(viewModelScope)

events
.filterIsInstance<Event.ShareClicked>()
.onEach {
if (!shareLogFile()) {
_state.update { it.copy(errors = it.errors + Error.Share) }
}
}
.launchIn(viewModelScope)

events
.filterIsInstance<Event.FilterChanged>()
.onEach { event -> _state.update { it.copy(filter = event.severity) } }
.launchIn(viewModelScope)

events
.filterIsInstance<Event.ErrorShown>()
.onEach { event -> _state.update { it.copy(errors = it.errors - event.error) } }
.launchIn(viewModelScope)
}

fun onEvent(event: Event) {
Expand All @@ -54,13 +69,20 @@ class LogViewModel(
data class State(
val log: List<String> = emptyList(),
val filter: Severity? = null,
val errors: List<Error> = emptyList(),
)

sealed interface Event {
data object BackClicked : Event

data object ClearClicked : Event

data object ShareClicked : Event

data class FilterChanged(val severity: Severity?) : Event

data class ErrorShown(val error: Error) : Event
}

enum class Error { Share }
}
Loading