Skip to content
Open
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
85 changes: 39 additions & 46 deletions rnmodules/kb-common/src/ItemProviderHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,30 +173,26 @@ public class ItemProviderHelper: NSObject {
}

private func handleAndCompleteMediaFile(_ url: URL, isVideo: Bool) {
if isVideo {
MediaUtils.processVideo(fromOriginal: url) { error, scaled in
if let error = error {
let completion: (Error?, URL?) -> Void = { error, scaled in
if let error = error {
if isVideo {
self.completeItemAndAppendManifestAndLogError(
text: "handleAndCompleteMediaFile", error: error)
return
} else {
// Unscalable image (odd format, corrupt, etc): still deliver the original
self.completeItemAndAppendManifest(type: "file", originalFileURL: url)
}
self.completeItemAndAppendManifest(
type: isVideo ? "video" : "image",
originalFileURL: url,
scaledFileURL: scaled!)
return
}
self.completeItemAndAppendManifest(
type: isVideo ? "video" : "image",
originalFileURL: url,
scaledFileURL: scaled!)
}
if isVideo {
MediaUtils.processVideo(fromOriginal: url, completion: completion)
} else {
MediaUtils.processImage(fromOriginal: url) { error, scaled in
if let error = error {
self.completeItemAndAppendManifestAndLogError(
text: "handleAndCompleteMediaFile", error: error)
return
}
self.completeItemAndAppendManifest(
type: isVideo ? "video" : "image",
originalFileURL: url,
scaledFileURL: scaled!)
}
MediaUtils.processImage(fromOriginal: url, completion: completion)
}
}

Expand Down Expand Up @@ -315,25 +311,23 @@ public class ItemProviderHelper: NSObject {
}
}

private func sendMovie(_ url: URL?) {
var filePayloadURL: URL?
var error: Error?

if let url = url {
filePayloadURL = getPayloadURL(from: url)
do {
try FileManager.default.copyItem(at: url, to: filePayloadURL!)
} catch let copyError {
error = copyError
}
// Copy the provider's temporary file into our payload folder (it's deleted when
// the completion handler returns), then scale it so the manifest carries both
// original and compressed versions.
private func sendMedia(_ url: URL?, isVideo: Bool) {
guard let url = url else {
completeItemAndAppendManifestAndLogError(text: "sendMedia: unable to decode share", error: nil)
return
}

if let filePayloadURL = filePayloadURL, error == nil {
handleAndCompleteMediaFile(filePayloadURL, isVideo: true)
} else {
completeItemAndAppendManifestAndLogError(
text: "movieFileHandlerSimple2: copy error", error: error)
let filePayloadURL = getPayloadURL(from: url)
do {
try FileManager.default.copyItem(at: url, to: filePayloadURL)
} catch {
completeItemAndAppendManifestAndLogError(text: "sendMedia: copy error", error: error)
return
}
handleAndCompleteMediaFile(filePayloadURL, isVideo: isVideo)
}

private func sendImage(_ imgData: Data?) {
Expand Down Expand Up @@ -363,7 +357,11 @@ public class ItemProviderHelper: NSObject {
}

let movieHandler: @Sendable (URL?, Error?) -> Void = { url, error in
self.sendMovie(error == nil ? url : nil)
self.sendMedia(error == nil ? url : nil, isVideo: true)
}

let imageFileHandler: @Sendable (URL?, Error?) -> Void = { url, error in
self.sendMedia(error == nil ? url : nil, isVideo: false)
}

let imageHandler: @Sendable (NSSecureCoding?, Error?) -> Void = { item, error in
Expand Down Expand Up @@ -430,17 +428,12 @@ public class ItemProviderHelper: NSObject {
item.loadFileRepresentation(forTypeIdentifier: stype, completionHandler: movieHandler)
break
}
// Images (PNG, GIF, JPEG)
else if type.conforms(to: .png) || type.conforms(to: .gif) || type.conforms(to: .jpeg) {
incrementUnprocessed()
item.loadFileRepresentation(forTypeIdentifier: stype, completionHandler: fileHandler)
break
}
// HEIC Images
else if stype == "public.heic" {
// Images (PNG, GIF, JPEG, HEIC): keep the original file, offer a scaled version
else if type.conforms(to: .png) || type.conforms(to: .gif) || type.conforms(to: .jpeg)
|| stype == "public.heic"
{
incrementUnprocessed()
item.loadFileRepresentation(
forTypeIdentifier: "public.heic", completionHandler: fileHandler)
item.loadFileRepresentation(forTypeIdentifier: stype, completionHandler: imageFileHandler)
break
}
// Other Images (coerce)
Expand Down
161 changes: 64 additions & 97 deletions shared/android/app/src/main/java/io/keybase/ossifrage/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,7 @@ class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
NativeLogger.info("Activity onCreate")
setupKBRuntime(this, true)
cachedIntent = intent
if (Intent.ACTION_SEND == intent.action || Intent.ACTION_SEND_MULTIPLE == intent.action) {
normalizeShareIntent(intent)
cachedIntent = intent
pendingShareUris = extractSharedUris(intent).toMutableList()
pendingShareSubject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
pendingShareText = intent.getStringExtra(Intent.EXTRA_TEXT)
}
val bundleFromNotification = intent.getBundleExtra("notification")
if (bundleFromNotification != null) {
KbModule.setInitialNotification(bundleFromNotification.clone() as Bundle)
}
captureIntent(intent)

super.onCreate(null)
Handler(Looper.getMainLooper()).postDelayed({
Expand Down Expand Up @@ -184,27 +173,31 @@ class MainActivity : ReactActivity() {

private var cachedIntent: Intent? = null

private var pendingShareUris: MutableList<Uri>? = null
private var pendingShareUris: List<Uri>? = null
private var pendingShareSubject: String? = null
private var pendingShareText: String? = null

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
// Snapshot share/notification data out of the intent right away: share URI
// permission grants and clip data are tied to the delivered intent, and JS may
// not be ready to consume them until much later (see tryHandleIntentWithRetry).
private fun captureIntent(intent: Intent) {
cachedIntent = intent
if (Intent.ACTION_SEND == intent.action || Intent.ACTION_SEND_MULTIPLE == intent.action) {
normalizeShareIntent(intent)
setIntent(intent)
cachedIntent = intent
pendingShareUris = extractSharedUris(intent).toMutableList()
pendingShareUris = extractSharedUris(intent)
pendingShareSubject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
pendingShareText = intent.getStringExtra(Intent.EXTRA_TEXT)
}
val bundleFromNotification = intent.getBundleExtra("notification")
if (bundleFromNotification != null) {
KbModule.setInitialNotification(bundleFromNotification.clone() as Bundle)
}
NativeLogger.info("MainActivity.onNewIntent: action=${intent.action}, uriCount=${pendingShareUris?.size ?: 0}, hasNotification=${bundleFromNotification != null}")
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
captureIntent(intent)
NativeLogger.info("MainActivity.onNewIntent: action=${intent.action}, uriCount=${pendingShareUris?.size ?: 0}, hasNotification=${intent.getBundleExtra("notification") != null}")
}

private var jsIsListening = false
Expand All @@ -216,14 +209,6 @@ class MainActivity : ReactActivity() {

private var handledIntentHash: String? = null

private fun normalizeShareIntent(intent: Intent) {
val uris = extractSharedUris(intent)
intent.removeExtra(Intent.EXTRA_STREAM)
if (uris.isNotEmpty()) {
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
}
}

private fun extractSharedUris(intent: Intent): List<Uri> {
val action = intent.action
if (Intent.ACTION_SEND != action && Intent.ACTION_SEND_MULTIPLE != action) {
Expand Down Expand Up @@ -292,109 +277,91 @@ class MainActivity : ReactActivity() {
// If other sources start the app, we can get their intent data the same way.
val bundleFromNotification = intent.getBundleExtra("notification")

var didSomething = false

if (bundleFromNotification != null) {
// Prevent duplicate handling of the same notification
val convID = bundleFromNotification.getString("convID") ?: bundleFromNotification.getString("c")
val messageId = bundleFromNotification.getString("msgID") ?: bundleFromNotification.getString("d") ?: ""
val intentHash = "${convID}_${messageId}"
val shouldEmitNotification = handledIntentHash != intentHash
if (!shouldEmitNotification) {
if (handledIntentHash == intentHash) {
NativeLogger.info("MainActivity.handleIntent skipping duplicate notification: $intentHash")
} else {
handledIntentHash = intentHash
NativeLogger.info("MainActivity.handleIntent processing notification: $intentHash")

// If there are any other bundle sources we care about, emit them here
val bundle1 = bundleFromNotification.clone() as Bundle
val bundle2 = bundleFromNotification.clone() as Bundle
val payload1 = Arguments.fromBundle(bundle1)
rc.emitDeviceEvent(
"initialIntentFromNotification",
payload1
)
val payload2 = Arguments.fromBundle(bundle2)
rc.emitDeviceEvent(
"onPushNotification",
payload2
)
didSomething = true
rc.emitDeviceEvent("initialIntentFromNotification", Arguments.fromBundle(bundleFromNotification))
rc.emitDeviceEvent("onPushNotification", Arguments.fromBundle(bundleFromNotification))
}

intent.removeExtra("notification")
}

val action = intent.action
if (Intent.ACTION_SEND == action || Intent.ACTION_SEND_MULTIPLE == action) {
val uris = pendingShareUris?.also { pendingShareUris = null }
?: extractSharedUris(intent)
val subject = pendingShareSubject?.also { pendingShareSubject = null }
?: intent.getStringExtra(Intent.EXTRA_SUBJECT)
val text = pendingShareText?.also { pendingShareText = null }
?: intent.getStringExtra(Intent.EXTRA_TEXT)
val uris = pendingShareUris.orEmpty().also { pendingShareUris = null }
val subject = pendingShareSubject.also { pendingShareSubject = null }
val text = pendingShareText.also { pendingShareText = null }

// Strip consumed extras so an activity recreation (which redelivers this
// same intent instance) doesn't re-share.
intent.removeExtra(Intent.EXTRA_STREAM)
intent.removeExtra(Intent.EXTRA_SUBJECT)
intent.removeExtra(Intent.EXTRA_TEXT)
intent.setClipData(null)

val sb = StringBuilder()
if (subject != null) {
sb.append(subject)
}
if (subject != null && text != null) {
sb.append(" ")
}
if (text != null) {
sb.append(text)
}

val textPayload = sb.toString()
val filePaths = uris.mapNotNull { uri ->
try {
readFileFromUri(rc, uri)
} catch (e: SecurityException) {
null
}
}.toTypedArray()

val intentType = intent.type
val isTextMime = intentType?.startsWith("text/") == true
val textPayload = listOfNotNull(subject, text).joinToString(" ")
val isTextMime = intent.type?.startsWith("text/") == true

if (isTextMime && textPayload.isNotEmpty()) {
// Text-type intent (e.g. URL from Chrome): prefer text over any preview images
val args = Arguments.createMap()
args.putString("text", text ?: textPayload)
rc.emitDeviceEvent("onShareData", args)
didSomething = true
} else if (filePaths.isNotEmpty()) {
val args = Arguments.createMap()
val lPaths = Arguments.createArray()
for (path in filePaths) {
lPaths.pushString(path)
emitShareText(rc, text ?: textPayload)
} else if (uris.isEmpty()) {
if (textPayload.isNotEmpty()) {
emitShareText(rc, textPayload)
}
args.putArray("localPaths", lPaths)
rc.emitDeviceEvent("onShareData", args)
didSomething = true
} else if (textPayload.isNotEmpty()) {
// Fallback: non-text MIME but no files resolved, send text
val args = Arguments.createMap()
args.putString("text", textPayload)
rc.emitDeviceEvent("onShareData", args)
didSomething = true
} else if (uris.isNotEmpty()) {
val args = Arguments.createMap()
args.putArray("localPaths", Arguments.createArray())
rc.emitDeviceEvent("onShareData", args)
didSomething = true
} else {
// Copying out of the content providers can be slow for big files; don't
// block the main thread on it.
Thread {
val filePaths = uris.mapNotNull { uri ->
try {
readFileFromUri(rc, uri)
} catch (e: SecurityException) {
null
}
}
if (filePaths.isNotEmpty()) {
emitShareFiles(rc, filePaths)
} else if (textPayload.isNotEmpty()) {
// Fallback: non-text MIME but no files resolved, send text
emitShareText(rc, textPayload)
} else {
emitShareFiles(rc, emptyList())
}
}.start()
}
}

cachedIntent = null
return true
}

private fun emitShareText(rc: ReactContext, text: String) {
val args = Arguments.createMap()
args.putString("text", text)
rc.emitDeviceEvent("onShareData", args)
}

private fun emitShareFiles(rc: ReactContext, paths: List<String>) {
val args = Arguments.createMap()
val lPaths = Arguments.createArray()
for (path in paths) {
lPaths.pushString(path)
}
args.putArray("localPaths", lPaths)
rc.emitDeviceEvent("onShareData", args)
}

override fun getMainComponentName(): String = "Keybase"

override fun createReactActivityDelegate(): ReactActivityDelegate {
Expand Down
Loading