Files
ARIA-AGENT/android/android/app/src/main/java/com/ariacockpit/ShareIntentModule.kt
T
duffyduckandClaude Opus 4.8 b6195d3a53 feat(app): Teilen-Ziel — Bilder/Videos/Dateien aus anderen Apps an ARIA
'Teilen -> ARIA' aus WhatsApp, Galerie, Browser & Co. Ablauf wie gewuenscht:
sehen WAS geteilt wurde -> WOHIN (Projekt oder Hauptchat) -> optional Text dazu.
Ohne Text geht genauso.

- Manifest: intent-filter fuer ACTION_SEND (text/image/video/audio/application)
  und ACTION_SEND_MULTIPLE. launchMode=singleTask war bereits gesetzt.
- ShareIntentModule (Kotlin): liest den Intent und kopiert die Inhalte SOFORT in
  den App-Cache — die Leseerlaubnis fuer content://-URIs gilt nur fuer diesen
  Intent und waere nach der Projektauswahl womoeglich erloschen. Liefert echte
  Dateipfade, dadurch laeuft Teilen durch exakt dieselbe Upload-Strecke wie die
  Bueroklammer (RNFS -> Base64 -> Anhang im Chat).
- MainActivity.onNewIntent: setIntent + Event an JS (best effort). Zweitweg in
  JS ueber AppState 'active' -> getSharedData(), falls das Event je nach
  RN-Architektur nicht durchkommt; getSharedData markiert den Intent als
  verbraucht, also kein doppeltes Aufpoppen.
- ShareModal: Vorschau der geteilten Inhalte, Projektliste (inkl. Hauptchat),
  optionales Textfeld. Senden erst nach Zielwahl moeglich.
- sendPendingAttachments nimmt die Anhaenge jetzt optional als Parameter — beim
  Teilen waere der State zum Aufrufzeitpunkt noch leer.
tsc --noEmit: 0 Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 01:19:55 +02:00

155 lines
6.3 KiB
Kotlin

package com.ariacockpit
import android.content.Intent
import android.net.Uri
import android.provider.OpenableColumns
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.core.DeviceEventManagerModule
import java.io.File
/**
* Teilen-Ziel: nimmt Inhalte entgegen, die aus anderen Apps (WhatsApp, Galerie,
* Browser ...) per "Teilen → ARIA" geschickt werden.
*
* Die geteilten Dateien kommen als content://-URI, auf die nur DIESE App und nur
* JETZT zugreifen darf. Deshalb kopieren wir sie sofort in den App-Cache und
* geben einen echten Dateipfad zurueck — den kann die JS-Seite wie eine normal
* ausgewaehlte Datei weiterverarbeiten (RNFS.readFile → Base64 → Upload).
*
* Ablauf:
* App geschlossen → Intent liegt beim Start an → getSharedData()
* App laeuft → onNewIntent (launchMode=singleTask) → Event 'ShareReceived'
*/
class ShareIntentModule(private val reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = "ShareIntent"
companion object {
private const val EVENT = "ShareReceived"
/** Vom MainActivity.onNewIntent gerufen, wenn die App schon laeuft. */
@JvmStatic
fun notifyNewIntent(ctx: ReactApplicationContext?, intent: Intent?) {
if (ctx == null || intent == null || !isShare(intent)) return
try {
val data = extract(ctx, intent)
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(EVENT, data)
} catch (_: Throwable) {
}
}
private fun isShare(i: Intent): Boolean =
i.action == Intent.ACTION_SEND || i.action == Intent.ACTION_SEND_MULTIPLE
/** Baut {text, items:[{path,name,type,size}]} aus dem Intent. */
private fun extract(ctx: ReactApplicationContext, intent: Intent): WritableMap {
val out = Arguments.createMap()
out.putString("text", intent.getStringExtra(Intent.EXTRA_TEXT) ?: "")
val uris = mutableListOf<Uri>()
if (intent.action == Intent.ACTION_SEND) {
@Suppress("DEPRECATION")
(intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM))?.let { uris.add(it) }
} else {
@Suppress("DEPRECATION")
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
?.forEach { uris.add(it) }
}
val items: WritableArray = Arguments.createArray()
for (uri in uris) {
copyToCache(ctx, uri)?.let { items.pushMap(it) }
}
out.putArray("items", items)
return out
}
/**
* Kopiert den geteilten Inhalt in den Cache. Noetig, weil die Leseerlaubnis
* fuer die content://-URI nur fuer diesen Intent gilt — spaeter (nach der
* Projektauswahl) waere sie womoeglich schon erloschen.
*/
private fun copyToCache(ctx: ReactApplicationContext, uri: Uri): WritableMap? {
return try {
val cr = ctx.contentResolver
var name = "geteilt"
var size = 0L
cr.query(uri, null, null, null, null)?.use { c ->
val ni = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val si = c.getColumnIndex(OpenableColumns.SIZE)
if (c.moveToFirst()) {
if (ni >= 0) c.getString(ni)?.let { name = it }
if (si >= 0) size = c.getLong(si)
}
}
val type = cr.getType(uri) ?: "application/octet-stream"
val dir = File(ctx.cacheDir, "shared").apply { mkdirs() }
// Eindeutiger Name, damit zwei gleichnamige Bilder sich nicht ueberschreiben.
val safe = name.replace(Regex("[^A-Za-z0-9._-]"), "_").take(80)
val dst = File(dir, "${System.currentTimeMillis()}_$safe")
cr.openInputStream(uri)?.use { input ->
dst.outputStream().use { output -> input.copyTo(output) }
} ?: return null
Arguments.createMap().apply {
putString("path", dst.absolutePath)
putString("name", name)
putString("type", type)
putDouble("size", (if (size > 0) size else dst.length()).toDouble())
}
} catch (_: Throwable) {
null
}
}
}
/**
* Holt den Inhalt, mit dem die App GESTARTET wurde (App war zu).
* Gibt {text, items:[]} zurueck — items leer, wenn nichts geteilt wurde.
*/
@ReactMethod
fun getSharedData(promise: Promise) {
try {
val activity = currentActivity
val intent = activity?.intent
if (intent == null || !isShare(intent)) {
promise.resolve(Arguments.createMap().apply {
putString("text", "")
putArray("items", Arguments.createArray())
})
return
}
val data = extract(reactContext, intent)
// Verbraucht markieren, damit ein Rotieren/Zurueckkehren den Dialog
// nicht erneut oeffnet.
activity.intent = Intent(Intent.ACTION_MAIN)
promise.resolve(data)
} catch (e: Throwable) {
promise.reject("SHARE_READ_FAILED", e.message, e)
}
}
/** Aufgeraeumte Kopien loeschen (nach erfolgreichem Senden). */
@ReactMethod
fun clearCache(promise: Promise) {
try {
File(reactContext.cacheDir, "shared").listFiles()?.forEach { it.delete() }
promise.resolve(true)
} catch (e: Throwable) {
promise.resolve(false)
}
}
// Fuer NativeEventEmitter (RN verlangt diese beiden Methoden).
@ReactMethod fun addListener(eventName: String) {}
@ReactMethod fun removeListeners(count: Int) {}
}