'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>
155 lines
6.3 KiB
Kotlin
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) {}
|
|
}
|