Release IKAR 1.0.7: port navigation and sharing improvements, harden calls
Tests / test (push) Canceled after 0s
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Tests / test (push) Canceled after 0s
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/.kotlin/
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
|
||||
@@ -77,8 +77,6 @@ class MainActivity : AudioServiceFragmentActivity() {
|
||||
|
||||
private var pendingCall: Map<String, Any?>? = null
|
||||
private var pendingChat: Long = 0L
|
||||
private var pendingShare: Map<String, Any?>? = null
|
||||
private var pendingShareTask: java.util.concurrent.Future<Map<String, Any?>?>? = null
|
||||
private val shareExecutor = java.util.concurrent.Executors.newSingleThreadExecutor()
|
||||
private val shareHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
@@ -507,26 +505,25 @@ class MainActivity : AudioServiceFragmentActivity() {
|
||||
"ru.ikar.app/share",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"consumeInitialShare" -> {
|
||||
"consumeInitialShare", "acknowledgeShare", "releaseShare" -> {
|
||||
stashShare(intent, emit = false)
|
||||
val ready = pendingShare
|
||||
val task = pendingShareTask
|
||||
if (ready != null) {
|
||||
pendingShare = null
|
||||
result.success(ready)
|
||||
} else if (task != null) {
|
||||
pendingShareTask = null
|
||||
shareExecutor.execute {
|
||||
val payload = try {
|
||||
task.get()
|
||||
} catch (e: Exception) {
|
||||
Log.w(SHARE_TAG, "materialize failed: $e")
|
||||
null
|
||||
shareExecutor.execute {
|
||||
try {
|
||||
val value = when (call.method) {
|
||||
"consumeInitialShare" -> ShareQueue.consume(applicationContext)
|
||||
"acknowledgeShare" -> {
|
||||
ShareQueue.acknowledge(applicationContext, call.argument<String>("id")!!)
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
ShareQueue.release(applicationContext, call.argument<String>("id")!!)
|
||||
null
|
||||
}
|
||||
}
|
||||
shareHandler.post { result.success(payload) }
|
||||
shareHandler.post { result.success(value) }
|
||||
} catch (e: Exception) {
|
||||
shareHandler.post { result.error("SHARE_QUEUE", e.message, null) }
|
||||
}
|
||||
} else {
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
"clearCache" -> {
|
||||
@@ -658,23 +655,14 @@ class MainActivity : AudioServiceFragmentActivity() {
|
||||
intent.action = Intent.ACTION_MAIN
|
||||
intent.removeExtra(Intent.EXTRA_STREAM)
|
||||
intent.removeExtra(Intent.EXTRA_TEXT)
|
||||
val task = shareExecutor.submit<Map<String, Any?>?> {
|
||||
ShareIntake.materialize(applicationContext, snapshot)
|
||||
}
|
||||
if (!emit) {
|
||||
pendingShareTask = task
|
||||
return
|
||||
}
|
||||
shareExecutor.execute {
|
||||
val payload = try {
|
||||
task.get()
|
||||
try {
|
||||
val payload = ShareIntake.materialize(applicationContext, snapshot)
|
||||
?: return@execute
|
||||
ShareQueue.enqueue(applicationContext, payload)
|
||||
if (emit) shareHandler.post { ShareIntake.sink?.success(true) }
|
||||
} catch (e: Exception) {
|
||||
Log.w(SHARE_TAG, "materialize failed: $e")
|
||||
null
|
||||
} ?: return@execute
|
||||
shareHandler.post {
|
||||
val sink = ShareIntake.sink
|
||||
if (sink != null) sink.success(payload) else pendingShare = payload
|
||||
Log.w(SHARE_TAG, "cannot persist share: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ object ShareIntake {
|
||||
|
||||
fun clearCache(context: Context, paths: List<String>) {
|
||||
try {
|
||||
val dir = File(context.cacheDir, CACHE_DIR).canonicalFile
|
||||
val dir = File(context.filesDir, CACHE_DIR).canonicalFile
|
||||
if (!dir.isDirectory) return
|
||||
for (path in paths) {
|
||||
val file = File(path).canonicalFile
|
||||
@@ -96,7 +96,7 @@ object ShareIntake {
|
||||
val displayName = queryDisplayName(resolver, uri) ?: fallbackName(uri, mime)
|
||||
|
||||
return try {
|
||||
val dir = File(context.cacheDir, CACHE_DIR).apply { mkdirs() }
|
||||
val dir = File(context.filesDir, CACHE_DIR).apply { mkdirs() }
|
||||
val target = File(dir, "${System.currentTimeMillis()}_${seq.incrementAndGet()}_${sanitize(displayName)}")
|
||||
resolver.openInputStream(uri).use { input ->
|
||||
if (input == null) return null
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package ru.ikar.app
|
||||
|
||||
import android.content.Context
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
/** Durable intake; all methods are called on MainActivity's share executor. */
|
||||
object ShareQueue {
|
||||
private fun directory(context: Context) = File(context.filesDir, "share_queue").apply { mkdirs() }
|
||||
private fun record(context: Context, id: String): File {
|
||||
require(id.matches(Regex("[0-9a-f-]{36}")))
|
||||
return File(directory(context), "$id.json")
|
||||
}
|
||||
|
||||
fun enqueue(context: Context, payload: Map<String, Any?>) {
|
||||
val id = UUID.randomUUID().toString()
|
||||
val target = record(context, id)
|
||||
val json = JSONObject(payload).put("id", id).put("receivedAt", System.currentTimeMillis())
|
||||
val staged = File(target.path + ".next")
|
||||
staged.writeText(json.toString())
|
||||
check(staged.renameTo(target)) { "Cannot persist shared content" }
|
||||
}
|
||||
|
||||
fun consume(context: Context): Map<String, Any?>? {
|
||||
val entries = directory(context).listFiles()?.filter { it.extension == "json" }
|
||||
?.sortedBy { it.lastModified() } ?: return null
|
||||
for (entry in entries) {
|
||||
val json = try { JSONObject(entry.readText()) } catch (_: Exception) { continue }
|
||||
if (json.optBoolean("acknowledged")) {
|
||||
release(context, json.getString("id"))
|
||||
continue
|
||||
}
|
||||
return decode(json)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun acknowledge(context: Context, id: String) {
|
||||
val target = record(context, id)
|
||||
if (!target.exists()) return
|
||||
val json = JSONObject(target.readText()).put("acknowledged", true)
|
||||
val staged = File(target.path + ".next")
|
||||
staged.writeText(json.toString())
|
||||
check(staged.renameTo(target))
|
||||
}
|
||||
|
||||
fun release(context: Context, id: String) {
|
||||
val target = record(context, id)
|
||||
if (!target.exists()) return
|
||||
val json = JSONObject(target.readText())
|
||||
val files = json.optJSONArray("files")
|
||||
val paths = (0 until (files?.length() ?: 0)).mapNotNull {
|
||||
files?.optJSONObject(it)?.optString("path")
|
||||
}
|
||||
ShareIntake.clearCache(context, paths)
|
||||
check(target.delete())
|
||||
}
|
||||
|
||||
private fun decode(json: JSONObject): Map<String, Any?> = json.keys().asSequence().associateWith { key ->
|
||||
when (val value = json.get(key)) {
|
||||
JSONObject.NULL -> null
|
||||
is org.json.JSONArray -> (0 until value.length()).map { decode(value.getJSONObject(it)) }
|
||||
else -> value
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user