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
4 changes: 2 additions & 2 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import java.util.Properties
import java.net.URI
import java.security.MessageDigest

val appVersion = "0.3.3"
val appVersionCode = 27
val appVersion = "0.3.4"
val appVersionCode = 28

val appVersionSource = file("src/commonMain/kotlin/dev/stade/AppVersion.kt")
val declaredAppVersion = Regex("""APP_VERSION\s*=\s*"([^"]+)"""")
Expand Down
5 changes: 5 additions & 0 deletions composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>

<service
Expand Down
43 changes: 43 additions & 0 deletions composeApp/src/androidMain/kotlin/dev/stade/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.OpenableColumns
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
Expand Down Expand Up @@ -68,6 +69,10 @@ class MainActivity : FragmentActivity() {

private fun handleInviteFileIntent(intent: Intent?) {
intent ?: return
if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
handleSharedStickers(extractStreamUris(intent))
return
}
val uri: Uri? = when (intent.action) {
Intent.ACTION_VIEW -> intent.data
Intent.ACTION_SEND -> extractStreamUri(intent)
Expand Down Expand Up @@ -95,10 +100,48 @@ class MainActivity : FragmentActivity() {
}
if (!text.isNullOrBlank() && text.startsWith("STADE2-")) {
app.handleOpenInviteIntent(text)
} else {
handleSharedStickers(listOf(uri))
}
}
}

private fun handleSharedStickers(uris: List<Uri>) {
if (uris.isEmpty()) return
val app = application as StadeApplication
lifecycleScope.launch(Dispatchers.IO) {
val files = uris.mapNotNull { readSharedSticker(it) }
if (files.isNotEmpty()) app.handleStickerFilesIntent(files)
}
}

private fun readSharedSticker(uri: Uri): dev.stade.ui.PickedStickerFile? = runCatching {
val name = contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
?.use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null }
?: uri.lastPathSegment ?: "sticker"
val bytes = contentResolver.openInputStream(uri)?.use { input ->
val out = ByteArrayOutputStream()
val buffer = ByteArray(64 * 1024)
while (true) {
val read = input.read(buffer)
if (read < 0) break
if (out.size() + read > dev.stade.sticker.MAX_IMPORT_ARCHIVE_BYTES) return@use null
out.write(buffer, 0, read)
}
out.toByteArray()
} ?: return null
if (!dev.stade.sticker.looksLikeSticker(bytes) && !dev.stade.sticker.isZipArchive(bytes)) return null
dev.stade.ui.PickedStickerFile(name, bytes)
}.getOrNull()

@Suppress("DEPRECATION")
private fun extractStreamUris(intent: Intent): List<Uri> =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
}.orEmpty()

@Suppress("DEPRECATION")
private fun extractStreamUri(intent: Intent): Uri? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Expand Down
11 changes: 11 additions & 0 deletions composeApp/src/androidMain/kotlin/dev/stade/StadeApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class StadeApplication : Application() {
private var pendingGroupAtBoot: String? = null
private var pendingGoHomeAtBoot: Boolean = false
private var pendingInviteAtBoot: String? = null
private var pendingStickersAtBoot: List<dev.stade.ui.PickedStickerFile> = emptyList()

fun handleOpenChatIntent(contactId: String) {
val c = activeContainer
Expand All @@ -42,6 +43,12 @@ class StadeApplication : Application() {
if (c != null) c.pendingInvite.value = inviteText else pendingInviteAtBoot = inviteText
}

fun handleStickerFilesIntent(files: List<dev.stade.ui.PickedStickerFile>) {
if (files.isEmpty()) return
val c = activeContainer
if (c != null) c.pendingStickerImport.value = files else pendingStickersAtBoot = files
}

fun handleOpenStadiumIntent(stadiumId: String) {
val c = activeContainer
if (c != null) c.pendingOpenStadium.value = stadiumId else pendingStadiumAtBoot = stadiumId
Expand Down Expand Up @@ -98,6 +105,10 @@ class StadeApplication : Application() {
}
pendingInviteAtBoot?.let { c.pendingInvite.value = it }
pendingInviteAtBoot = null
if (pendingStickersAtBoot.isNotEmpty()) {
c.pendingStickerImport.value = pendingStickersAtBoot
pendingStickersAtBoot = emptyList()
}
}
)
var startedCount = 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package dev.stade.ui

import android.net.Uri
import android.provider.OpenableColumns
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalContext
import dev.stade.sticker.MAX_IMPORT_ARCHIVE_BYTES
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

actual class StickerImportLauncher(private val onPick: () -> Unit) {
actual fun pickFiles() = onPick()
}

@Composable
actual fun rememberStickerImportLauncher(
onPicked: (List<PickedStickerFile>) -> Unit
): StickerImportLauncher {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val callback by rememberUpdatedState(onPicked)

val open = rememberLauncherForActivityResult(
ActivityResultContracts.OpenMultipleDocuments()
) { uris ->
if (uris.isNullOrEmpty()) {
callback(emptyList())
return@rememberLauncherForActivityResult
}
scope.launch {
val files = withContext(Dispatchers.IO) {
uris.mapNotNull { uri -> readPicked(context.contentResolver, uri) }
}
callback(files)
}
}

return remember(open) {
StickerImportLauncher { open.launch(arrayOf("*/*")) }
}
}

private fun readPicked(
resolver: android.content.ContentResolver,
uri: Uri
): PickedStickerFile? = runCatching {
val name = resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) cursor.getString(0) else null
} ?: uri.lastPathSegment ?: "sticker"

