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
35 changes: 9 additions & 26 deletions apps/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -136,33 +136,16 @@ dependencies {
// Chrome/composer icons (mic, attach, pair, settings, menu, close). The
// full extended set is large, but release builds R8-minify (above) and
// drop every unused icon; debug builds simply carry the dead weight.
implementation("androidx.compose.material:material-icons-extended")
implementation(libs.androidx.material.icons.extended)
implementation(libs.kotlinx.coroutines.android)
// JNA: the FFI bridge uniffi-bindgen's generated Kotlin uses to call into
// crates/client-ffi's cdylib. Version pinned loosely on purpose — this
// is exactly the dependency the generated bindings file itself declares
// as a prerequisite; keep it in step with whatever `uniffi` crate version
// crates/client-ffi/Cargo.toml pins (currently 0.32).
implementation("net.java.dev.jna:jna:5.19.0@aar")

// Tink directly, not its deprecated androidx.security:security-crypto
// wrapper (frozen at 1.1.0-alpha07): platform/SecureIdentityStore.kt
// uses AndroidKeysetManager to keep the persisted bridge identity
// secret encrypted at rest under a Keystore-held master key.
implementation("com.google.crypto.tink:tink-android:1.19.0")

// Pairing-QR camera scan (ui/screens/PairingScanView.kt): CameraX for the
// preview/analysis pipeline, ZXing's core decoder for the QR code — a
// plain Java library with no dependencies of its own. Not ML Kit: it
// brings Play Services, Firebase components and a usage-reporting
// transport (a startup provider, a background upload job) into every
// launch, all to read one QR code. Neither library is covered by the
// Compose BOM, so they are pinned explicitly.
val camerax = "1.6.2"
implementation("androidx.camera:camera-camera2:$camerax")
implementation("androidx.camera:camera-lifecycle:$camerax")
implementation("androidx.camera:camera-view:$camerax")
implementation("com.google.zxing:core:3.5.4")
// The aar: JNA's Android build, with its native dispatch libraries.
// Why each of these is here: gradle/libs.versions.toml.
implementation(variantOf(libs.jna) { artifactType("aar") })
implementation(libs.tink.android)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.zxing.core)

// Compose-native Markdown for assistant/plan transcript rows.
// GFM (tables, task lists, strikethrough, autolinks) is the renderer's
Expand Down
10 changes: 10 additions & 0 deletions apps/android/app/lint.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- The UniFFI bindings are generated (./codedeck gen-android-bindings)
and checked for drift, so a finding there cannot be fixed by hand.
Its one finding, java.lang.ref.Cleaner on API < 33, is guarded at
run time: it falls back to JNA's cleaner when the class is absent. -->
<issue id="all">
<ignore path="src/main/java/uniffi/**" />
</issue>
</lint>
4 changes: 3 additions & 1 deletion apps/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
</intent>
</queries>

<!-- No backup or device transfer: see res/xml/data_extraction_rules.xml. -->
<!-- No backup or device transfer: see res/xml/data_extraction_rules.xml
(Android 12+); fullBackupContent says the same to older versions. -->
<application
android:allowBackup="false"
android:fullBackupContent="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:networkSecurityConfig="@xml/network_security_config"
android:icon="@mipmap/ic_launcher"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.codedeck.plus.core

import com.codedeck.plus.platform.CoreHttpFetch
import android.util.Log
import com.codedeck.plus.platform.CoreHttpFetch
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down
21 changes: 12 additions & 9 deletions apps/android/app/src/main/java/com/codedeck/plus/platform/Login.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.codedeck.plus.platform

import android.content.Context
import androidx.core.content.edit
import uniffi.client_ffi.UniffiIdentitySigner
import uniffi.client_ffi.localIdentitySigner

