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 @@ -6,13 +6,10 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension

class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.pluginManager.apply("com.android.application")
target.pluginManager.apply("org.jetbrains.kotlin.android")

val extension = target.extensions.getByType<ApplicationExtension>()
extension.apply {
Expand All @@ -37,8 +34,5 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
}
}

target.extensions.configure<KotlinAndroidProjectExtension> {
compilerOptions.jvmTarget.set(JvmTarget.JVM_11)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@ import org.gradle.api.publish.maven.MavenPublication
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension

class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.pluginManager.apply("com.android.library")
target.pluginManager.apply("org.jetbrains.kotlin.android")
target.pluginManager.apply("maven-publish")

val extension = target.extensions.getByType<LibraryExtension>()
Expand Down Expand Up @@ -44,10 +41,6 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
}
}

target.extensions.configure<KotlinAndroidProjectExtension> {
compilerOptions.jvmTarget.set(JvmTarget.JVM_11)
}

target.afterEvaluate {
val releaseComponent = components.findByName("release") ?: return@afterEvaluate

Expand Down
6 changes: 5 additions & 1 deletion snapo-link-android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ kotlin.code.style=official
android.nonTransitiveRClass=true

GROUP=com.openai.snapo
VERSION_FILE=../VERSION
VERSION_FILE=../VERSION
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false
android.r8.strictFullModeForKeepRules=false
6 changes: 3 additions & 3 deletions snapo-link-android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
[versions]
agp = "8.13.2"
agp = "9.0.0"
detektGradlePlugin = "1.23.8"
gradle = "8.13.2"
gradle = "9.0.0"
kotlin = "2.3.0"
coroutines = "1.10.2"
kotlinGradlePlugin = "2.2.20"
serialization = "1.9.0"
coreKtx = "1.17.0"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.2"
composeBom = "2025.12.01"
composeBom = "2026.01.00"
okhttpBom = "5.3.2"
ktor = "3.3.3"
detekt = "1.23.8"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Wed Sep 24 11:05:57 EDT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ class SnapOInitProvider : ContentProvider() {
val allowRelease = meta?.getBoolean("snapo.allow_release", false) ?: false

val linkConfig = SnapOLinkConfig(
singleClientOnly = true,
modeLabel = modeLabel,
allowRelease = allowRelease,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.openai.snapo.link.core

data class SnapOLinkConfig(
/** Single-client policy keeps ordering simple. */
val singleClientOnly: Boolean = true,

/** For the Hello record; reflect your current redaction mode/prefs. */
val modeLabel: String = "safe", // or "unredacted"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.openai.snapo.link.core

import android.app.ActivityManager
import android.app.Application
import android.os.Process
import android.os.SystemClock

internal class SnapOLinkContext(
private val app: Application,
private val config: SnapOLinkConfig,
private val appIconProvider: AppIconProvider = AppIconProvider(app),
featureSinkProvider: (String) -> LinkEventSink,
private val serverStartWallMs: Long = System.currentTimeMillis(),
private val serverStartMonoNs: Long = SystemClock.elapsedRealtimeNanos(),
) {
@Volatile
private var latestAppIcon: AppIcon? = null

init {
SnapOLinkRegistry.bindSinkProvider(featureSinkProvider)
}

fun buildHello(): Hello =
Hello(
packageName = app.packageName,
processName = appProcessName(),
pid = Process.myPid(),
serverStartWallMs = serverStartWallMs,
serverStartMonoNs = serverStartMonoNs,
mode = config.modeLabel,
features = SnapOLinkRegistry.snapshot().map { LinkFeatureInfo(it.featureId) },
)

fun latestAppIcon(): AppIcon? = latestAppIcon

fun snapshotFeatures(): List<SnapOLinkFeature> = SnapOLinkRegistry.snapshot()

fun loadAppIconIfAvailable(): AppIcon? {
val iconEvent = appIconProvider.loadAppIcon() ?: return null
latestAppIcon = iconEvent
return iconEvent
}

private fun appProcessName(): String {
return try {
val am = app.getSystemService(Application.ACTIVITY_SERVICE) as ActivityManager
val pid = Process.myPid()
am.runningAppProcesses?.firstOrNull { it.pid == pid }?.processName ?: app.packageName
} catch (_: Throwable) {
app.packageName
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,39 @@ interface SnapOLinkFeature {
/** Stable identifier used for feature envelopes. */
val featureId: String

suspend fun onClientConnected(sink: LinkEventSink)
suspend fun onFeatureOpened()
fun onClientDisconnected()
/** Called once when the link server is available so features can broadcast or target clients. */
fun onLinkAvailable(sink: LinkEventSink) {}

/** Invoked once per client session when a client opens this feature. */
suspend fun onFeatureOpened(clientId: Long)

/** Invoked when a client disconnects. */
fun onClientDisconnected(clientId: Long) {}
}

interface LinkEventSink {
suspend fun <T> sendHighPriority(payload: T, serializer: SerializationStrategy<T>)
suspend fun <T> sendLowPriority(payload: T, serializer: SerializationStrategy<T>)
sealed interface ClientId {
data object All : ClientId
data class Specific(val value: Long) : ClientId
}

suspend inline fun <reified T> LinkEventSink.sendHighPriority(payload: T) {
sendHighPriority(payload, serializer())
enum class EventPriority {
High,
Low,
}

interface LinkEventSink {
fun <T> send(
payload: T,
serializer: SerializationStrategy<T>,
clientId: ClientId = ClientId.All,
priority: EventPriority = EventPriority.High,
)
}

suspend inline fun <reified T> LinkEventSink.sendLowPriority(payload: T) {
sendLowPriority(payload, serializer())
inline fun <reified T> LinkEventSink.send(
payload: T,
clientId: ClientId = ClientId.All,
priority: EventPriority = EventPriority.High,
) {
send(payload, serializer(), clientId, priority)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,42 @@ package com.openai.snapo.link.core

object SnapOLinkRegistry {
private val lock = Any()
private val features = LinkedHashSet<SnapOLinkFeature>()
private val features = LinkedHashMap<String, SnapOLinkFeature>()
private val linkedFeatureIds = HashSet<String>()
private var sinkProvider: ((String) -> LinkEventSink)? = null

fun register(feature: SnapOLinkFeature) {
synchronized(lock) { features.add(feature) }
val (storedFeature, provider) = synchronized(lock) {
val existing = features.putIfAbsent(feature.featureId, feature)
Pair(existing ?: feature, sinkProvider)
}
if (provider != null) {
linkFeatureIfNeeded(storedFeature, provider)
}
}

fun snapshot(): List<SnapOLinkFeature> = synchronized(lock) { features.toList() }
internal fun bindSinkProvider(provider: (String) -> LinkEventSink) {
val snapshot = synchronized(lock) {
sinkProvider = provider
features.values.toList()
}
snapshot.forEach { linkFeatureIfNeeded(it, provider) }
}

fun snapshot(): List<SnapOLinkFeature> =
synchronized(lock) { features.values.toList() }

private fun linkFeatureIfNeeded(
feature: SnapOLinkFeature,
provider: (String) -> LinkEventSink,
) {
val shouldLink = synchronized(lock) {
if (linkedFeatureIds.contains(feature.featureId)) return@synchronized false
linkedFeatureIds.add(feature.featureId)
true
}
if (shouldLink) {
feature.onLinkAvailable(provider(feature.featureId))
}
}
}
Loading