val bytes = resolver.openInputStream(uri)?.use { input ->
val out = java.io.ByteArrayOutputStream()
val buffer = ByteArray(64 * 1024)
while (true) {
val read = input.read(buffer)
if (read < 0) break
if (out.size() + read > MAX_IMPORT_ARCHIVE_BYTES) return@use null
out.write(buffer, 0, read)
}
out.toByteArray()
} ?: return null

PickedStickerFile(name, bytes)
}.getOrNull()
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ actual fun AnimatedImage(
contentScale: ContentScale
) {
val playable = remember(bytes) {
isGifBytes(bytes) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
isAnimatedBytes(bytes) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
}
if (!playable) {
StaticImageBytes(bytes, contentDescription, modifier, contentScale)
Expand Down
19 changes: 19 additions & 0 deletions composeApp/src/commonMain/kotlin/dev/stade/AppContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,21 @@ class AppContainer(
runCatching { createdDriver.execute(null, "CREATE TABLE IF NOT EXISTS Sticker (id TEXT NOT NULL PRIMARY KEY, ownerId TEXT NOT NULL, imageBytes BLOB NOT NULL, createdAt INTEGER NOT NULL)", 0) }
runCatching { createdDriver.execute(null, "CREATE INDEX IF NOT EXISTS idxSticker ON Sticker(ownerId)", 0) }
}
runCatching {
createdDriver.executeQuery(null, "SELECT packId, contentHash FROM Sticker LIMIT 0",
{ _: SqlCursor -> QueryResult.Value(Unit) }, 0)
}.onFailure {
runCatching { createdDriver.execute(null, "ALTER TABLE Sticker ADD COLUMN packId TEXT", 0) }
runCatching { createdDriver.execute(null, "ALTER TABLE Sticker ADD COLUMN contentHash TEXT NOT NULL DEFAULT ''", 0) }
runCatching { createdDriver.execute(null, "CREATE INDEX IF NOT EXISTS idxStickerPackRef ON Sticker(packId)", 0) }
}
runCatching {
createdDriver.executeQuery(null, "SELECT id FROM StickerPack LIMIT 0",
{ _: SqlCursor -> QueryResult.Value(Unit) }, 0)
}.onFailure {
runCatching { createdDriver.execute(null, "CREATE TABLE IF NOT EXISTS StickerPack (id TEXT NOT NULL PRIMARY KEY, ownerId TEXT NOT NULL, title TEXT NOT NULL, author TEXT NOT NULL DEFAULT '', origin TEXT NOT NULL DEFAULT '', createdAt INTEGER NOT NULL)", 0) }
runCatching { createdDriver.execute(null, "CREATE INDEX IF NOT EXISTS idxStickerPack ON StickerPack(ownerId)", 0) }
}
runCatching {
createdDriver.executeQuery(null, "SELECT sessionId FROM VanishSession LIMIT 0",
{ _: SqlCursor -> QueryResult.Value(Unit) }, 0)
Expand Down Expand Up @@ -248,6 +263,8 @@ class AppContainer(

val pendingGoHome = MutableStateFlow(false)

val pendingStickerImport = MutableStateFlow<List<dev.stade.ui.PickedStickerFile>>(emptyList())

val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + backgroundFailures)

@Volatile var isClosed = false
Expand All @@ -259,6 +276,7 @@ class AppContainer(
pendingInvite.value = null
pendingOpenChat.value = null
pendingGoHome.value = false
pendingStickerImport.value = emptyList()
activeContactId = null
runCatching {
db.stadeDbQueries.transaction {
Expand All @@ -278,6 +296,7 @@ class AppContainer(
db.stadeDbQueries.wipeKeyValue()
db.stadeDbQueries.wipeProcessedEnvelope()
db.stadeDbQueries.wipeStickers()
db.stadeDbQueries.wipeStickerPacks()
db.stadeDbQueries.wipeReactions()
db.stadeDbQueries.wipeVanishSessions()
db.stadeDbQueries.wipeScheduledMessages()
Expand Down
2 changes: 1 addition & 1 deletion composeApp/src/commonMain/kotlin/dev/stade/AppVersion.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package dev.stade

const val APP_VERSION = "0.3.3"
const val APP_VERSION = "0.3.4"
3 changes: 3 additions & 0 deletions composeApp/src/commonMain/kotlin/dev/stade/BootContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,8 @@ class BootContext(
from.pendingOpenStadium.value?.let { to.pendingOpenStadium.value = it }
from.pendingOpenGroup.value?.let { to.pendingOpenGroup.value = it }
if (from.pendingGoHome.value) to.pendingGoHome.value = true
if (from.pendingStickerImport.value.isNotEmpty()) {
to.pendingStickerImport.value = from.pendingStickerImport.value
}
}
}
12 changes: 11 additions & 1 deletion composeApp/src/commonMain/kotlin/dev/stade/sticker/Sticker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ data class Sticker(
val id: String,
val ownerId: String,
val imageBytes: ByteArray,
val createdAt: Long
val createdAt: Long,
val packId: String? = null
) {
override fun equals(other: Any?): Boolean = other is Sticker && other.id == id
override fun hashCode(): Int = id.hashCode()
}

data class StickerPack(
val id: String,
val ownerId: String,
val title: String,
val author: String,
val origin: String,
val createdAt: Long
)
Loading
Loading