Skip to content
Open
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 @@ -41,6 +41,7 @@ class DebugScreenComposeTest {
onDeleteLogs = {},
onDatabaseLoggerEnabledChanged = {},
onFlushLogs = { CompletableDeferred(Unit) },
onShareLogsViaWire = {},
debugDataOptionsContent = {},
dangerOptionsContent = {},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.wire.android.R
import com.wire.android.util.EmailComposer
import com.wire.android.util.SupportPage
import com.wire.android.util.SupportUrlResolver
import com.wire.android.util.externalShareChooserIntent
import com.wire.android.util.getDeviceIdString
import com.wire.android.util.getGitBuildId
import com.wire.android.util.sha256
Expand Down Expand Up @@ -114,7 +115,7 @@ object GiveFeedbackDestination : IntentDirection {
)
)
intent.selector = Intent(Intent.ACTION_SENDTO).setData(Uri.parse("mailto:"))
return Intent.createChooser(intent, context.getString(R.string.send_feedback_choose_email))
return context.externalShareChooserIntent(intent, context.getString(R.string.send_feedback_choose_email))
}

override val route: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
*/
package com.wire.android.ui

import android.content.Context
import androidx.lifecycle.SavedStateHandle
import com.wire.android.datastore.UserDataStoreProvider
import com.wire.android.di.ApplicationContext
import com.wire.android.di.CurrentAccount
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.ui.analytics.AnalyticsConfiguration
Expand Down Expand Up @@ -51,6 +53,7 @@

@Suppress("LongParameterList")
class MiscViewModelFactory @Inject constructor(
@ApplicationContext private val context: Context,

Check warning on line 56 in app/src/main/kotlin/com/wire/android/ui/MiscViewModelFactory.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/MiscViewModelFactory.kt#L56

Added line #L56 was not covered by tests
private val analyticsEnabled: AnalyticsConfiguration,
private val selfServerConfig: Lazy<SelfServerConfigUseCase>,
private val observeSyncState: ObserveSyncStateUseCase,
Expand Down Expand Up @@ -110,6 +113,7 @@
)

fun importMediaAuthenticatedViewModel() = ImportMediaAuthenticatedViewModel(
context = context,

Check warning on line 116 in app/src/main/kotlin/com/wire/android/ui/MiscViewModelFactory.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/MiscViewModelFactory.kt#L116

Added line #L116 was not covered by tests
getSelf = getSelf,
getConversationsPaginated = getConversationsPaginated,
handleUriAsset = handleUriAsset,
Expand Down
70 changes: 59 additions & 11 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -153,6 +154,8 @@ import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedDialog
import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedState
import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedViewModel
import com.wire.android.ui.settings.devices.e2ei.E2EICertificateDetails
import com.wire.android.ui.sharing.hasTrustedWireShareCaller
import com.wire.android.ui.sharing.sharingUris
import com.wire.android.ui.theme.ThemeOption
import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.userprofile.self.LocalSelfUserProfileLogoutAction
Expand All @@ -165,6 +168,7 @@ import com.wire.android.util.SwitchAccountObserver
import com.wire.android.util.SyncStateObserver
import com.wire.android.util.debug.FeatureVisibilityFlags
import com.wire.android.util.debug.LocalFeatureVisibilityFlags
import com.wire.android.util.getProviderAuthority
import com.wire.android.util.launchUpdateTheApp
import com.wire.kalium.logic.data.user.UserId
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -214,7 +218,14 @@ class WireActivity : BaseActivity() {
}
}

private val newIntents = Channel<Pair<Intent, Bundle?>>(Channel.UNLIMITED) // keep new intents until subscribed but do not replay them
private data class QueuedIntent(
val intent: Intent,
val savedInstanceState: Bundle?,
val hasTrustedWireShareCaller: Boolean
)

// Keep new intents until subscribed but do not replay them.
private val newIntents = Channel<QueuedIntent>(Channel.UNLIMITED)
private lateinit var shakeDetector: ShakeDetector

