Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.core.content.IntentCompat
Expand All @@ -33,6 +32,8 @@ import com.gatecontrol.android.ui.theme.GateControlTheme
import com.gatecontrol.android.familie.PluginDirectory
import com.gatecontrol.android.push.PushNavTarget
import com.gatecontrol.android.push.PushNavigation
import com.gatecontrol.android.push.PushOpenActivity
import com.gatecontrol.android.gatedrop.GateDropNotifier
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -167,12 +168,6 @@ class ChatShareActivity : ComponentActivity() {
}
GateControlTheme(darkTheme = dark) {
val ui by gateDrop.ui.collectAsStateWithLifecycle()
LaunchedEffect(ui.stage) {
if (ui.stage == GdShareStage.DONE) {
Toast.makeText(this@ChatShareActivity, R.string.gatedrop_sending_toast, Toast.LENGTH_SHORT).show()
finish()
}
}
GateDropShareSheet(
ui = ui,
serverUrl = serverUrl,
Expand All @@ -193,7 +188,17 @@ class ChatShareActivity : ComponentActivity() {
onCloseLink = gateDrop::closeLinkOptions,
onRetry = gateDrop::loadTargets,
onSend = gateDrop::send,
onDismiss = { finish() },
onDismiss = {
// Closed while it still sends: the worker goes on, its notifications tell the rest.
if (ui.stage == GdShareStage.WAITING || ui.stage == GdShareStage.PREPARING) {
Toast.makeText(this@ChatShareActivity, R.string.gatedrop_sending_toast, Toast.LENGTH_SHORT).show()
}
finish()
},
onOpenShared = {
runCatching { startActivity(PushOpenActivity.sharedIntent(this@ChatShareActivity, GateDropNotifier.TAB_SENT)) }
finish()
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,28 @@ class GateDropNotifier @Inject constructor(
)
}

/**
* Not through yet (server not reachable, busy): the send waits and is
* tried again by itself. Replaced by [sent] / [sendFailed] (same id).
*/
suspend fun sendWaiting(key: String, names: String?) {
val ctx = localized()
ensureChannels(ctx)
val text = ctx.getString(R.string.gatedrop_send_waiting_desc)
post(
resultId(key),
NotificationCompat.Builder(ctx, CHANNEL_PROGRESS)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(ctx.getString(R.string.gatedrop_send_waiting))
.setContentText(names?.let { ctx.getString(R.string.gatedrop_sent_to, it) } ?: text)
.setStyle(NotificationCompat.BigTextStyle().bigText(listOfNotNull(names?.let { ctx.getString(R.string.gatedrop_sent_to, it) }, text).joinToString("\n")))
.setOnlyAlertOnce(true)
.setSilent(true)
.setContentIntent(sharedTap(TAB_SENT, slot = 17))
.build(),
)
}

/** The link for externals is ready: URL (and PIN) with Kopieren / Teilen. */
suspend fun linkReady(key: String, link: GdLinkResult) {
val ctx = localized()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ sealed interface GdJobState {
/** GateDrop 1.4 `423 scan_pending`: a file is still being checked for viruses, the send is asked again soon. */
data object Checking : GdJobState
data class Direct(val percent: Int?) : GdJobState
/** The server was not reachable (or busy): the worker tries again later by itself. */
data object Retrying : GdJobState
data class Done(val sendId: String?, val link: GdLinkResult?) : GdJobState
/** [name]: the file a virus-scan refusal names (`422 infected`, `423 scan_blocked`). */
data class Failed(val code: String?, val name: String? = null) : GdJobState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ class GateDropSendWorker(appContext: Context, params: WorkerParameters) : Corout
}
} catch (e: CancellationException) {
repo.setJobState(jobId, GdJobState.Failed("cancelled"))
// an earlier run's "will try again" notice must not stay behind
notifier.cancel(GateDropNotifier.resultId(jobId))
throw e
}
return when (outcome) {
Expand All @@ -90,7 +92,10 @@ class GateDropSendWorker(appContext: Context, params: WorkerParameters) : Corout
is GdJobOutcome.Retry -> if (runAttemptCount >= MAX_RUNS) {
giveUp(repo, notifier, outcome.code, null)
} else {
repo.setJobState(jobId, GdJobState.Uploading(null))
Timber.i("GateDrop: send not through yet (%s), tried again later", outcome.code)
repo.setJobState(jobId, GdJobState.Retrying)
// Without it nothing would say why the send waits (hours of backoff in the worst case).
notifier.sendWaiting(jobId, inputData.getString(KEY_NAMES))
Result.retry()
}
is GdJobOutcome.Failed -> giveUp(repo, notifier, outcome.code, outcome.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ class TunnelConnector @Inject constructor(
applyVpnSubnet(config)

// Pre-resolve server hostname before the tunnel comes up. Once the
// VPN is established the system DNS points to 10.8.0.1, which is
// unreachable from the GateControl app itself when the app is in
// the user's exclude list.
// VPN is established the system DNS points to 10.8.0.1, which answers
// the server's name with its VPN address — unusable when the app is
// in the user's exclude list (ApiClientProvider / VpnSafeDns).
if (serverUrl.isNotEmpty()) {
try {
val host = java.net.URI(serverUrl).host
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ fun GateDropShareSheet(
onRetry: () -> Unit,
onSend: () -> Unit,
onDismiss: () -> Unit,
/** Opens GateDrop's "Geteilt" (Gesendet / Empfangen) in the app. */
onOpenShared: () -> Unit = {},
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
Expand All @@ -126,8 +128,9 @@ fun GateDropShareSheet(
GdShareStage.LINK_OPTIONS -> LinkOptions(ui, onLinkDraft, onApplyLink, onRemoveLink, onCloseLink)
GdShareStage.PREPARING, GdShareStage.WAITING -> Progress(ui)
GdShareStage.RESULT -> LinkResult(ui, onDismiss)
GdShareStage.FAILED -> Failed(ui.failure, onDismiss)
else -> Main(ui, serverUrl, onShareInChat, onTogglePerson, onToggleDevice, onToggleNas, onToggleDrive, onNote, onOpenLink, onRetry, onSend)
GdShareStage.SENT -> Sent(ui, onDismiss, onOpenShared)
GdShareStage.FAILED -> Failed(ui.failure, onDismiss, onOpenShared)
else -> Main(ui, serverUrl, onShareInChat, onTogglePerson, onToggleDevice, onToggleNas, onToggleDrive, onNote, onOpenLink, onRetry, onSend, onOpenShared)
}
}
}
Expand Down Expand Up @@ -167,6 +170,7 @@ private fun Main(
onOpenLink: () -> Unit,
onRetry: () -> Unit,
onSend: () -> Unit,
onOpenShared: () -> Unit,
) {
val extra = GateControlTheme.extraColors
Header(ui)
Expand Down Expand Up @@ -278,6 +282,8 @@ private fun Main(
if (onShareInChat != null) {
GcSecondaryButton(text = stringResource(R.string.gatedrop_in_chat), onClick = onShareInChat)
}
// GateDrop's history lives in Settings › Geteilt: reachable from where GateDrop is used.
TextButton(onClick = onOpenShared, modifier = Modifier.fillMaxWidth()) { Text(stringResource(R.string.gatedrop_open_shared)) }
}

@Composable
Expand Down Expand Up @@ -453,17 +459,23 @@ private fun Progress(ui: GdShareUi) {
Header(ui)
Column(Modifier.fillMaxWidth().padding(vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
val p = ui.percent
val toLink = ui.selection.link != null
Text(
when {
ui.stage == GdShareStage.PREPARING -> stringResource(R.string.gatedrop_preparing)
ui.retrying -> stringResource(R.string.gatedrop_send_waiting_desc)
ui.checking -> stringResource(R.string.scan_detail_checking)
p != null -> stringResource(R.string.gatedrop_uploading_percent, p)
else -> stringResource(R.string.gatedrop_waiting_link)
p != null && toLink -> stringResource(R.string.gatedrop_uploading_percent, p)
p != null -> stringResource(R.string.gatedrop_sending_percent, p)
toLink -> stringResource(R.string.gatedrop_waiting_link)
else -> stringResource(R.string.gatedrop_progress_sending)
},
style = MaterialTheme.typography.bodyMedium,
color = extra.muted,
color = if (ui.retrying) MaterialTheme.colorScheme.onSurface else extra.muted,
)
ui.targetNames?.let { Text(stringResource(R.string.gatedrop_sent_to, it), style = MaterialTheme.typography.bodySmall, color = extra.muted) }
if (p != null) LinearProgressIndicator(progress = { p / 100f }, modifier = Modifier.fillMaxWidth()) else LinearProgressIndicator(Modifier.fillMaxWidth())
if (ui.unread > 0) Text(pluralStringResource(R.plurals.gatedrop_unread, ui.unread, ui.unread), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface)
Text(stringResource(R.string.gatedrop_progress_background), style = MaterialTheme.typography.bodySmall, color = extra.faint)
}
}
Expand Down Expand Up @@ -510,12 +522,27 @@ private fun LinkResult(ui: GdShareUi, onDone: () -> Unit) {
}

@Composable
private fun Failed(reason: String?, onDone: () -> Unit) {
private fun Failed(reason: String?, onDone: () -> Unit, onOpenShared: () -> Unit) {
GcBanner(tone = GcTone.Error, icon = GcIcons.Alert) {
Column {
Text(stringResource(R.string.gatedrop_send_failed), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface)
Text(reason ?: stringResource(R.string.gatedrop_send_failed_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface)
}
}
GcOutlineButton(text = stringResource(R.string.gatedrop_open_shared), onClick = onOpenShared)
GcSecondaryButton(text = stringResource(R.string.gatedrop_done), onClick = onDone)
}

/** Delivered (no link for externals): "Gesendet an …", the way to "Geteilt › Gesendet". */
@Composable
private fun Sent(ui: GdShareUi, onDone: () -> Unit, onOpenShared: () -> Unit) {
GcBanner(tone = GcTone.Ok, icon = GcIcons.Check) {
Column {
Text(stringResource(R.string.gatedrop_sent_title), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface)
ui.targetNames?.let { Text(stringResource(R.string.gatedrop_sent_to, it), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface) }
if (ui.unread > 0) Text(pluralStringResource(R.plurals.gatedrop_unread, ui.unread, ui.unread), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface)
}
}
GcOutlineButton(text = stringResource(R.string.gatedrop_open_shared), onClick = onOpenShared)
GcPrimaryButton(text = stringResource(R.string.gatedrop_done), onClick = onDone)
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import javax.inject.Inject
/** A file shared from another app (read only when sending). */
data class GdSource(val uri: Uri, val name: String, val mime: String, val size: Long?)

/** What the share sheet shows. */
enum class GdShareStage { EDIT, LINK_OPTIONS, PREPARING, WAITING, RESULT, DONE, FAILED }
/** What the share sheet shows ([SENT]: delivered, without a link for externals). */
enum class GdShareStage { EDIT, LINK_OPTIONS, PREPARING, WAITING, RESULT, SENT, FAILED }

data class GdShareUi(
val sources: List<GdSource> = emptyList(),
Expand All @@ -60,6 +60,12 @@ data class GdShareUi(
val checking: Boolean = false,
/** Why the send failed when the virus scan refused it ("<Name> abgelehnt – Schadsoftware gefunden."). */
val failure: String? = null,
/** The server was not reachable: the send waits and is tried again by itself. */
val retrying: Boolean = false,
/** Shared files that could not be read when sending (left out of the send). */
val unread: Int = 0,
/** Who it goes to ("FEC Laptop, Mein Drive") for "Gesendet an …". */
val targetNames: String? = null,
) {
val totalBytes: Long get() = sources.sumOf { it.size ?: 0L }
val canSend: Boolean get() = targets != null && GateDropLogic.canSend(selection, targets, sources.size, !text.isNullOrBlank())
Expand Down Expand Up @@ -124,6 +130,8 @@ class GateDropShareViewModel @Inject constructor(
_ui.update { it.copy(stage = GdShareStage.FAILED) }
return@launch
}
val unread = s.sources.size - files.size
if (unread > 0) Timber.w("GateDrop: %d of %d shared files not readable, left out", unread, s.sources.size)
val to = GateDropLogic.recipients(s.selection, targets)
val shareLink = if (files.isEmpty()) GateDropLogic.asLink(s.text) else null
val job = GdJob(
Expand All @@ -138,24 +146,28 @@ class GateDropShareViewModel @Inject constructor(
repository.store.saveJob(job)
repository.setJobState(jobId, GdJobState.Preparing)
val names = GateDropLogic.chosenNames(s.selection, targets, getApplication<Application>().getString(com.gatecontrol.android.R.string.gatedrop_drive)).joinToString(", ").ifBlank { null }
GateDropSendWorker.enqueue(getApplication(), jobId, names)
if (to.link == null) {
_ui.update { it.copy(stage = GdShareStage.DONE) }
if (GateDropSendWorker.enqueue(getApplication(), jobId, names) == null) {
repository.store.dropJob(jobId)
_ui.update { it.copy(stage = GdShareStage.FAILED) }
return@launch
}
_ui.update { it.copy(stage = GdShareStage.WAITING) }
// The sheet stays and shows how the send goes (progress, "Gesendet an …", the error); closing it
// leaves the send to the worker and its notifications.
_ui.update { it.copy(stage = GdShareStage.WAITING, unread = unread, targetNames = names) }
waitJob = launch {
repository.jobs.mapNotNull { it[jobId] }.first { st ->
when (st) {
is GdJobState.Uploading -> _ui.update { it.copy(percent = st.percent, checking = false) }
is GdJobState.Direct -> _ui.update { it.copy(percent = st.percent, checking = false) }
GdJobState.Checking -> _ui.update { it.copy(checking = true) }
is GdJobState.Uploading -> _ui.update { it.copy(percent = st.percent, checking = false, retrying = false) }
is GdJobState.Direct -> _ui.update { it.copy(percent = st.percent, checking = false, retrying = false) }
GdJobState.Sending -> _ui.update { it.copy(checking = false, retrying = false) }
GdJobState.Checking -> _ui.update { it.copy(checking = true, retrying = false) }
GdJobState.Retrying -> _ui.update { it.copy(retrying = true, checking = false, percent = null) }
else -> Unit
}
st is GdJobState.Done || st is GdJobState.Failed
}.let { st ->
when (st) {
is GdJobState.Done -> _ui.update { it.copy(stage = if (st.link != null) GdShareStage.RESULT else GdShareStage.DONE, link = st.link) }
is GdJobState.Done -> _ui.update { it.copy(stage = if (st.link != null) GdShareStage.RESULT else GdShareStage.SENT, link = st.link, retrying = false) }
is GdJobState.Failed -> _ui.update {
it.copy(stage = GdShareStage.FAILED, failure = scanCodeText(getApplication(), st.code, name = st.name))
}
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/res/values-de/strings_gatedrop.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<string name="gatedrop_preparing">Wird vorbereitet …</string>
<string name="gatedrop_waiting_link">Link wird erstellt …</string>
<string name="gatedrop_uploading_percent">Wird hochgeladen … %1$d %%</string>
<string name="gatedrop_sending_percent">Wird gesendet … %1$d %%</string>
<string name="gatedrop_progress_background">Du kannst das schließen – das Senden läuft im Hintergrund weiter.</string>
<string name="gatedrop_sending_toast">Wird gesendet …</string>
<string name="gatedrop_done">Fertig</string>
Expand Down Expand Up @@ -87,6 +88,13 @@
<string name="gatedrop_link_with_pin">%1$s\nPIN: %2$s</string>
<string name="gatedrop_send_failed">Nicht gesendet</string>
<string name="gatedrop_send_failed_desc">Dein GateControl-Server war nicht erreichbar oder hat es abgelehnt.</string>
<string name="gatedrop_send_waiting">Noch nicht gesendet</string>
<string name="gatedrop_send_waiting_desc">Dein GateControl-Server ist gerade nicht erreichbar – GateControl versucht es von selbst erneut.</string>
<string name="gatedrop_open_shared">Gesendet und Empfangen ansehen</string>
<plurals name="gatedrop_unread">
<item quantity="one">%d Datei konnte nicht gelesen werden und wird nicht gesendet.</item>
<item quantity="other">%d Dateien konnten nicht gelesen werden und werden nicht gesendet.</item>
</plurals>
<string name="gatedrop_someone">Jemand</string>
<string name="gatedrop_received_title">%1$s hat dir %2$s geschickt</string>
<string name="gatedrop_received_generic">GateDrop: etwas Neues für dich</string>
Expand All @@ -105,7 +113,7 @@
<string name="gatedrop_receive_failed_from">Von %1$s</string>

<!-- Seite „Geteilt“ -->
<string name="gatedrop_settings_group">Teilen</string>
<string name="gatedrop_settings_group">GateDrop</string>
<string name="gatedrop_settings_row_desc">Empfangen, Gesendet und Links für Externe</string>
<string name="gatedrop_shared_title">Geteilt</string>
<string name="gatedrop_receive_from_title">Wer darf an dieses Gerät senden?</string>
Expand Down
Loading
Loading