diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts
index 31bb73004..3b0fe53c0 100644
--- a/androidApp/build.gradle.kts
+++ b/androidApp/build.gradle.kts
@@ -11,6 +11,8 @@ plugins {
id("io.github.takahirom.roborazzi")
alias(libs.plugins.compose.compiler)
alias(libs.plugins.screenshot)
+ id("com.google.devtools.ksp")
+ id("org.jetbrains.kotlin.kapt")
}
configureCompilerOptions()
@@ -53,6 +55,13 @@ android {
versionName = versionName()
resourceConfigurations += listOf("en", "fr")
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ ksp {
+ arg("appfunctions:aggregateAppFunctions", "true")
+ arg("appfunctions:generateMetadataFromSchema", "true")
+ }
}
signingConfigs {
@@ -198,6 +207,15 @@ dependencies {
implementation(libs.googleid)
implementation(libs.androidx.credentials.play.services.auth)
+ implementation(libs.androidx.appfunctions)
+ implementation(libs.androidx.appfunctions.service)
+ ksp(libs.androidx.appfunctions.compiler)
+ implementation(libs.androidx.appsearch)
+ implementation(libs.androidx.appsearch.ktx)
+ implementation(libs.androidx.appsearch.platform.storage)
+ implementation(libs.kotlinx.coroutines.guava)
+ kapt(libs.androidx.appsearch.compiler)
+
testImplementation(libs.junit)
testImplementation(libs.robolectric)
testImplementation(libs.compose.ui.test.junit4)
@@ -209,4 +227,10 @@ dependencies {
debugImplementation(libs.compose.ui.manifest)
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
+
+ androidTestImplementation("androidx.test:core-ktx:1.6.1")
+ androidTestImplementation("androidx.test:rules:1.6.1")
+ androidTestImplementation("androidx.test.ext:junit:1.2.1")
+ androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
+ androidTestImplementation("androidx.test.ext:truth:1.6.0")
}
diff --git a/androidApp/src/androidTest/kotlin/dev/johnoreilly/confetti/appfunctions/IntegrationTest.kt b/androidApp/src/androidTest/kotlin/dev/johnoreilly/confetti/appfunctions/IntegrationTest.kt
new file mode 100644
index 000000000..cd8990ae1
--- /dev/null
+++ b/androidApp/src/androidTest/kotlin/dev/johnoreilly/confetti/appfunctions/IntegrationTest.kt
@@ -0,0 +1,110 @@
+package dev.johnoreilly.confetti.appfunctions;
+
+import android.content.Context
+import androidx.appfunctions.AppFunctionData
+import androidx.appfunctions.AppFunctionManagerCompat
+import androidx.appfunctions.AppFunctionSearchSpec
+import androidx.appfunctions.ExecuteAppFunctionRequest
+import androidx.appfunctions.ExecuteAppFunctionResponse
+import androidx.appsearch.app.GlobalSearchSession
+import androidx.appsearch.app.SearchSpec
+import androidx.appsearch.platformstorage.PlatformStorage
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.guava.await
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assume.assumeNotNull
+import org.junit.Before
+import org.junit.Test
+import kotlin.time.Duration.Companion.seconds
+
+class IntegrationTest {
+ private val context = InstrumentationRegistry.getInstrumentation().context
+ private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
+ private lateinit var appFunctionManager: AppFunctionManagerCompat
+ private val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
+
+ @Before
+ fun setup(): Unit = runBlocking {
+ val appFunctionManagerCompatOrNull = AppFunctionManagerCompat.getInstance(targetContext)
+ assumeNotNull(appFunctionManagerCompatOrNull)
+ appFunctionManager = checkNotNull(appFunctionManagerCompatOrNull)
+
+ uiAutomation.apply {
+ adoptShellPermissionIdentity("android.permission.EXECUTE_APP_FUNCTIONS")
+ }
+ }
+
+ @Test
+ fun listAppSearchDocuments(): Unit = runBlocking {
+ val searchSession = createSearchSession(context)
+
+ repeat(10) {
+ val searchResults = searchSession.search(
+ "",
+ SearchSpec.Builder()
+ .addFilterPackageNames("dev.johnoreilly.confetti")
+ .build(),
+ )
+ var nextPage = searchResults.nextPageAsync.await()
+ if (nextPage.isNotEmpty()) {
+ while (nextPage.isNotEmpty()) {
+ for (result in nextPage) {
+ println(result.genericDocument)
+ }
+
+ nextPage = searchResults.nextPageAsync.await()
+ }
+ return@repeat
+ } else {
+ delay(1.seconds)
+ }
+ }
+
+ searchSession.close()
+ }
+
+ private suspend fun createSearchSession(context: Context): GlobalSearchSession {
+ return PlatformStorage.createGlobalSearchSessionAsync(
+ PlatformStorage.GlobalSearchContext.Builder(context).build()
+ )
+ .await()
+ }
+
+ @After
+ fun tearDown() {
+ uiAutomation.dropShellPermissionIdentity()
+ }
+
+ @Test
+ fun executeAppFunction_success(): Unit = runBlocking {
+ val result = appFunctionManager.observeAppFunctions(AppFunctionSearchSpec()).first()
+
+ result.forEach {
+ println(it)
+ }
+
+ val response =
+ appFunctionManager.executeAppFunction(
+ request =
+ ExecuteAppFunctionRequest(
+ targetContext.packageName,
+ "dev.johnoreilly.confetti.appfunctions.ConferenceAppFunctions#conferenceInfo",
+ AppFunctionData.Builder("").build()
+ )
+ )
+
+ when (response) {
+ is ExecuteAppFunctionResponse.Success -> {
+ val returnValue = response.returnValue
+ val genericDocument = returnValue.genericDocument.getPropertyDocument("androidAppfunctionsReturnValue")!!
+ println(genericDocument.getPropertyStringArray("title")?.toList())
+ println(genericDocument.getProperty("dates"))
+ println(genericDocument.propertyNames)
+ }
+ is ExecuteAppFunctionResponse.Error -> throw response.error
+ }
+ }
+}
\ No newline at end of file
diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
index 384d3e122..add4f6469 100644
--- a/androidApp/src/main/AndroidManifest.xml
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -58,6 +58,5 @@
-
diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/ConfettiApplication.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/ConfettiApplication.kt
index 8b459dfc7..95b69083b 100644
--- a/androidApp/src/main/java/dev/johnoreilly/confetti/ConfettiApplication.kt
+++ b/androidApp/src/main/java/dev/johnoreilly/confetti/ConfettiApplication.kt
@@ -1,6 +1,9 @@
package dev.johnoreilly.confetti
import android.app.Application
+import android.os.Build
+import androidx.appfunctions.service.AppFunctionConfiguration
+import androidx.appsearch.platformstorage.PlatformStorage
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.work.WorkManager
@@ -8,18 +11,20 @@ import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.ktx.Firebase
+import dev.johnoreilly.confetti.appsearch.AppSearchManager
import dev.johnoreilly.confetti.di.appModule
import dev.johnoreilly.confetti.di.initKoin
import dev.johnoreilly.confetti.work.SessionNotificationSender
import dev.johnoreilly.confetti.work.SessionNotificationWorker
import dev.johnoreilly.confetti.work.setupDailyRefresh
+import kotlinx.coroutines.guava.asDeferred
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.workmanager.koin.workManagerFactory
-class ConfettiApplication : Application() {
+class ConfettiApplication : Application(), AppFunctionConfiguration.Provider {
private val isFirebaseInstalled
get() = try {
@@ -57,5 +62,12 @@ class ConfettiApplication : Application() {
ProcessLifecycleOwner.get().lifecycleScope.launch {
get().updateSchedule()
}
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ get().scheduleImmediate()
+ }
}
+
+ override val appFunctionConfiguration: AppFunctionConfiguration
+ get() = get()
}
diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/appfunctions/ConferenceAppFunctions.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/appfunctions/ConferenceAppFunctions.kt
new file mode 100644
index 000000000..3f1000f24
--- /dev/null
+++ b/androidApp/src/main/java/dev/johnoreilly/confetti/appfunctions/ConferenceAppFunctions.kt
@@ -0,0 +1,129 @@
+package dev.johnoreilly.confetti.appfunctions
+
+import android.app.PendingIntent
+import androidx.appfunctions.AppFunctionContext
+import androidx.appfunctions.AppFunctionSchemaCapability
+import androidx.appfunctions.AppFunctionSchemaDefinition
+import androidx.appfunctions.AppFunctionSerializable
+import androidx.appfunctions.service.AppFunction
+import com.apollographql.cache.normalized.FetchPolicy
+import dev.johnoreilly.confetti.ConfettiRepository
+import kotlinx.datetime.toJavaLocalDate
+import kotlinx.datetime.toJavaLocalDateTime
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import java.time.LocalDateTime
+
+
+@AppFunctionSchemaCapability
+interface OpenIntent {
+ val intentToOpen: PendingIntent?
+}
+
+@AppFunctionSchemaDefinition(name = "conferenceInfo", version = 1, category = "Conference")
+interface ConferenceInfoSchemaDefinition {
+ suspend fun conferenceInfo(
+ appFunctionContext: AppFunctionContext,
+ ): AppFunctionConference?
+}
+
+@AppFunctionSchemaDefinition(name = "findSessions", version = 1, category = "Sessions")
+interface ConferenceSessionsSchemaDefinition {
+ suspend fun findSessions(
+ appFunctionContext: AppFunctionContext,
+ findSessionsParams: FindSessionsParams,
+ ): List?
+}
+
+@AppFunctionSchemaDefinition(name = "listSpeakers", version = 1, category = "Speakers")
+interface ConferenceSpeakersSchemaDefinition {
+ suspend fun listSpeakers(
+ appFunctionContext: AppFunctionContext,
+ ): List?
+}
+
+@AppFunctionSerializable
+data class FindSessionsParams(
+ val speaker: String? = null,
+)
+
+@AppFunctionSerializable
+data class AppFunctionSession(
+ val id: String,
+ val title: String,
+ val room: String,
+ val speakers: List,
+ val time: LocalDateTime,
+ override val intentToOpen: PendingIntent? = null
+): OpenIntent
+
+@AppFunctionSerializable
+data class AppFunctionSpeaker(
+ val id: String,
+ val name: String,
+ override val intentToOpen: PendingIntent? = null
+): OpenIntent
+
+@AppFunctionSerializable
+data class AppFunctionConference(
+ val id: String,
+ val title: String,
+ val dates: List,
+ override val intentToOpen: PendingIntent? = null
+): OpenIntent
+
+class ConferenceAppFunctions : KoinComponent, ConferenceInfoSchemaDefinition, ConferenceSessionsSchemaDefinition,
+ ConferenceSpeakersSchemaDefinition {
+ private val confettiRepository: ConfettiRepository by inject()
+
+ @AppFunction
+ override suspend fun conferenceInfo(appFunctionContext: AppFunctionContext): AppFunctionConference {
+ try {
+ val conference = "kotlinconf2025"//confettiRepository.getConference()
+ println("conferenceInfo, conference = $conference")
+ val details =
+ confettiRepository.conferenceData(conference, fetchPolicy = FetchPolicy.CacheFirst).data!!
+
+ return AppFunctionConference(
+ details.config.id,
+ details.config.name,
+ details.config.days.map { it.toJavaLocalDate().atStartOfDay() })
+ } catch (e: Exception) {
+ e.printStackTrace()
+ throw e
+ }
+ }
+
+ @AppFunction
+ override suspend fun findSessions(
+ appFunctionContext: AppFunctionContext,
+ findSessionsParams: FindSessionsParams
+ ): List {
+ val conference = confettiRepository.getConference()
+ val sessions = confettiRepository.sessions(conference, null, null, FetchPolicy.CacheOnly).data?.sessions?.nodes?.map { it.sessionDetails }.orEmpty()
+
+ return sessions.map { session ->
+ AppFunctionSession(
+ session.id,
+ session.title,
+ session.room?.name ?: "",
+ session.speakers.map { it.speakerDetails.name },
+ session.startsAt.toJavaLocalDateTime(),
+ )
+ }
+ }
+
+ @AppFunction
+ override suspend fun listSpeakers(appFunctionContext: AppFunctionContext): List {
+ val conference = confettiRepository.getConference()
+ val results =
+ confettiRepository.conferenceData(conference, fetchPolicy = FetchPolicy.CacheOnly).data?.speakers?.nodes?.map { it.speakerDetails }.orEmpty()
+
+ return results.map { speaker ->
+ AppFunctionSpeaker(
+ speaker.id,
+ speaker.name
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/appsearch/AppSearchManager.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/appsearch/AppSearchManager.kt
new file mode 100644
index 000000000..981898ab9
--- /dev/null
+++ b/androidApp/src/main/java/dev/johnoreilly/confetti/appsearch/AppSearchManager.kt
@@ -0,0 +1,81 @@
+package dev.johnoreilly.confetti.appsearch
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.appsearch.app.AppSearchSession
+import androidx.appsearch.app.GlobalSearchSession
+import androidx.appsearch.app.PutDocumentsRequest
+import androidx.appsearch.app.SetSchemaRequest
+import androidx.appsearch.platformstorage.PlatformStorage
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import dev.johnoreilly.confetti.fragment.SessionDetails
+import dev.johnoreilly.confetti.work.RefreshWorker
+import kotlinx.coroutines.guava.await
+
+@RequiresApi(Build.VERSION_CODES.S)
+class AppSearchManager(
+ private val context: Context,
+ private val workManager: WorkManager
+) {
+ private lateinit var globalSession: GlobalSearchSession
+ lateinit var session: AppSearchSession
+
+ suspend fun init() {
+ globalSession = PlatformStorage.createGlobalSearchSessionAsync(
+ PlatformStorage.GlobalSearchContext.Builder(context).build()
+ ).await()
+ session = PlatformStorage.createSearchSessionAsync(
+ PlatformStorage.SearchContext.Builder(context, "confetti")
+ .build()
+ ).await()
+ }
+
+ suspend fun defineSchema() {
+ session.setSchemaAsync(
+ SetSchemaRequest.Builder()
+ .addDocumentClasses(SearchSessionModel::class.java)
+ .setDocumentClassDisplayedBySystem(SearchSessionModel::class.java, true)
+ .build()
+ ).await()
+ }
+
+ fun scheduleDailyUpdate() {
+ workManager.enqueueUniquePeriodicWork(
+ RefreshWorker.WorkDaily,
+ ExistingPeriodicWorkPolicy.UPDATE,
+ AppSearchWorker.dailyRefresh()
+ )
+ }
+
+ suspend fun flushAndClose() {
+ session.requestFlushAsync().await()
+ session.close()
+ }
+
+ fun scheduleImmediate() {
+ workManager.enqueueUniqueWork(
+ "AppSearch",
+ ExistingWorkPolicy.KEEP,
+ OneTimeWorkRequest.from(AppSearchWorker::class.java)
+ )
+ }
+
+ suspend fun writeSessions(conference: String, nodes: List) {
+ val putRequest = PutDocumentsRequest.Builder().apply {
+ nodes.forEach { details ->
+ val sessionDocument = SearchSessionModel(
+ conference,
+ details.id,
+ details.title,
+ details.room?.name ?: "",
+ details.speakers.map { it.speakerDetails.name })
+ addDocuments(sessionDocument)
+ }
+ }.build()
+ session.putAsync(putRequest).await()
+ }
+}
\ No newline at end of file
diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/appsearch/AppSearchWorker.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/appsearch/AppSearchWorker.kt
new file mode 100644
index 000000000..0fdb4c26d
--- /dev/null
+++ b/androidApp/src/main/java/dev/johnoreilly/confetti/appsearch/AppSearchWorker.kt
@@ -0,0 +1,91 @@
+package dev.johnoreilly.confetti.appsearch
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkerParameters
+import com.apollographql.cache.normalized.FetchPolicy
+import dev.johnoreilly.confetti.ApolloClientCache
+import dev.johnoreilly.confetti.ConfettiRepository
+import dev.johnoreilly.confetti.GetConferenceDataQuery
+import dev.johnoreilly.confetti.fetchPolicy
+import dev.johnoreilly.confetti.work.ConferenceSetting
+import dev.johnoreilly.confetti.work.RefreshWorker
+import dev.johnoreilly.confetti.work.RefreshWorker.Companion.ConferenceKey
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.supervisorScope
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import java.time.Duration
+
+@RequiresApi(Build.VERSION_CODES.S)
+class AppSearchWorker(
+ private val appContext: Context,
+ private val workerParams: WorkerParameters
+): CoroutineWorker(appContext, workerParams), KoinComponent {
+ private val apolloClientCache: ApolloClientCache by inject()
+ private val confettiRepository: ConfettiRepository by inject()
+ private val appSearchManager: AppSearchManager by inject()
+
+ override suspend fun doWork(): Result {
+ val conference = confettiRepository.getConference()
+
+ return if (conference.isBlank()) {
+ Result.success()
+ } else {
+ updateAppSearch(appSearchManager, conference, apolloClientCache)
+ Result.success()
+ }
+ }
+
+ companion object {
+ fun dailyRefresh(): PeriodicWorkRequest =
+ PeriodicWorkRequestBuilder(
+ Duration.ofDays(1)
+ )
+ .setConstraints(
+ Constraints.Builder()
+ .setRequiresCharging(true)
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .setRequiresDeviceIdle(true)
+ .build()
+ )
+ .build()
+ }
+}
+
+
+@RequiresApi(Build.VERSION_CODES.S)
+suspend fun updateAppSearch(
+ appSearchManager: AppSearchManager,
+ conference: String,
+ apolloClientCache: ApolloClientCache,
+) {
+ println("updateAppSearch")
+ val client = apolloClientCache.getClient(conference)
+
+ val result = client.query(GetConferenceDataQuery())
+ .fetchPolicy(FetchPolicy.CacheOnly)
+ .execute()
+
+ println("init")
+ appSearchManager.init()
+ println("defineSchema")
+ appSearchManager.defineSchema()
+
+ val nodes = result.data?.sessions?.nodes?.map { it.sessionDetails } ?: return
+
+ supervisorScope {
+ println("writing Session")
+ appSearchManager.writeSessions(conference, nodes)
+ }
+
+ println("flushAndClose")
+ appSearchManager.flushAndClose()
+}
\ No newline at end of file
diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/appsearch/SearchSessionModel.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/appsearch/SearchSessionModel.kt
new file mode 100644
index 000000000..9529c3c69
--- /dev/null
+++ b/androidApp/src/main/java/dev/johnoreilly/confetti/appsearch/SearchSessionModel.kt
@@ -0,0 +1,22 @@
+package dev.johnoreilly.confetti.appsearch
+
+import androidx.appsearch.annotation.Document
+import androidx.appsearch.app.AppSearchSchema
+
+@Document(name = "Session")
+data class SearchSessionModel(
+ @Document.Namespace
+ val conference: String,
+
+ @Document.Id
+ val id: String,
+
+ @Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ val title: String,
+
+ @Document.StringProperty
+ val room: String,
+
+ @Document.StringProperty
+ val speakers: List
+)
\ No newline at end of file
diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/di/AppModule.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/di/AppModule.kt
index efc6e008e..618fb4982 100644
--- a/androidApp/src/main/java/dev/johnoreilly/confetti/di/AppModule.kt
+++ b/androidApp/src/main/java/dev/johnoreilly/confetti/di/AppModule.kt
@@ -2,12 +2,14 @@
package dev.johnoreilly.confetti.di
+import androidx.appfunctions.service.AppFunctionConfiguration
import androidx.credentials.CredentialManager
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper
import dev.johnoreilly.confetti.ConfettiRepository
import dev.johnoreilly.confetti.R
import dev.johnoreilly.confetti.account.SignInProcess
+import dev.johnoreilly.confetti.appsearch.AppSearchManager
import dev.johnoreilly.confetti.auth.Authentication
import dev.johnoreilly.confetti.auth.DefaultAuthentication
import dev.johnoreilly.confetti.decompose.ConferenceRefresh
@@ -56,4 +58,13 @@ val appModule = module {
webClientId = androidContext().getString(R.string.default_web_client_id)
)
}
+
+ factory {
+ AppSearchManager(get(), get())
+ }
+
+ factory {
+ AppFunctionConfiguration.Builder()
+ .build()
+ }
}
diff --git a/build-logic/src/main/kotlin/Dependencies.kt b/build-logic/src/main/kotlin/Dependencies.kt
index 757bb1439..7434338f7 100644
--- a/build-logic/src/main/kotlin/Dependencies.kt
+++ b/build-logic/src/main/kotlin/Dependencies.kt
@@ -2,7 +2,7 @@
object AndroidSdk {
const val min = 24
- const val compile = 35
+ const val compile = 36
const val target = compile
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index fdb4d8c93..921560294 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,12 +1,16 @@
[versions]
+appfunctionsService = "1.0.0-SNAPSHOT"
+appsearch = "1.1.0-beta01"
+appsearchPlatformStorage = "1.1.0-beta01"
kotlin = "2.1.20"
+kotlinxCoroutinesGuava = "1.10.2"
ksp = "2.1.20-2.0.0"
-kotlinx-coroutines = "1.10.1"
+kotlinx-coroutines = "1.10.2"
kotlinx-datetime = "0.6.2"
apollo-kotlin-execution = "0.1.1-SNAPSHOT-fd7fa806b95c5b9046494989a7dd478c47237e12"
compatPatrouille = "0.0.0"
-agp = "8.8.2"
+agp = "8.9.2"
activity-compose = "1.10.1"
androidx-lifecycle = "2.8.7"
androidx-datastore = "1.1.5"
@@ -32,13 +36,13 @@ koin-android = "4.0.4"
koin-android-compose = "4.0.4"
koin-compose-multiplatform = "4.0.4"
koin-core = "4.0.4"
-kotlinx-coroutines-play-services = "1.10.1"
+kotlinx-coroutines-play-services = "1.10.2"
lifecycle = "2.8.7"
lifecycle-livedata-ktx = "2.8.7"
materialkolor = "2.0.0"
multiplatform-settings = "1.3.0"
nav-compose = "2.9.0-rc01"
-okio = "3.10.2"
+okio = "3.11.0"
permissions = "0.19.1"
permissionsCompose = "0.19.1"
permissionsNotifications = "0.19.1"
@@ -60,6 +64,13 @@ screenshot = "0.0.1-alpha09"
[libraries]
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
+androidx-appfunctions = { module = "androidx.appfunctions:appfunctions", version.ref = "appfunctionsService" }
+androidx-appfunctions-service = { module = "androidx.appfunctions:appfunctions-service", version.ref = "appfunctionsService" }
+androidx-appfunctions-compiler = { module = "androidx.appfunctions:appfunctions-compiler", version.ref = "appfunctionsService" }
+androidx-appsearch-platform-storage = { module = "androidx.appsearch:appsearch-platform-storage", version.ref = "appsearchPlatformStorage" }
+androidx-appsearch = { module = "androidx.appsearch:appsearch", version.ref = "appsearch" }
+androidx-appsearch-ktx = { module = "androidx.appsearch:appsearch-ktx", version.ref = "appsearch" }
+androidx-appsearch-compiler = { module = "androidx.appsearch:appsearch-compiler", version.ref = "appsearch" }
androidx-benchmarkmacro = "androidx.benchmark:benchmark-macro-junit4:1.3.4"
androidx-complications-rendering = { module = "androidx.wear.watchface:watchface-complications-rendering", version.ref = "wear-watchface"}
androidx-compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "wear-compose" }
@@ -81,6 +92,7 @@ androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" }
androidx-wear-phone-interactions = { module = "androidx.wear:wear-phone-interactions", version.ref = "wearPhoneInteractions" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work-runtime-ktx" }
androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "work-runtime-ktx" }
+kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" }
lifecyle-runtime = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "composeLifecyleRuntime" }
apollo-adapters = { module = "com.apollographql.apollo:apollo-adapters" }
apollo-normalized-cache-in-memory = { module = "com.apollographql.cache:normalized-cache-incubating", version.ref = "apollo-cache" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 1fa61026a..784221e87 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,6 +19,9 @@ pluginManagement {
includeVersionByRegex("com.apollographql.execution", ".*", ".*SNAPSHOT.*")
}
}
+ maven {
+ url = uri("https://androidx.dev/snapshots/builds/13439521/artifacts/repository")
+ }
}
}