// This flag is used to keep the splash screen open until the first screen is drawn.
Expand All @@ -232,7 +243,7 @@ class WireActivity : BaseActivity() {
wireApplicationGraph.inject(this)
super.onCreate(savedInstanceState)
splashScreen.setKeepOnScreenCondition { shouldKeepSplashOpen }
traceStartup("activity.onCreate.afterSuper", startupAt)
val initialQueuedIntent = captureInitialIntent(startupAt, savedInstanceState)

enableEdgeToEdge()
setupOrientationForDevice()
Expand Down Expand Up @@ -283,7 +294,7 @@ class WireActivity : BaseActivity() {
(application as? WireApplication)?.initializeDeferredLoggingAfterSplash()
traceStartup("activity.deferredLogging.triggered", startupAt)

handleNewIntent(intent, savedInstanceState)
handleNewIntent(initialQueuedIntent)
traceStartup("activity.initialIntent.dispatched", startupAt)
}

Expand Down Expand Up @@ -324,12 +335,38 @@ class WireActivity : BaseActivity() {
handleSynchronizeExternalData(intent)
return
}
setIntent(intent)
handleNewIntent(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
setIntentWithCurrentCaller(intent)
} else {
setIntent(intent)
}
handleNewIntent(queuedIntent(intent))
}

@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
private fun setIntentWithCurrentCaller(intent: Intent) {
setIntent(intent, getCurrentCaller())
}

private fun queuedIntent(intent: Intent, savedInstanceState: Bundle? = null): QueuedIntent {
val providerAuthority = getProviderAuthority()
return QueuedIntent(
intent = intent,
savedInstanceState = savedInstanceState,
hasTrustedWireShareCaller = hasTrustedWireShareCaller(
providerAuthority = providerAuthority,
uris = intent.sharingUris()
)
)
}

private fun captureInitialIntent(startupAt: Long, savedInstanceState: Bundle?): QueuedIntent {
traceStartup("activity.onCreate.afterSuper", startupAt)
return queuedIntent(intent, savedInstanceState)
}

