236 lines
10 KiB
Diff
236 lines
10 KiB
Diff
diff --git a/android/src/main/java/expo/modules/sharing/ShareIntoUtils.kt b/android/src/main/java/expo/modules/sharing/ShareIntoUtils.kt
|
|
--- a/android/src/main/java/expo/modules/sharing/ShareIntoUtils.kt
|
|
+++ b/android/src/main/java/expo/modules/sharing/ShareIntoUtils.kt
|
|
@@ -47,9 +47,26 @@
|
|
internal fun isShareIntent(intent: Intent): Boolean {
|
|
val allowedShareActions =
|
|
listOf("android.intent.action.SEND", "android.intent.action.SEND_MULTIPLE")
|
|
- return intent.type != null && allowedShareActions.contains(intent.action)
|
|
+ // Android can recreate a task with its original SEND intent. That is a
|
|
+ // history launch, not a new share, even after the JS process has been killed.
|
|
+ return intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == 0 &&
|
|
+ intent.type != null && allowedShareActions.contains(intent.action)
|
|
}
|
|
|
|
+internal fun getShareIntentUris(intent: Intent): List<Uri> {
|
|
+ val streams = when (intent.action) {
|
|
+ Intent.ACTION_SEND -> listOfNotNull(intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))
|
|
+ Intent.ACTION_SEND_MULTIPLE -> intent.getParcelableArrayListExtraCompat<Uri>(Intent.EXTRA_STREAM).orEmpty()
|
|
+ else -> emptyList()
|
|
+ }
|
|
+ if (streams.isNotEmpty()) {
|
|
+ return streams
|
|
+ }
|
|
+
|
|
+ val clipData = intent.clipData ?: return emptyList()
|
|
+ return (0 until clipData.itemCount).mapNotNull { index -> clipData.getItemAt(index).uri }
|
|
+}
|
|
+
|
|
internal inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(name: String): T? {
|
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
getParcelableExtra(name, T::class.java)
|
|
diff --git a/android/src/main/java/expo/modules/sharing/dataParsers/ResolvingShareIntentDataParser.kt b/android/src/main/java/expo/modules/sharing/dataParsers/ResolvingShareIntentDataParser.kt
|
|
--- a/android/src/main/java/expo/modules/sharing/dataParsers/ResolvingShareIntentDataParser.kt
|
|
+++ b/android/src/main/java/expo/modules/sharing/dataParsers/ResolvingShareIntentDataParser.kt
|
|
@@ -6,9 +6,11 @@
|
|
import android.net.Uri
|
|
import android.provider.OpenableColumns
|
|
import java.io.File
|
|
+import java.io.FileNotFoundException
|
|
import java.io.FileOutputStream
|
|
import java.net.HttpURLConnection
|
|
import java.net.URL
|
|
+import java.util.UUID
|
|
import kotlin.text.indexOf
|
|
import kotlin.text.substring
|
|
|
|
@@ -25,21 +27,22 @@
|
|
}
|
|
|
|
private fun handleSendAction(context: Context, intent: Intent, type: String): List<ResolvedSharePayload> {
|
|
- return if (type == "text/plain") {
|
|
+ val uri = getShareIntentUris(intent).firstOrNull()
|
|
+ return if (uri != null) {
|
|
+ listOf(resolveUri(context, uri, type))
|
|
+ } else if (type == "text/plain") {
|
|
extractTextPayload(intent)
|
|
- } else {
|
|
- val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
|
- listOfNotNull(uri?.let { resolveUri(context, it, type) })
|
|
- }
|
|
+ } else emptyList()
|
|
}
|
|
|
|
private fun handleSendMultipleAction(context: Context, intent: Intent, type: String): List<ResolvedSharePayload> {
|
|
- val uris = intent.getParcelableArrayListExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
|
- return uris?.map { resolveUri(context, it, type) } ?: emptyList()
|
|
+ return getShareIntentUris(intent).map { resolveUri(context, it, type) }
|
|
}
|
|
|
|
private fun extractTextPayload(intent: Intent): List<ResolvedSharePayload> {
|
|
- val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return emptyList()
|
|
+ val text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
|
|
+ ?: intent.clipData?.takeIf { it.itemCount > 0 }?.getItemAt(0)?.text?.toString()
|
|
+ ?: return emptyList()
|
|
|
|
if (android.util.Patterns.WEB_URL.matcher(text).matches()) {
|
|
return listOf(resolveUrlContext(text))
|
|
@@ -58,19 +61,27 @@
|
|
val contentResolver = context.contentResolver
|
|
val fileName = getFileName(contentResolver, uri) ?: "unknown"
|
|
val fileSize = getFileSize(contentResolver, uri)
|
|
- val specificMimeType = contentResolver.getType(uri) ?: mimeType
|
|
+ val specificMimeType = contentResolver.getType(uri)
|
|
+ ?: mimeType.takeUnless { it == "text/plain" }
|
|
+ ?: "image/*"
|
|
val type = ShareType.fromMimeType(specificMimeType)
|
|
|
|
// Copy to cache dir to ensure access
|
|
- val file = File(context.cacheDir, fileName)
|
|
+ val file = File(context.cacheDir, "shared-${UUID.randomUUID()}-${File(fileName).name}")
|
|
try {
|
|
- contentResolver.openInputStream(uri)?.use { input ->
|
|
+ val inputStream = contentResolver.openInputStream(uri)
|
|
+ ?: throw FileNotFoundException("No readable stream for shared URI")
|
|
+ inputStream.use { input ->
|
|
FileOutputStream(file).use { output ->
|
|
input.copyTo(output)
|
|
}
|
|
}
|
|
+ if (file.length() == 0L) {
|
|
+ throw FileNotFoundException("The shared file is empty")
|
|
+ }
|
|
} catch (e: Exception) {
|
|
- e.printStackTrace()
|
|
+ file.delete()
|
|
+ throw FailedToResolveSharedDataException("Could not read the shared file", e)
|
|
}
|
|
|
|
return ResolvedSharePayload().apply {
|
|
diff --git a/android/src/main/java/expo/modules/sharing/dataParsers/SimpleShareIntentDataParser.kt b/android/src/main/java/expo/modules/sharing/dataParsers/SimpleShareIntentDataParser.kt
|
|
--- a/android/src/main/java/expo/modules/sharing/dataParsers/SimpleShareIntentDataParser.kt
|
|
+++ b/android/src/main/java/expo/modules/sharing/dataParsers/SimpleShareIntentDataParser.kt
|
|
@@ -17,8 +17,13 @@
|
|
}
|
|
|
|
private fun handleSendAction(context: Context, intent: Intent, type: String): List<SharePayload> {
|
|
- return if (type == "text/plain") {
|
|
- val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return emptyList()
|
|
+ val uri = getShareIntentUris(intent).firstOrNull()
|
|
+ return if (uri != null) {
|
|
+ listOf(createUriPayload(context, uri, type))
|
|
+ } else if (type == "text/plain") {
|
|
+ val text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
|
|
+ ?: intent.clipData?.takeIf { it.itemCount > 0 }?.getItemAt(0)?.text?.toString()
|
|
+ ?: return emptyList()
|
|
val isUrl = android.util.Patterns.WEB_URL.matcher(text).matches()
|
|
|
|
listOf(
|
|
@@ -28,19 +33,17 @@
|
|
mimeType = "text/plain"
|
|
}
|
|
)
|
|
- } else {
|
|
- val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
|
- listOfNotNull(uri?.let { createUriPayload(context, it, type) })
|
|
- }
|
|
+ } else emptyList()
|
|
}
|
|
|
|
private fun handleSendMultipleAction(context: Context, intent: Intent, type: String): List<SharePayload> {
|
|
- val uris = intent.getParcelableArrayListExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
|
- return uris?.map { createUriPayload(context, it, type) } ?: emptyList()
|
|
+ return getShareIntentUris(intent).map { createUriPayload(context, it, type) }
|
|
}
|
|
|
|
private fun createUriPayload(context: Context, uri: Uri, defaultType: String): SharePayload {
|
|
- val specificType = context.contentResolver.getType(uri) ?: defaultType
|
|
+ val specificType = context.contentResolver.getType(uri)
|
|
+ ?: defaultType.takeUnless { it == "text/plain" }
|
|
+ ?: "image/*"
|
|
return SharePayload().apply {
|
|
value = uri.toString()
|
|
shareType = ShareType.fromMimeType(specificType)
|
|
diff --git a/src/useIncomingShare.ts b/src/useIncomingShare.ts
|
|
--- a/src/useIncomingShare.ts
|
|
+++ b/src/useIncomingShare.ts
|
|
@@ -2,7 +2,7 @@
|
|
import type { AppStateStatus } from 'react-native';
|
|
import { AppState } from 'react-native';
|
|
|
|
-import { clearSharedPayloads, getResolvedSharedPayloadsAsync, getSharedPayloads } from './Sharing';
|
|
+import { clearSharedPayloads as clearNativeSharedPayloads, getResolvedSharedPayloadsAsync, getSharedPayloads } from './Sharing';
|
|
import type { ResolvedSharePayload, SharePayload, UseIncomingShareResult } from './Sharing.types';
|
|
|
|
function sharePayloadsAreEqual(a: SharePayload[], b: SharePayload[]): boolean {
|
|
@@ -42,6 +42,17 @@
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const currentSharedDataRef = useRef<SharePayload[]>([]);
|
|
|
|
+ const resolutionVersionRef = useRef(0);
|
|
+ const clearSharedPayloads = useCallback(() => {
|
|
+ clearNativeSharedPayloads();
|
|
+ resolutionVersionRef.current += 1;
|
|
+ currentSharedDataRef.current = [];
|
|
+ setSharedPayloads([]);
|
|
+ setResolvedSharedPayloads([]);
|
|
+ setError(null);
|
|
+ setIsResolving(false);
|
|
+ }, []);
|
|
+
|
|
const refreshSharePayloads = useCallback(async () => {
|
|
try {
|
|
const newSharedData: SharePayload[] = getSharedPayloads();
|
|
@@ -51,22 +62,28 @@
|
|
return;
|
|
}
|
|
|
|
+ const version = ++resolutionVersionRef.current;
|
|
currentSharedDataRef.current = newSharedData;
|
|
setSharedPayloads(newSharedData);
|
|
setResolvedSharedPayloads([]);
|
|
setError(null);
|
|
|
|
+ setIsResolving(newSharedData.length > 0);
|
|
if (newSharedData.length > 0) {
|
|
- setIsResolving(true);
|
|
try {
|
|
const resolved = await getResolvedSharedPayloadsAsync();
|
|
+ if (version !== resolutionVersionRef.current) return;
|
|
+ if (resolved.length === 0) {
|
|
+ throw new Error('Shared content could not be resolved');
|
|
+ }
|
|
setResolvedSharedPayloads(resolved);
|
|
} catch (e) {
|
|
+ if (version !== resolutionVersionRef.current) return;
|
|
setError(
|
|
e instanceof Error ? e : new Error('Unknown error during shared payload resolution')
|
|
);
|
|
} finally {
|
|
- setIsResolving(false);
|
|
+ if (version === resolutionVersionRef.current) setIsResolving(false);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
diff --git a/android/src/main/java/expo/modules/sharing/SharingModule.kt b/android/src/main/java/expo/modules/sharing/SharingModule.kt
|
|
--- a/android/src/main/java/expo/modules/sharing/SharingModule.kt
|
|
+++ b/android/src/main/java/expo/modules/sharing/SharingModule.kt
|
|
@@ -75,6 +75,17 @@
|
|
|
|
Function("clearSharedPayloads") {
|
|
SharingSingleton.intent = null
|
|
+ // ReactActivity retains the last SEND intent across task/activity recreation.
|
|
+ // Clearing only the singleton lets a consumed share be imported again.
|
|
+ appContext.currentActivity?.let { activity ->
|
|
+ val intent = activity.intent
|
|
+ if (intent != null && (isShareIntent(intent) || intent.data?.host == "expo-sharing")) {
|
|
+ activity.intent = Intent(Intent.ACTION_MAIN).apply {
|
|
+ component = intent.component
|
|
+ addCategory(Intent.CATEGORY_LAUNCHER)
|
|
+ }
|
|
+ }
|
|
+ }
|
|
}
|
|
|
|
OnActivityResult { _, (requestCode) ->
|