diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index fbcf0e169..9d2db540d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.compose.compiler)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
}
@@ -163,6 +164,7 @@ dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.compose.material3)
implementation(libs.compose.materialIconsExtended)
+ implementation(libs.compose.navigation)
implementation(libs.compose.runtime.livedata)
debugImplementation(libs.compose.ui.tooling)
implementation(libs.compose.ui.toolingPreview)
@@ -189,6 +191,7 @@ dependencies {
@Suppress("RedundantSuppression")
implementation(libs.dnsjava)
implementation(libs.guava)
+ implementation(libs.kotlinx.serialization)
implementation(libs.mikepenz.aboutLibraries)
implementation(libs.nsk90.kstatemachine)
implementation(libs.okhttp.base)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 173b80fc0..6bbd778c7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -62,23 +62,35 @@
+
+
+
+
+
+
+
+
+
+ android:parentActivityName=".ui.MainActivity"/>
@@ -106,7 +118,7 @@
@@ -134,7 +146,7 @@
+ android:parentActivityName=".ui.MainActivity" />
- if (cancelled)
- finish()
- }
-
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- // handle "Sync all" intent from launcher shortcut
- val syncAccounts = intent.action == Intent.ACTION_SYNC
+ val uri = Destination.Accounts.PATH.toUri().buildUpon()
+ val syncOnLaunch = intent.action == Intent.ACTION_SYNC
+ if (syncOnLaunch)
+ uri.appendQueryParameter("syncAccounts", "true")
- setContent {
- AccountsScreen(
- initialSyncAccounts = syncAccounts,
- onShowAppIntro = {
- introActivityLauncher.launch(null)
- },
- accountsDrawerHandler = accountsDrawerHandler,
- onAddAccount = {
- startActivity(Intent(this, LoginActivity::class.java))
- },
- onShowAccount = { account ->
- val intent = Intent(this, AccountActivity::class.java)
- intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
- startActivity(intent)
- },
- onManagePermissions = {
- startActivity(Intent(this, PermissionsActivity::class.java))
- }
- )
- }
+ MainActivity.legacyRedirect(
+ activity = this,
+ uri = uri.build()
+ )
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt
index dc718ac99..d0923ea8e 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt
@@ -50,6 +50,7 @@ import java.util.logging.Logger
class AccountsModel @AssistedInject constructor(
@Assisted val syncAccountsOnInit: Boolean,
private val accountRepository: AccountRepository,
+ internal val accountsDrawerHandler: AccountsDrawerHandler,
@ApplicationContext val context: Context,
private val db: AppDatabase,
introPageFactory: IntroPageFactory,
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt
index e0ec50d83..8bd655234 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt
@@ -6,11 +6,13 @@ package at.bitfire.davdroid.ui
import android.Manifest
import android.accounts.Account
+import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.activity.compose.BackHandler
+import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -73,20 +75,55 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
+import at.bitfire.davdroid.ui.account.AccountActivity
import at.bitfire.davdroid.ui.account.AccountProgress
import at.bitfire.davdroid.ui.composable.ActionCard
import at.bitfire.davdroid.ui.composable.ProgressBar
+import at.bitfire.davdroid.ui.intro.IntroActivity
+import at.bitfire.davdroid.ui.setup.LoginActivity
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+@Composable
+fun AccountsScreen(
+ initialSyncAccounts: Boolean
+) {
+ val context = LocalContext.current
+ val activity = context as? Activity
+
+ val introActivityLauncher = rememberLauncherForActivityResult(IntroActivity.Contract) { cancelled ->
+ if (cancelled) activity?.finish()
+ }
+
+ AccountsScreen(
+ initialSyncAccounts = initialSyncAccounts,
+ onShowAppIntro = {
+ introActivityLauncher.launch(null)
+ },
+ onAddAccount = {
+ // eventually this will become a navigation
+ context.startActivity(Intent(context, LoginActivity::class.java))
+ },
+ onShowAccount = { account ->
+ // eventually this will become a navigation
+ val intent = Intent(context, AccountActivity::class.java)
+ intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
+ context.startActivity(intent)
+ },
+ onManagePermissions = {
+ // eventually this will become a navigation
+ context.startActivity(Intent(context, PermissionsActivity::class.java))
+ }
+ )
+}
+
@Composable
fun AccountsScreen(
initialSyncAccounts: Boolean,
onShowAppIntro: () -> Unit,
- accountsDrawerHandler: AccountsDrawerHandler,
onAddAccount: () -> Unit,
onShowAccount: (Account) -> Unit,
onManagePermissions: () -> Unit,
@@ -111,7 +148,7 @@ fun AccountsScreen(
}
AccountsScreen(
- accountsDrawerHandler = accountsDrawerHandler,
+ accountsDrawerHandler = model.accountsDrawerHandler,
accounts = accounts,
showSyncAll = showSyncAll,
onSyncAll = { model.syncAllAccounts() },
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/MainActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/MainActivity.kt
new file mode 100644
index 000000000..abb877c89
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/MainActivity.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
+ */
+
+package at.bitfire.davdroid.ui
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.annotation.UiThread
+import androidx.appcompat.app.AppCompatActivity
+import at.bitfire.davdroid.ui.navigation.Destination
+import at.bitfire.davdroid.ui.navigation.Navigation
+import dagger.hilt.android.AndroidEntryPoint
+import java.util.logging.Logger
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class MainActivity: AppCompatActivity() {
+
+ @Inject
+ lateinit var logger: Logger
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ Navigation(
+ deepLinkUri = intent.data,
+ logger = logger
+ )
+ }
+ }
+
+
+ companion object {
+
+ fun intentWithDestination(context: Context, uri: Uri) =
+ Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java).apply {
+ // Create a new activity, do not allow going back.
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+
+ /**
+ * Starts [MainActivity] as a redirect of a legacy activity.
+ *
+ * @param activity The activity that is requesting the redirection.
+ * @param uri The URI to launch. Should have schema of [Destination.APP_BASE_URI].
+ */
+ @UiThread
+ fun legacyRedirect(activity: Activity, uri: Uri) {
+ activity.startActivity(intentWithDestination(activity, uri))
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt
index 4b1038a79..53ebe365e 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt
@@ -5,7 +5,6 @@
package at.bitfire.davdroid.ui
import android.content.Context
-import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.Typeface
@@ -34,10 +33,12 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.core.content.getSystemService
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
+import androidx.core.net.toUri
import androidx.core.text.getSpans
import at.bitfire.davdroid.R
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
+import at.bitfire.davdroid.ui.navigation.Destination
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
@@ -86,7 +87,12 @@ object UiUtils {
ShortcutInfo.Builder(context, SHORTCUT_SYNC_ALL)
.setIcon(Icon.createWithResource(context, R.drawable.ic_sync_shortcut))
.setShortLabel(context.getString(R.string.accounts_sync_all))
- .setIntent(Intent(Intent.ACTION_SYNC, null, context, AccountsActivity::class.java))
+ .setIntent(
+ MainActivity.intentWithDestination(
+ context,
+ Destination.Accounts.PATH.toUri().buildUpon().appendQueryParameter("syncAccounts", "true").build()
+ )
+ )
.build()
)
} catch(e: Exception) {
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/Destination.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/Destination.kt
new file mode 100644
index 000000000..0c0bc3093
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/Destination.kt
@@ -0,0 +1,24 @@
+package at.bitfire.davdroid.ui.navigation
+
+import androidx.navigation.navDeepLink
+import kotlinx.serialization.Serializable
+
+object Destination {
+
+ /**
+ * Base URI for internal (non-exposed) deep links (used for navigation).
+ */
+ private const val APP_BASE_URI = "nav:"
+
+ @Serializable
+ data class Accounts(val syncAccounts: Boolean = false) {
+ companion object {
+ const val PATH = "$APP_BASE_URI/accounts"
+
+ val deepLinks = listOf(
+ navDeepLink(basePath = PATH)
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/Navigation.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/Navigation.kt
new file mode 100644
index 000000000..4b01b5450
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/Navigation.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
+ */
+
+package at.bitfire.davdroid.ui.navigation
+
+import android.net.Uri
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.navigation.NavDeepLinkRequest
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.toRoute
+import at.bitfire.davdroid.ui.AccountsScreen
+import java.util.logging.Logger
+
+@Composable
+fun Navigation(deepLinkUri: Uri?, logger: Logger) {
+ val navController = rememberNavController()
+
+ LaunchedEffect(Unit) {
+ if (deepLinkUri != null) {
+ val deepLinkRq = NavDeepLinkRequest.Builder.fromUri(deepLinkUri).build()
+ logger.info("Got deep link: $deepLinkUri → $deepLinkRq")
+ navController.navigate(
+ deepLinkRq,
+ NavOptions.Builder().setLaunchSingleTop(true).build()
+ )
+ }
+ }
+
+ NavHost(
+ navController = navController,
+ startDestination = Destination.Accounts()
+ ) {
+ composable(
+ deepLinks = Destination.Accounts.deepLinks
+ ) { backStackEntry ->
+ val route = backStackEntry.toRoute()
+ AccountsScreen(
+ initialSyncAccounts = route.syncAccounts
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt b/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt
index 3efcf73f0..6c20518ba 100644
--- a/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt
+++ b/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt
@@ -23,9 +23,6 @@ interface OseFlavorModules {
@Module
@InstallIn(ActivityComponent::class)
interface ForActivities {
- @Binds
- fun accountsDrawerHandler(impl: OseAccountsDrawerHandler): AccountsDrawerHandler
-
@Binds
fun loginTypesProvider(impl: StandardLoginTypesProvider): LoginTypesProvider
}
@@ -33,6 +30,9 @@ interface OseFlavorModules {
@Module
@InstallIn(ViewModelComponent::class)
interface ForViewModels {
+ @Binds
+ fun accountsDrawerHandler(impl: OseAccountsDrawerHandler): AccountsDrawerHandler
+
@Binds
fun appLicenseInfoProvider(impl: OpenSourceLicenseInfoProvider): AboutActivity.AppLicenseInfoProvider
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b2f9722c5..fa4f092b5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -24,6 +24,7 @@ bitfire-ical4android = "12df9bfddb"
bitfire-vcard4android = "ae5d609f92"
compose-accompanist = "0.37.0"
compose-bom = "2024.12.01"
+compose-navigation = "2.8.5"
dnsjava = "3.6.0"
glance = "1.1.1"
guava = "33.4.0-android"
@@ -31,6 +32,7 @@ hilt = "2.55"
# keep in sync with ksp version
kotlin = "2.1.0"
kotlinx-coroutines = "1.10.1"
+kotlinx-serialization = "1.7.3"
# see https://github.com/google/ksp/releases for version numbers
ksp = "2.1.0-1.0.29"
mikepenz-aboutLibraries = "11.4.0"
@@ -72,6 +74,7 @@ compose-accompanist-permissions = { module = "com.google.accompanist:accompanist
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-materialIconsExtended = { module = "androidx.compose.material:material-icons-extended" }
+compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "compose-navigation" }
compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-ui-toolingPreview = { module = "androidx.compose.ui:ui-tooling-preview" }
@@ -85,6 +88,7 @@ hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", vers
junit = { module = "junit:junit", version = "4.13.2" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
+kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
mikepenz-aboutLibraries = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "mikepenz-aboutLibraries" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" }
@@ -106,5 +110,6 @@ android-application = { id = "com.android.application", version.ref = "android-a
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
mikepenz-aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "mikepenz-aboutLibraries" }