private fun handleNewIntent(intent: Intent, savedInstanceState: Bundle? = null) = lifecycleScope.launch {
newIntents.send(intent to savedInstanceState)
private fun handleNewIntent(queuedIntent: QueuedIntent) = lifecycleScope.launch {
newIntents.send(queuedIntent)
}

private fun setComposableContent(startDestination: Direction) {
Expand Down Expand Up @@ -848,9 +885,14 @@ class WireActivity : BaseActivity() {
newIntents
.receiveAsFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collectLatest { (intent, savedInstanceState) ->
.collectLatest { queuedIntent ->
currentKeyboardController?.hide()
handleDeepLinkOrIntent(currentNavigator, intent, savedInstanceState)
handleDeepLinkOrIntent(
navigator = currentNavigator,
intent = queuedIntent.intent,
savedInstanceState = queuedIntent.savedInstanceState,
hasTrustedWireShareCaller = queuedIntent.hasTrustedWireShareCaller
)
}
}
}
Expand Down Expand Up @@ -1223,7 +1265,8 @@ class WireActivity : BaseActivity() {
private suspend fun handleDeepLinkOrIntent(
navigator: Navigator,
intent: Intent?,
savedInstanceState: Bundle? = null
savedInstanceState: Bundle? = null,
hasTrustedWireShareCaller: Boolean = false
) {
val navigate: (NavigationCommand) -> Unit = {
runOnUiThread {
Expand All @@ -1246,7 +1289,12 @@ class WireActivity : BaseActivity() {
} else {
val handled = viewModel.handleIntentsThatAreNotDeepLinks(intent)
if (!handled) {
viewModel.handleDeepLink(intent)
val providerAuthority = getProviderAuthority()
viewModel.handleDeepLink(
intent = intent,
providerAuthority = providerAuthority,
hasTrustedWireShareCaller = hasTrustedWireShareCaller
)
intent.putExtra(HANDLED_DEEPLINK_FLAG, true)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import com.ramcosta.composedestinations.generated.app.destinations.WelcomeScreenDestination
import com.wire.android.ui.authentication.login.LoginPasswordPath
import com.wire.android.ui.newauthentication.login.NewLoginViewModel
import com.wire.android.ui.sharing.ImportMediaNavArgs
import com.wire.android.ui.sharing.ImportSource
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -86,10 +88,10 @@

private fun openImportMediaScreen(navigator: Navigator) {
navigator.navigate(
NavigationCommand(
ImportMediaScreenDestination,
BackStackMode.UPDATE_EXISTED
)
NavigationCommand(
ImportMediaScreenDestination(ImportMediaNavArgs(source = ImportSource.EXTERNAL_SHARE)),
BackStackMode.UPDATE_EXISTED

Check warning on line 93 in app/src/main/kotlin/com/wire/android/ui/WireActivityActionsHandler.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/WireActivityActionsHandler.kt#L91-L93

Added lines #L91 - L93 were not covered by tests
)
)
}

Expand Down
37 changes: 35 additions & 2 deletions app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState
import com.wire.android.ui.common.dialogs.CustomServerDialogState
import com.wire.android.ui.common.dialogs.CustomServerNoNetworkDialogState
import com.wire.android.ui.joinConversation.JoinConversationViaCodeState
import com.wire.android.ui.sharing.sharingUris
import com.wire.android.ui.sharing.shouldRejectSharingIntent
import com.wire.android.ui.theme.Accent
import com.wire.android.ui.theme.ThemeOption
import com.wire.android.util.BackendSupportConfig
Expand Down Expand Up @@ -114,6 +116,8 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.InputStream
import java.io.InputStreamReader
import dev.zacsweers.metro.Inject
Expand Down Expand Up @@ -441,7 +445,11 @@ class WireActivityViewModel @Inject constructor(
}

@Suppress("ComplexMethod")
fun handleDeepLink(intent: Intent?) {
fun handleDeepLink(
intent: Intent?,
providerAuthority: String? = null,
hasTrustedWireShareCaller: Boolean = false
) {
viewModelScope.launch(dispatchers.io()) {
when (val result = deepLinkProcessor.value.invoke(intent?.data, intent?.action)) {
DeepLinkResult.AuthorizationNeeded -> sendAction(OnAuthorizationNeeded)
Expand Down Expand Up @@ -470,7 +478,16 @@ class WireActivityViewModel @Inject constructor(
is DeepLinkResult.OpenConversation -> sendAction(OpenConversation(result))
is DeepLinkResult.OpenOtherUserProfile -> onOpenUserProfileDeepLink(result)

DeepLinkResult.SharingIntent -> sendAction(OnShowImportMediaScreen)
DeepLinkResult.SharingIntent -> {
val shouldRejectSharingIntent = providerAuthority != null &&
intent?.shouldRejectSharingIntent(providerAuthority, hasTrustedWireShareCaller) == true
if (shouldRejectSharingIntent) {
logRejectedSharingIntent(intent, providerAuthority, hasTrustedWireShareCaller)
sendAction(ShowToast(R.string.public_share_ignored_wire_internal_files))
return@launch
}
sendAction(OnShowImportMediaScreen)
}
DeepLinkResult.Unknown -> {
sendAction(OnUnknownDeepLink)
appLogger.e("unknown deeplink result $result")
Expand All @@ -479,6 +496,22 @@ class WireActivityViewModel @Inject constructor(
}
}

private fun logRejectedSharingIntent(
intent: Intent?,
providerAuthority: String?,
hasTrustedWireShareCaller: Boolean
) {
val logMap = mapOf(
"event" to "public_share_rejected",
"reason" to "wire_file_provider_uri",
"action" to intent?.action.orEmpty(),
"providerAuthority" to providerAuthority.orEmpty(),
"hasTrustedWireShareCaller" to hasTrustedWireShareCaller.toString(),
"uriCount" to (intent?.sharingUris()?.size ?: 0).toString()
)
appLogger.w("Rejected public share intent: ${Json.encodeToString(logMap)}")
}

// Returns whether an intent was handled, or if there was nothing to do
@Suppress("ReturnCount")
suspend fun handleIntentsThatAreNotDeepLinks(intent: Intent?): Boolean {
Expand Down
36 changes: 34 additions & 2 deletions app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.wire.android.ui.debug

import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
Expand Down Expand Up @@ -51,10 +52,13 @@ import com.wire.android.ui.common.topappbar.NavigationIconType
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
import com.ramcosta.composedestinations.generated.app.destinations.ConversationCryptoStatsScreenDestination
import com.ramcosta.composedestinations.generated.app.destinations.DebugFeatureFlagsScreenDestination
import com.ramcosta.composedestinations.generated.app.destinations.ImportMediaScreenDestination
import com.wire.android.ui.common.rowitem.SectionHeader
import com.wire.android.ui.home.settings.SettingsItem
import com.wire.android.ui.home.settings.backup.BackupAndRestoreDialog
import com.wire.android.ui.home.settings.backup.rememberBackUpAndRestoreStateHolder
import com.wire.android.ui.sharing.ImportMediaNavArgs
import com.wire.android.ui.sharing.ImportSource
import com.wire.android.ui.theme.WireTheme
import com.wire.android.util.AppNameUtil
import com.wire.android.util.logging.LogShareLauncher
Expand Down Expand Up @@ -94,6 +98,18 @@ fun DebugScreen(
dangerOptionsContent = {
DangerOptions(exportObfuscatedCopyViewModel = exportObfuscatedCopyViewModel)
},
onShareLogsViaWire = { uri ->
navigator.navigate(
NavigationCommand(
ImportMediaScreenDestination(
ImportMediaNavArgs(
source = ImportSource.INTERNAL_SHARE,
internalAssetUriList = arrayListOf(uri)
)
)
)
)
},
)
}

Expand All @@ -105,6 +121,7 @@ internal fun UserDebugContent(
onDatabaseLoggerEnabledChanged: (Boolean) -> Unit,
onDeleteLogs: () -> Unit,
onFlushLogs: () -> Deferred<Unit>,
onShareLogsViaWire: (Uri) -> Unit,
debugDataOptionsContent: @Composable (DebugContentState) -> Unit,
dangerOptionsContent: @Composable () -> Unit,
) {
Expand All @@ -131,7 +148,8 @@ internal fun UserDebugContent(
isLoggingEnabled = isLoggingEnabled,
onLoggingEnabledChange = onLoggingEnabledChange,
onDeleteLogs = onDeleteLogs,
onShareLogs = { debugContentState.shareLogs(onFlushLogs) },
onShareLogsExternally = { debugContentState.shareLogsExternally(onFlushLogs) },
onShareLogsViaWire = { debugContentState.shareLogsViaWire(onFlushLogs, onShareLogsViaWire) },
isDBLoggerEnabled = state.isDBLoggingEnabled,
onDBLoggerEnabledChange = onDatabaseLoggerEnabledChanged,
isPrivateBuild = BuildConfig.PRIVATE_BUILD,
Expand Down Expand Up @@ -230,7 +248,7 @@ data class DebugContentState(
).show()
}

fun shareLogs(onFlushLogs: () -> Deferred<Unit>) {
fun shareLogsExternally(onFlushLogs: () -> Deferred<Unit>) {
val dir = File(logPath).parentFile
if (dir != null && dir.exists()) {
logShareLauncher.shareLogs(dir) {
Expand All @@ -239,6 +257,19 @@ data class DebugContentState(
}
}
}

fun shareLogsViaWire(onFlushLogs: () -> Deferred<Unit>, onShareUri: (Uri) -> Unit) {
val dir = File(logPath).parentFile
if (dir != null && dir.exists()) {
logShareLauncher.shareLogsViaWire(
logsDirectory = dir,
onShareUri = onShareUri
) {
// Flush any buffered logs before sharing to ensure completeness.
onFlushLogs().await()
}
}
}
}

@Preview(heightDp = 1400)
Expand All @@ -253,6 +284,7 @@ internal fun PreviewUserDebugContent() = WireTheme {
onLoggingEnabledChange = {},
onDeleteLogs = {},
onFlushLogs = { CompletableDeferred(Unit) },
onShareLogsViaWire = {},
onDatabaseLoggerEnabledChanged = {},
debugDataOptionsContent = {
DebugDataOptions(
Expand Down
Loading
Loading