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") + } } }