Expand Down Expand Up @@ -29,19 +30,21 @@ class LoginStore(context: Context) {
fun load(): Login? = decodeLogin(prefs.getString(KIND, null), prefs.getString(PACKAGE, null), prefs.getString(PUBKEY, null))

fun save(login: Login) {
val edit = prefs.edit().clear()
when (login) {
Login.OnDevice -> edit.putString(KIND, "device")
is Login.SignerApp -> edit.putString(KIND, "signer")
.putString(PACKAGE, login.packageName)
.putString(PUBKEY, login.pubkeyHex)
// Committed synchronously: the login must be on disk before the core
// starts, and the service reads it right after.
prefs.edit(commit = true) {
clear()
when (login) {
Login.OnDevice -> putString(KIND, "device")
is Login.SignerApp -> putString(KIND, "signer")
.putString(PACKAGE, login.packageName)
.putString(PUBKEY, login.pubkeyHex)
}
}
// Committed synchronously: the service reads it right after.
edit.commit()
}

fun clear() {
prefs.edit().clear().commit()
prefs.edit(commit = true) { clear() }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.util.Log
import androidx.core.net.toUri
import kotlinx.serialization.json.addJsonObject
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.put
Expand Down Expand Up @@ -39,7 +39,7 @@ data class SignerAppInfo(val packageName: String, val label: String)
/** Every installed NIP-55 signer app, by label. */
fun installedSignerApps(context: Context): List<SignerAppInfo> {
val pm = context.packageManager
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:"))
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:".toUri())
return pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
.map { SignerAppInfo(it.activityInfo.packageName, it.loadLabel(pm).toString()) }
.distinctBy { it.packageName }
Expand All @@ -50,7 +50,7 @@ fun installedSignerApps(context: Context): List<SignerAppInfo> {
* everything the core will need so later requests can be answered without
* the user. */
fun getPublicKeyIntent(packageName: String): Intent =
Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:")).apply {
Intent(Intent.ACTION_VIEW, "nostrsigner:".toUri()).apply {
`package` = packageName
putExtra("type", "get_public_key")
putExtra("permissions", signerPermissionsJson())
Expand Down Expand Up @@ -157,7 +157,7 @@ class Nip55Signer(
}

private fun resolve(method: String, payload: String, peer: String): ProviderAnswer {
val uri = Uri.parse("content://$packageName.$method")
val uri = "content://$packageName.$method".toUri()
val cursor: Cursor = try {
// NIP-55 passes the arguments in the projection slot.
app.contentResolver.query(uri, arrayOf(payload, peer, currentUser), null, null, null)
Expand All @@ -170,7 +170,7 @@ class Nip55Signer(

private fun viaActivity(type: String, payload: String, peer: String?): SignerAnswer {
val id = UUID.randomUUID().toString()
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$payload")).apply {
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$payload".toUri()).apply {
`package` = packageName
putExtra("type", type)
putExtra("id", id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ class Notifier(private val context: Context) : UniffiNotifier {
}

val notification = builder.build()
NotificationManagerCompat.from(context).notify(effectiveTag, idFor(effectiveTag), notification)
try {
NotificationManagerCompat.from(context).notify(effectiveTag, idFor(effectiveTag), notification)
} catch (e: SecurityException) {
// Permission revoked since the check above: drop the post.
}
}

override fun cancel(tag: String) {
Expand Down Expand Up @@ -119,7 +123,11 @@ class Notifier(private val context: Context) : UniffiNotifier {
),
)
.build()
NotificationManagerCompat.from(context).notify(SIGNER_TAG, idFor(SIGNER_TAG), notification)
try {
NotificationManagerCompat.from(context).notify(SIGNER_TAG, idFor(SIGNER_TAG), notification)
} catch (e: SecurityException) {
// Permission revoked since the check above: drop the post.
}
}

fun cancelSignerApproval() = cancel(SIGNER_TAG)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.codedeck.plus.platform

import android.content.Context
import androidx.core.content.edit
import com.google.crypto.tink.Aead
import com.google.crypto.tink.KeyTemplates
import com.google.crypto.tink.RegistryConfiguration
Expand Down Expand Up @@ -143,7 +144,7 @@ class KeyVault(context: Context) {
}
}
app.getSharedPreferences("codedeck_identity_keyset_prefs", Context.MODE_PRIVATE)
.edit().clear().apply()
.edit { clear() }
identityFile.delete()
sessionKeysFile.delete()
buildKeysetHandle()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.util.Log
import android.os.SystemClock
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
Expand Down Expand Up @@ -419,14 +419,14 @@ class StayConnectedService : Service() {
* and its bridges is exactly that type, and unlike `dataSync` it has no
* daily time budget — Android 15 stops a `dataSync` service after 6 h per
* 24 h, which silently ended "stay connected". Older versions only know
* `dataSync` (no budget there).
* `dataSync` (no budget there), and before Android 10 a foreground
* service has no type at all.
*/
private fun foregroundType(): Int =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
} else {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
}
private fun foregroundType(): Int = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
else -> 0
}

/** Re-posting under the foreground notification's own id replaces it in
* place; the platform drops the post silently when notifications are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ fun IconAction(icon: ImageVector, label: String, onClick: () -> Unit, tint: Colo
*/
@Composable
fun Group(
modifier: Modifier = Modifier,
title: String? = null,
footer: String? = null,
modifier: Modifier = Modifier,
rows: @Composable GroupScope.() -> Unit,
) {
Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(Tokens.Space2)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PersistableBundle
import android.provider.Settings
import androidx.activity.compose.BackHandler
Expand All @@ -29,6 +30,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
Expand Down Expand Up @@ -376,7 +378,7 @@ internal fun AppearancePage(view: UniffiSettingsView, dispatch: (UniffiIntent) -
// Local position while dragging; re-keyed on the stored value
// so a change made elsewhere snaps the thumb to it. The intent
// fires on release.
var position by remember(view.uiScale) { mutableStateOf(view.uiScale.toFloat()) }
var position by remember(view.uiScale) { mutableFloatStateOf(view.uiScale.toFloat()) }
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Tokens.Space2)) {
Text("${Math.round(position * 100)}%", color = Tokens.Text, fontSize = Tokens.TextXl, modifier = Modifier.weight(1f))
QuietButton("Reset", enabled = position != UI_SCALE_DEFAULT, onClick = {
Expand Down Expand Up @@ -592,7 +594,11 @@ private fun SecretKeyGroup(revealKey: suspend () -> String?) {
private fun copySensitive(context: Context, text: String) {
val clipboard = context.getSystemService(ClipboardManager::class.java) ?: return
val clip = ClipData.newPlainText("Secret key", text)
clip.description.extras = PersistableBundle().apply { putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) }
// API 33 only: before that there is no clipboard preview to hide the
// secret from.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
clip.description.extras = PersistableBundle().apply { putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) }
}
clipboard.setPrimaryClip(clip)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.net.Uri
import android.provider.OpenableColumns
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.graphics.scale
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
Expand Down Expand Up @@ -163,7 +164,7 @@ private fun downscaledIfHuge(resolver: ContentResolver, picked: PickedFile, file
BitmapFactory.decodeStream(it, null, BitmapFactory.Options().apply { inSampleSize = sampleSize })
} ?: return null
val scaled = if (decoded.width != targetWidth || decoded.height != targetHeight) {
Bitmap.createScaledBitmap(decoded, targetWidth, targetHeight, true)
decoded.scale(targetWidth, targetHeight, true)
} else {
decoded
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ internal fun Composer(
.size(44.dp)
.clip(CircleShape)
.background(if (canSend || stop) Tokens.Accent else Tokens.SurfaceHover)
.clickable(enabled = canSend || stop, onClick = if (stop) onStop!! else onSend)
.clickable(enabled = canSend || stop, onClick = if (stop) onStop else onSend)
.semantics { contentDescription = if (stop) "Stop" else "Send" },
contentAlignment = Alignment.Center,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.SystemClock
import android.util.Log
import android.speech.RecognizerIntent
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
Expand Down Expand Up @@ -182,8 +182,8 @@ fun SessionScreen(
core: CoreHost,
machine: String,
sessionId: String,
onBack: (() -> Unit)? = null,
modifier: Modifier = Modifier,
onBack: (() -> Unit)? = null,
) {
val machinesView by core.machines.collectAsState()
val uiView by core.ui.collectAsState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ fun markdownBlocks(text: String, target: Int = BLOCK_TARGET): List<String> {
}
append(line)
} else {
val open = fence!!
val open = fence
if (trimmed.startsWith(open) && trimmed.trimEnd().all { it == open[0] }) {
fence = null
append(line)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight
import androidx.compose.material.icons.outlined.Build
import androidx.compose.material.icons.outlined.ChatBubbleOutline
import androidx.compose.material.icons.outlined.Checklist
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Description
import androidx.compose.material.icons.outlined.DriveFileMove
import androidx.compose.material.icons.outlined.Language
import androidx.compose.material.icons.outlined.Psychology
import androidx.compose.material.icons.outlined.Search
Expand Down Expand Up @@ -49,7 +49,7 @@ internal fun toolKindIcon(kind: String): ImageVector = when (kind) {
"read" -> Icons.Outlined.Visibility
"edit" -> Icons.Outlined.Description
"delete" -> Icons.Outlined.Delete
"move" -> Icons.Outlined.DriveFileMove
"move" -> Icons.AutoMirrored.Outlined.DriveFileMove
"search" -> Icons.Outlined.Search
"execute" -> Icons.Outlined.Terminal
"think" -> Icons.Outlined.Checklist
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class MarkdownParityTest {
)

@Composable
private fun dark(content: @Composable () -> Unit) {
private fun Dark(content: @Composable () -> Unit) {
CodeDeckTheme {
Surface(color = Tokens.Bg, contentColor = Tokens.Text) {
Column(Modifier.background(Tokens.Bg).fillMaxWidth().padding(16.dp)) { content() }
Expand All @@ -42,6 +42,6 @@ class MarkdownParityTest {

@Test
fun assistant_markdown_gfm_and_code() {
paparazzi.snapshot { dark { TranscriptMarkdown(MarkdownCorpus.ASSISTANT_MARKDOWN) } }
paparazzi.snapshot { Dark { TranscriptMarkdown(MarkdownCorpus.ASSISTANT_MARKDOWN) } }
}
}
Loading
Loading