From f2754c74ae0dcb8e5a0aeb0c02afd5ae083ed84b Mon Sep 17 00:00:00 2001 From: Nabi Date: Tue, 15 Sep 2026 17:27:02 +0430 Subject: [PATCH 01/19] Fix cold-start file sharing to open Submit expense --- .../java/com/expensify/chat/MainActivity.kt | 16 ++++- .../chat/ShareActionHandlerModule.kt | 29 ++++++--- .../intentHandler/AbstractIntentHandler.kt | 2 +- .../chat/intentHandler/FileIntentHandler.kt | 40 +++++++----- .../chat/intentHandler/IntentHandler.kt | 2 +- .../chat/intentHandler/TextIntentHandler.kt | 59 ++++++++++------- .../com/expensify/chat/utils/FileUtils.kt | 63 +++++++++++++------ src/libs/ShareActionHandlerModule/index.ts | 2 +- src/pages/Share/ShareRootPage.tsx | 31 ++++++++- 9 files changed, 166 insertions(+), 78 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/MainActivity.kt b/android/app/src/main/java/com/expensify/chat/MainActivity.kt index c1fc3c15b1e4..ea7fe0cd8f5b 100644 --- a/android/app/src/main/java/com/expensify/chat/MainActivity.kt +++ b/android/app/src/main/java/com/expensify/chat/MainActivity.kt @@ -2,6 +2,7 @@ package com.expensify.chat import android.content.Intent import android.content.pm.ActivityInfo +import android.net.Uri import android.os.Bundle import android.util.Log import android.view.KeyEvent @@ -40,6 +41,14 @@ class MainActivity : ReactActivity() { )) override fun onCreate(savedInstanceState: Bundle?) { + val initialIntent = intent + val isColdShareIntent = initialIntent?.action == Intent.ACTION_SEND + if (isColdShareIntent && initialIntent != null && handleIntent(initialIntent, shouldLaunchActivity = false)) { + setIntent(Intent(Intent.ACTION_VIEW, Uri.parse("new-expensify://share/root")).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + }) + } + getSharedPreferences(APP_START_TIME_PREFERENCES, MODE_PRIVATE) .edit() .putLong(APP_START_TIME_PREFERENCES, System.currentTimeMillis()) @@ -60,7 +69,7 @@ class MainActivity : ReactActivity() { ) } - if (intent != null) { + if (!isColdShareIntent && intent != null) { handleIntent(intent) } } @@ -78,12 +87,13 @@ class MainActivity : ReactActivity() { handleIntent(intent) } - private fun handleIntent(intent: Intent) { + private fun handleIntent(intent: Intent, shouldLaunchActivity: Boolean = true): Boolean { try { val intenthandler = IntentHandlerFactory.getIntentHandler(this, intent.type, intent.toString()) - intenthandler?.handle(intent) + return intenthandler?.handle(intent, shouldLaunchActivity) ?: false } catch (exception: Exception) { Log.e("handleIntentException", exception.toString()) + return false } } diff --git a/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt b/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt index 14258ea744f9..6ec9de3102e7 100644 --- a/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt +++ b/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt @@ -29,24 +29,32 @@ class ShareActionHandlerModule(reactContext: ReactApplicationContext) : val shareObjectString = sharedPreferences.getString(IntentHandlerConstants.shareObjectProperty, null) if (shareObjectString == null) { - callback.invoke("No data found", null) + callback.invoke(null) return } val shareObject = JSONObject(shareObjectString) val content = shareObject.optString("content") - val mimeType = shareObject.optString("mimeType") + var mimeType = shareObject.optString("mimeType", "") val timestamp = System.currentTimeMillis() val file = File(content) - if (!file.exists()) { - val textObject = JSONObject().apply { - put("id", "text") - put("content", content) - put("mimeType", "txt") - put("processedAt", timestamp) + if (file.exists() && (mimeType.isEmpty() || mimeType.endsWith("/*") || mimeType == "null")) { + val extension = file.extension.lowercase() + mimeType = android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: mimeType + } + if (!file.exists() || file.length() == 0L) { + if (mimeType.startsWith("text/") || mimeType == "txt") { + val textObject = JSONObject().apply { + put("id", "text") + put("content", content) + put("mimeType", "txt") + put("processedAt", timestamp) + } + callback.invoke(textObject.toString()) + return } - callback.invoke(textObject.toString()) + callback.invoke(null) return } @@ -84,7 +92,8 @@ class ShareActionHandlerModule(reactContext: ReactApplicationContext) : callback.invoke(fileData.toString()) } catch (e: Exception) { - callback.invoke(e.toString(), null) + Log.e("ShareActionHandlerModule", "Error processing shared files: ${e.message}", e) + callback.invoke(null) } } } diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt index a5f554b53a18..3412c42ee8ea 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt @@ -11,7 +11,7 @@ abstract class AbstractIntentHandler: IntentHandler { val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) val editor = sharedPreferences.edit() editor.clear() - editor.apply() + editor.commit() // Clear leftover temporary files from previous share attempts clearInternalStorageDirectory(context) diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt index 0d52c66b1248..42ff20abf582 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt @@ -6,31 +6,41 @@ import android.net.Uri import com.expensify.chat.utils.FileUtils class FileIntentHandler(private val context: Context) : AbstractIntentHandler() { - override fun handle(intent: Intent): Boolean { - super.clearTemporaryFiles(context) + override fun handle(intent: Intent, shouldLaunchActivity: Boolean): Boolean { when(intent.action) { Intent.ACTION_SEND -> { - handleSingleFileIntent(intent, context) - onCompleted() + super.clearTemporaryFiles(context) + if (!handleSingleFileIntent(intent, context, shouldLaunchActivity)) { + return false + } + if (shouldLaunchActivity) { + onCompleted() + } return true } } return false } - private fun handleSingleFileIntent(intent: Intent, context: Context) { - (intent.getParcelableExtra(Intent.EXTRA_STREAM))?.let { fileUri -> - val resultingPath: String? = FileUtils.copyUriToStorage(fileUri, context) + private fun handleSingleFileIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { + val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return false + val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return false + val mimeType = try { + context.contentResolver.getType(fileUri) + } catch (exception: Exception) { + null + } ?: intent.type + val shareFileObject = ShareFileObject(resultingPath, mimeType) - if (resultingPath != null) { - val shareFileObject = ShareFileObject(resultingPath, intent.type) - - val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) - val editor = sharedPreferences.edit() - editor.putString(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString()) - editor.apply() - } + val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) + val editor = sharedPreferences.edit() + editor.putString(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString()) + if (shouldLaunchActivity) { + editor.apply() + return true } + + return editor.commit() } override fun onCompleted() { diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/IntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/IntentHandler.kt index d698096836b0..c4bc4d93a8a8 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/IntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/IntentHandler.kt @@ -7,6 +7,6 @@ object IntentHandlerConstants { const val shareObjectProperty = "shareObject" } interface IntentHandler { - fun handle(intent: Intent): Boolean + fun handle(intent: Intent, shouldLaunchActivity: Boolean = true): Boolean fun onCompleted() } diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt index 35aa8ab21719..1cf2ae594713 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt @@ -7,64 +7,75 @@ import com.expensify.chat.utils.FileUtils class TextIntentHandler(private val context: Context) : AbstractIntentHandler() { - override fun handle(intent: Intent): Boolean { + override fun handle(intent: Intent, shouldLaunchActivity: Boolean): Boolean { super.clearTemporaryFiles(context) when(intent.action) { Intent.ACTION_SEND -> { - handleTextIntent(intent, context) - onCompleted() + super.clearTemporaryFiles(context) + if (!handleTextIntent(intent, context, shouldLaunchActivity)) { + return false + } + if (shouldLaunchActivity) { + onCompleted() + } return true } } return false } - private fun handleTextIntent(intent: Intent, context: Context) { + private fun handleTextIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { when { intent.type == "text/plain" -> { val extras = intent.extras if (extras != null) { - when { + return when { extras.containsKey(Intent.EXTRA_STREAM) -> { - handleTextFileIntent(intent, context) + handleTextFileIntent(intent, context, shouldLaunchActivity) } extras.containsKey(Intent.EXTRA_TEXT) -> { - handleTextPlainIntent(intent, context) + handleTextPlainIntent(intent, context, shouldLaunchActivity) } else -> { throw UnsupportedOperationException("Unknown text/plain content") } } } + return false } - Regex("text/.*").matches(intent.type ?: "") -> handleTextFileIntent(intent, context) + Regex("text/.*").matches(intent.type ?: "") -> return handleTextFileIntent(intent, context, shouldLaunchActivity) else -> throw UnsupportedOperationException("Unsupported MIME type: ${intent.type}") } } - private fun saveToSharedPreferences(key: String, value: String) { + private fun saveToSharedPreferences(key: String, value: String, shouldLaunchActivity: Boolean): Boolean { val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) val editor = sharedPreferences.edit() editor.putString(key, value) - editor.apply() + if (shouldLaunchActivity) { + editor.apply() + return true + } + + return editor.commit() } - private fun handleTextFileIntent(intent: Intent, context: Context) { - (intent.getParcelableExtra(Intent.EXTRA_STREAM))?.let { fileUri -> - val resultingPath: String? = FileUtils.copyUriToStorage(fileUri, context) - if (resultingPath != null) { - val shareFileObject = ShareFileObject(resultingPath, intent.type) - saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString()) - } - } + private fun handleTextFileIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { + val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return false + val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return false + val mimeType = try { + context.contentResolver.getType(fileUri) + } catch (exception: Exception) { + null + } ?: intent.type + val shareFileObject = ShareFileObject(resultingPath, mimeType) + return saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString(), shouldLaunchActivity) } - private fun handleTextPlainIntent(intent: Intent, context: Context) { - var intentTextContent = intent.getStringExtra(Intent.EXTRA_TEXT) - if(intentTextContent != null) { - val shareFileObject = ShareFileObject(intentTextContent, intent.type) - saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString()) - } + private fun handleTextPlainIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { + val intentTextContent = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return false + val shareFileObject = ShareFileObject(intentTextContent, intent.type) + return saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString(), shouldLaunchActivity) } override fun onCompleted() { diff --git a/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt b/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt index a0f13ddad645..1d2578bf304b 100644 --- a/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt +++ b/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt @@ -13,26 +13,26 @@ import java.io.OutputStream object FileUtils { private const val tag = "FileUtils" - private const val directoryName = "Expensify" + private const val shareDirectoryName = "sharedFiles" - private fun getInternalStorageDirectory(context: Context): File { - val internalStorageDirectory = File(context.filesDir.absolutePath, directoryName) - if (!internalStorageDirectory.exists()) { - internalStorageDirectory.mkdirs() + private fun getShareStorageDirectory(context: Context): File { + val shareStorageDirectory = File(context.filesDir.absolutePath, shareDirectoryName) + if (!shareStorageDirectory.exists()) { + shareStorageDirectory.mkdirs() } - return internalStorageDirectory + return shareStorageDirectory } fun clearInternalStorageDirectory(context: Context) { - val internalStorageDirectory = getInternalStorageDirectory(context) - if (internalStorageDirectory.exists()) { - val files = internalStorageDirectory.listFiles() + val shareStorageDirectory = getShareStorageDirectory(context) + if (shareStorageDirectory.exists()) { + val files = shareStorageDirectory.listFiles() if (files != null && files.isNotEmpty()) { for (file in files) { file.delete() } } else { - Log.i(tag, "No files found to delete in directory: ${internalStorageDirectory.absolutePath}") + Log.i(tag, "No files found to delete in directory: ${shareStorageDirectory.absolutePath}") } } } @@ -83,7 +83,7 @@ object FileUtils { val file: File = File.createTempFile( getUniqueFilePrefix(), fileExtension, - getInternalStorageDirectory(context) + getShareStorageDirectory(context) ) Log.i(tag, "Created a temporary file at" + file.absolutePath) @@ -98,28 +98,51 @@ object FileUtils { * @return The absolute path of the image */ fun copyUriToStorage(fileUri: Uri, context: Context): String? { - val fileName = getFileName(context, fileUri) ?: return null - val destinationFile = File(getInternalStorageDirectory(context), fileName) + val fileName = getFileName(context, fileUri) ?: generateFileName(context, fileUri) + val destinationFile = File(getShareStorageDirectory(context), fileName) return try { saveFileFromProviderUri(fileUri, destinationFile, context) + if (!destinationFile.exists() || destinationFile.length() == 0L) { + destinationFile.delete() + return null + } destinationFile.absolutePath - } catch (ex: IOException) { + } catch (ex: Exception) { Log.e(tag, "Couldn't save file from intent", ex) null } } + private fun generateFileName(context: Context, uri: Uri): String { + val mimeTypeMap = MimeTypeMap.getSingleton() + val mimeType = try { + context.contentResolver.getType(uri) + } catch (e: Exception) { + null + } + val extension = if (mimeType != null) { + mimeTypeMap.getExtensionFromMimeType(mimeType) + } else { + null + } ?: "bin" + return "${getUniqueFilePrefix()}.$extension" + } + private fun getFileName(context: Context, uri: Uri): String? { var name: String? = null - val cursor = context.contentResolver.query(uri, null, null, null, null) - cursor?.use { - if (it.moveToFirst()) { - val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) - if (nameIndex != -1) { - name = it.getString(nameIndex) + try { + val cursor = context.contentResolver.query(uri, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (nameIndex != -1) { + name = it.getString(nameIndex) + } } } + } catch (e: Exception) { + Log.w(tag, "Failed to query filename from ContentResolver", e) } return name } diff --git a/src/libs/ShareActionHandlerModule/index.ts b/src/libs/ShareActionHandlerModule/index.ts index 205bd89960d9..6c846e64235a 100644 --- a/src/libs/ShareActionHandlerModule/index.ts +++ b/src/libs/ShareActionHandlerModule/index.ts @@ -14,7 +14,7 @@ type ShareActionContent = { // Type for the ShareActionHandler module type ShareActionHandlerModule = { // Method to process files, which takes a callback function - processFiles(callback: (array: ShareActionContent[]) => void): void; + processFiles(callback: (files: ShareActionContent[] | string | ShareActionContent | null | undefined) => void): void; }; export default ShareActionHandler; diff --git a/src/pages/Share/ShareRootPage.tsx b/src/pages/Share/ShareRootPage.tsx index f8c53c173f6c..4d1ac291735b 100644 --- a/src/pages/Share/ShareRootPage.tsx +++ b/src/pages/Share/ShareRootPage.tsx @@ -99,17 +99,42 @@ function ShareRootPage() { }, [errorTitle, errorMessage]); const handleProcessFiles = useCallback(() => { - ShareActionHandler.processFiles((processedFiles) => { - const tempFile = Array.isArray(processedFiles) ? processedFiles.at(0) : (JSON.parse(processedFiles) as ShareTempFile); + ShareActionHandler.processFiles((processedFiles: unknown) => { + let tempFile: ShareTempFile | undefined; + if (Array.isArray(processedFiles)) { + tempFile = processedFiles.at(0) as ShareTempFile | undefined; + } else if (typeof processedFiles === 'object' && processedFiles !== null) { + tempFile = processedFiles as ShareTempFile; + } else if (typeof processedFiles === 'string' && processedFiles.trim().length > 0) { + try { + tempFile = JSON.parse(processedFiles) as ShareTempFile; + } catch (error) { + Log.warn('[ShareRootPage] Failed to parse processedFiles', {error, processedFiles}); + } + } if (errorTitle) { return; } - if (!tempFile?.mimeType || !shareFileMimeTypes.includes(tempFile?.mimeType)) { + if (!tempFile) { + setErrorTitle(translate('attachmentPicker.attachmentError')); + setErrorMessage(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); + return; + } + + const rawMimeType = tempFile.mimeType?.split(';')[0]?.trim()?.toLowerCase() ?? ''; + const isValidMimeType = + Boolean(rawMimeType) && + (shareFileMimeTypes.includes(rawMimeType) || shareFileMimeTypes.some((allowed) => allowed.endsWith('/*') && rawMimeType.startsWith(allowed.replace('/*', '')))); + + if (!isValidMimeType) { setErrorTitle(translate('attachmentPicker.wrongFileType')); setErrorMessage(translate('attachmentPicker.notAllowedExtension')); + setIsFileReady(true); return; } + tempFile.mimeType = rawMimeType; + const isImage = /image\/.*/.test(tempFile?.mimeType); if (tempFile?.mimeType && tempFile?.mimeType !== 'txt' && !isImage) { getFileSize(tempFile?.content).then((size) => { From 5d73a7f22f477d615e321ec3660f04de7afadafa Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Wed, 16 Sep 2026 09:35:38 +0430 Subject: [PATCH 02/19] Fix duplicate identifier `shouldUseNarrowLayout` in InboxTabSelector --- src/pages/inbox/sidebar/InboxTabSelector.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/inbox/sidebar/InboxTabSelector.tsx b/src/pages/inbox/sidebar/InboxTabSelector.tsx index 65aaa00fc596..d0bab5d8f7d1 100644 --- a/src/pages/inbox/sidebar/InboxTabSelector.tsx +++ b/src/pages/inbox/sidebar/InboxTabSelector.tsx @@ -44,7 +44,6 @@ function InboxTabSelector() { const {setActiveTab, getReportIDsForTab} = useSidebarOrderedReportsActions(); const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {selector: reportNameValuePairsArchivedSelector}); const icons = useMemoizedLazyExpensifyIcons(['Checkmark', 'Feed', 'ChatBubbleUnread', 'Task']); - const {shouldUseNarrowLayout} = useResponsiveLayout(); const {showConfirmModal} = useConfirmModal(); // Only show the tooltip if we have unread message > 3 months old. const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( From 1ba8767d7c79db56553d2247180916465a4e354d Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Wed, 16 Sep 2026 14:15:05 +0430 Subject: [PATCH 03/19] Fix unsafe type assertions and lint errors in ShareRootPage --- src/pages/Share/ShareRootPage.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/pages/Share/ShareRootPage.tsx b/src/pages/Share/ShareRootPage.tsx index 4d1ac291735b..d1c82cf79b1d 100644 --- a/src/pages/Share/ShareRootPage.tsx +++ b/src/pages/Share/ShareRootPage.tsx @@ -44,6 +44,10 @@ function showErrorAlert(title: string, message: string) { Navigation.navigate(ROUTES.INBOX); } +function isShareTempFile(file: unknown): file is ShareTempFile { + return typeof file === 'object' && file !== null && 'id' in file && typeof file.id === 'string' && 'content' in file && typeof file.content === 'string'; +} + function ShareRootPage() { const [currentAttachment] = useOnyx(ONYXKEYS.SHARE_TEMP_FILE); @@ -102,12 +106,18 @@ function ShareRootPage() { ShareActionHandler.processFiles((processedFiles: unknown) => { let tempFile: ShareTempFile | undefined; if (Array.isArray(processedFiles)) { - tempFile = processedFiles.at(0) as ShareTempFile | undefined; - } else if (typeof processedFiles === 'object' && processedFiles !== null) { - tempFile = processedFiles as ShareTempFile; + const first: unknown = (processedFiles as unknown[]).at(0); + if (isShareTempFile(first)) { + tempFile = first; + } + } else if (isShareTempFile(processedFiles)) { + tempFile = processedFiles; } else if (typeof processedFiles === 'string' && processedFiles.trim().length > 0) { try { - tempFile = JSON.parse(processedFiles) as ShareTempFile; + const parsed: unknown = JSON.parse(processedFiles); + if (isShareTempFile(parsed)) { + tempFile = parsed; + } } catch (error) { Log.warn('[ShareRootPage] Failed to parse processedFiles', {error, processedFiles}); } @@ -123,7 +133,7 @@ function ShareRootPage() { const rawMimeType = tempFile.mimeType?.split(';')[0]?.trim()?.toLowerCase() ?? ''; const isValidMimeType = - Boolean(rawMimeType) && + !!rawMimeType && (shareFileMimeTypes.includes(rawMimeType) || shareFileMimeTypes.some((allowed) => allowed.endsWith('/*') && rawMimeType.startsWith(allowed.replace('/*', '')))); if (!isValidMimeType) { From fc54b2960167e922f6b7eba65d751bb30d1d29eb Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Thu, 17 Sep 2026 08:39:21 +0430 Subject: [PATCH 04/19] Move errorTitle early return to top of processFiles callback --- src/pages/Share/ShareRootPage.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/Share/ShareRootPage.tsx b/src/pages/Share/ShareRootPage.tsx index d1c82cf79b1d..9b7e0998bb29 100644 --- a/src/pages/Share/ShareRootPage.tsx +++ b/src/pages/Share/ShareRootPage.tsx @@ -104,6 +104,10 @@ function ShareRootPage() { const handleProcessFiles = useCallback(() => { ShareActionHandler.processFiles((processedFiles: unknown) => { + if (errorTitle) { + return; + } + let tempFile: ShareTempFile | undefined; if (Array.isArray(processedFiles)) { const first: unknown = (processedFiles as unknown[]).at(0); @@ -122,9 +126,6 @@ function ShareRootPage() { Log.warn('[ShareRootPage] Failed to parse processedFiles', {error, processedFiles}); } } - if (errorTitle) { - return; - } if (!tempFile) { setErrorTitle(translate('attachmentPicker.attachmentError')); setErrorMessage(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); From ad46ce4b8eb0c5d817834153bc4cc129fb91a804 Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Tue, 29 Sep 2026 14:17:12 +0430 Subject: [PATCH 05/19] Restore temporary file cleanup for all file intents --- .../java/com/expensify/chat/intentHandler/FileIntentHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt index 42ff20abf582..9486399b8e09 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt @@ -7,9 +7,9 @@ import com.expensify.chat.utils.FileUtils class FileIntentHandler(private val context: Context) : AbstractIntentHandler() { override fun handle(intent: Intent, shouldLaunchActivity: Boolean): Boolean { + super.clearTemporaryFiles(context) when(intent.action) { Intent.ACTION_SEND -> { - super.clearTemporaryFiles(context) if (!handleSingleFileIntent(intent, context, shouldLaunchActivity)) { return false } From 1b7804afe7d04012aa146b0e55cf1732af80da88 Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Tue, 29 Sep 2026 15:06:26 +0430 Subject: [PATCH 06/19] Address Android share flow review feedback --- .../chat/ShareActionHandlerModule.kt | 6 ++++- .../chat/intentHandler/TextIntentHandler.kt | 1 - .../com/expensify/chat/utils/FileUtils.kt | 24 ++++++++++++------- src/pages/Share/ShareRootPage.tsx | 4 +--- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt b/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt index 6ec9de3102e7..23cae6eaa61e 100644 --- a/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt +++ b/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt @@ -43,7 +43,7 @@ class ShareActionHandlerModule(reactContext: ReactApplicationContext) : val extension = file.extension.lowercase() mimeType = android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: mimeType } - if (!file.exists() || file.length() == 0L) { + if (!file.exists()) { if (mimeType.startsWith("text/") || mimeType == "txt") { val textObject = JSONObject().apply { put("id", "text") @@ -57,6 +57,10 @@ class ShareActionHandlerModule(reactContext: ReactApplicationContext) : callback.invoke(null) return } + if (file.length() == 0L) { + callback.invoke(null) + return + } val identifier = file.name var aspectRatio = 0.0f diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt index 1cf2ae594713..a7a1abbaee23 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt @@ -11,7 +11,6 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() super.clearTemporaryFiles(context) when(intent.action) { Intent.ACTION_SEND -> { - super.clearTemporaryFiles(context) if (!handleTextIntent(intent, context, shouldLaunchActivity)) { return false } diff --git a/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt b/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt index 1d2578bf304b..0c22421d9267 100644 --- a/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt +++ b/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt @@ -14,6 +14,7 @@ import java.io.OutputStream object FileUtils { private const val tag = "FileUtils" private const val shareDirectoryName = "sharedFiles" + private const val legacyShareDirectoryName = "Expensify" private fun getShareStorageDirectory(context: Context): File { val shareStorageDirectory = File(context.filesDir.absolutePath, shareDirectoryName) @@ -25,15 +26,22 @@ object FileUtils { fun clearInternalStorageDirectory(context: Context) { val shareStorageDirectory = getShareStorageDirectory(context) - if (shareStorageDirectory.exists()) { - val files = shareStorageDirectory.listFiles() - if (files != null && files.isNotEmpty()) { - for (file in files) { - file.delete() - } - } else { - Log.i(tag, "No files found to delete in directory: ${shareStorageDirectory.absolutePath}") + clearStorageDirectory(shareStorageDirectory) + clearStorageDirectory(File(context.filesDir.absolutePath, legacyShareDirectoryName)) + } + + private fun clearStorageDirectory(storageDirectory: File) { + if (!storageDirectory.exists()) { + return + } + + val files = storageDirectory.listFiles() + if (files != null && files.isNotEmpty()) { + for (file in files) { + file.delete() } + } else { + Log.i(tag, "No files found to delete in directory: ${storageDirectory.absolutePath}") } } diff --git a/src/pages/Share/ShareRootPage.tsx b/src/pages/Share/ShareRootPage.tsx index 9b7e0998bb29..25bb42a1aaf8 100644 --- a/src/pages/Share/ShareRootPage.tsx +++ b/src/pages/Share/ShareRootPage.tsx @@ -134,13 +134,11 @@ function ShareRootPage() { const rawMimeType = tempFile.mimeType?.split(';')[0]?.trim()?.toLowerCase() ?? ''; const isValidMimeType = - !!rawMimeType && - (shareFileMimeTypes.includes(rawMimeType) || shareFileMimeTypes.some((allowed) => allowed.endsWith('/*') && rawMimeType.startsWith(allowed.replace('/*', '')))); + !!rawMimeType && (shareFileMimeTypes.includes(rawMimeType) || shareFileMimeTypes.some((allowed) => allowed.endsWith('/*') && rawMimeType.startsWith(allowed.slice(0, -1)))); if (!isValidMimeType) { setErrorTitle(translate('attachmentPicker.wrongFileType')); setErrorMessage(translate('attachmentPicker.notAllowedExtension')); - setIsFileReady(true); return; } From 1efe97755a5948d2b7ce7703eced9b53f0c5f1a0 Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Fri, 2 Oct 2026 16:23:44 +0430 Subject: [PATCH 07/19] Show an error when shared attachments cannot be saved --- .../chat/intentHandler/FileIntentHandler.kt | 15 +++++++-------- .../chat/intentHandler/TextIntentHandler.kt | 5 ++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt index 9486399b8e09..d49d8a63d28c 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt @@ -10,9 +10,8 @@ class FileIntentHandler(private val context: Context) : AbstractIntentHandler() super.clearTemporaryFiles(context) when(intent.action) { Intent.ACTION_SEND -> { - if (!handleSingleFileIntent(intent, context, shouldLaunchActivity)) { - return false - } + handleSingleFileIntent(intent, context, shouldLaunchActivity) + // Open the Share screen even if persisting the attachment fails so it can display an error instead of silently discarding the share. if (shouldLaunchActivity) { onCompleted() } @@ -22,9 +21,9 @@ class FileIntentHandler(private val context: Context) : AbstractIntentHandler() return false } - private fun handleSingleFileIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { - val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return false - val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return false + private fun handleSingleFileIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean) { + val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return + val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return val mimeType = try { context.contentResolver.getType(fileUri) } catch (exception: Exception) { @@ -37,10 +36,10 @@ class FileIntentHandler(private val context: Context) : AbstractIntentHandler() editor.putString(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString()) if (shouldLaunchActivity) { editor.apply() - return true + return } - return editor.commit() + editor.commit() } override fun onCompleted() { diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt index a7a1abbaee23..14df679f80e1 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt @@ -11,9 +11,8 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() super.clearTemporaryFiles(context) when(intent.action) { Intent.ACTION_SEND -> { - if (!handleTextIntent(intent, context, shouldLaunchActivity)) { - return false - } + handleTextIntent(intent, context, shouldLaunchActivity) + // Open the Share screen even if persisting the attachment fails so it can display an error instead of silently discarding the share. if (shouldLaunchActivity) { onCompleted() } From eb895080925d6823b0dcb3d2672bca877099fab0 Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Fri, 2 Oct 2026 16:31:46 +0430 Subject: [PATCH 08/19] Prefer intent MIME type for shared attachments --- .../com/expensify/chat/intentHandler/FileIntentHandler.kt | 4 ++-- .../com/expensify/chat/intentHandler/TextIntentHandler.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt index d49d8a63d28c..8481ac2db166 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt @@ -24,11 +24,11 @@ class FileIntentHandler(private val context: Context) : AbstractIntentHandler() private fun handleSingleFileIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean) { val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return - val mimeType = try { + val mimeType = intent.type ?: try { context.contentResolver.getType(fileUri) } catch (exception: Exception) { null - } ?: intent.type + } val shareFileObject = ShareFileObject(resultingPath, mimeType) val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt index 14df679f80e1..35c30da387b9 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt @@ -61,11 +61,11 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() private fun handleTextFileIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return false val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return false - val mimeType = try { + val mimeType = intent.type ?: try { context.contentResolver.getType(fileUri) } catch (exception: Exception) { null - } ?: intent.type + } val shareFileObject = ShareFileObject(resultingPath, mimeType) return saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString(), shouldLaunchActivity) } From 4a54e404912786deb668e5146948c77ceb51235f Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Fri, 2 Oct 2026 16:37:00 +0430 Subject: [PATCH 09/19] Avoid blocking preference cleanup during share startup --- .../com/expensify/chat/intentHandler/AbstractIntentHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt index 3412c42ee8ea..a5f554b53a18 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt @@ -11,7 +11,7 @@ abstract class AbstractIntentHandler: IntentHandler { val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) val editor = sharedPreferences.edit() editor.clear() - editor.commit() + editor.apply() // Clear leftover temporary files from previous share attempts clearInternalStorageDirectory(context) From 8c6a18121f4b0734822ba879536330cb6d4bab2b Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Fri, 2 Oct 2026 17:05:25 +0430 Subject: [PATCH 10/19] Add tests for shared file response handling --- src/libs/ShareActionHandlerModule/utils.ts | 33 +++++++++++++++++++ src/pages/Share/ShareRootPage.tsx | 26 ++------------- .../unit/ShareActionHandlerModuleUtilsTest.ts | 25 ++++++++++++++ 3 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 src/libs/ShareActionHandlerModule/utils.ts create mode 100644 tests/unit/ShareActionHandlerModuleUtilsTest.ts diff --git a/src/libs/ShareActionHandlerModule/utils.ts b/src/libs/ShareActionHandlerModule/utils.ts new file mode 100644 index 000000000000..b00e38523738 --- /dev/null +++ b/src/libs/ShareActionHandlerModule/utils.ts @@ -0,0 +1,33 @@ +import type {ShareTempFile} from '@src/types/onyx'; + +function isShareTempFile(file: unknown): file is ShareTempFile { + return typeof file === 'object' && file !== null && 'id' in file && typeof file.id === 'string' && 'content' in file && typeof file.content === 'string'; +} + +function getShareTempFile(processedFiles: unknown): ShareTempFile | undefined { + if (Array.isArray(processedFiles)) { + const first: unknown = processedFiles.at(0); + return isShareTempFile(first) ? first : undefined; + } + + if (isShareTempFile(processedFiles)) { + return processedFiles; + } + + if (typeof processedFiles !== 'string' || processedFiles.trim().length === 0) { + return undefined; + } + + try { + const parsed: unknown = JSON.parse(processedFiles); + return isShareTempFile(parsed) ? parsed : undefined; + } catch { + return undefined; + } +} + +function normalizeShareFileMimeType(mimeType: string | undefined): string { + return mimeType?.split(';')[0]?.trim()?.toLowerCase() ?? ''; +} + +export {getShareTempFile, normalizeShareFileMimeType}; diff --git a/src/pages/Share/ShareRootPage.tsx b/src/pages/Share/ShareRootPage.tsx index 25bb42a1aaf8..637025e41dc9 100644 --- a/src/pages/Share/ShareRootPage.tsx +++ b/src/pages/Share/ShareRootPage.tsx @@ -16,6 +16,7 @@ import Navigation from '@libs/Navigation/Navigation'; import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; import {shouldValidateFile} from '@libs/ReceiptUtils'; import ShareActionHandler from '@libs/ShareActionHandlerModule'; +import {getShareTempFile, normalizeShareFileMimeType} from '@libs/ShareActionHandlerModule/utils'; import {close as closeModal} from '@userActions/Modal'; import Tab from '@userActions/Tab'; @@ -44,10 +45,6 @@ function showErrorAlert(title: string, message: string) { Navigation.navigate(ROUTES.INBOX); } -function isShareTempFile(file: unknown): file is ShareTempFile { - return typeof file === 'object' && file !== null && 'id' in file && typeof file.id === 'string' && 'content' in file && typeof file.content === 'string'; -} - function ShareRootPage() { const [currentAttachment] = useOnyx(ONYXKEYS.SHARE_TEMP_FILE); @@ -108,31 +105,14 @@ function ShareRootPage() { return; } - let tempFile: ShareTempFile | undefined; - if (Array.isArray(processedFiles)) { - const first: unknown = (processedFiles as unknown[]).at(0); - if (isShareTempFile(first)) { - tempFile = first; - } - } else if (isShareTempFile(processedFiles)) { - tempFile = processedFiles; - } else if (typeof processedFiles === 'string' && processedFiles.trim().length > 0) { - try { - const parsed: unknown = JSON.parse(processedFiles); - if (isShareTempFile(parsed)) { - tempFile = parsed; - } - } catch (error) { - Log.warn('[ShareRootPage] Failed to parse processedFiles', {error, processedFiles}); - } - } + const tempFile = getShareTempFile(processedFiles); if (!tempFile) { setErrorTitle(translate('attachmentPicker.attachmentError')); setErrorMessage(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); return; } - const rawMimeType = tempFile.mimeType?.split(';')[0]?.trim()?.toLowerCase() ?? ''; + const rawMimeType = normalizeShareFileMimeType(tempFile.mimeType); const isValidMimeType = !!rawMimeType && (shareFileMimeTypes.includes(rawMimeType) || shareFileMimeTypes.some((allowed) => allowed.endsWith('/*') && rawMimeType.startsWith(allowed.slice(0, -1)))); diff --git a/tests/unit/ShareActionHandlerModuleUtilsTest.ts b/tests/unit/ShareActionHandlerModuleUtilsTest.ts new file mode 100644 index 000000000000..a7cd036b57bd --- /dev/null +++ b/tests/unit/ShareActionHandlerModuleUtilsTest.ts @@ -0,0 +1,25 @@ +import {getShareTempFile, normalizeShareFileMimeType} from '@libs/ShareActionHandlerModule/utils'; + +describe('ShareActionHandlerModule utils', () => { + const sharedFile = {id: 'receipt.jpg', content: 'file:///receipt.jpg', mimeType: 'image/jpeg'}; + + it.each([ + ['an array response', [sharedFile]], + ['an object response', sharedFile], + ['a JSON string response', JSON.stringify(sharedFile)], + ])('returns the shared file from %s', (_description, response) => { + expect(getShareTempFile(response)).toEqual(sharedFile); + }); + + it.each([null, undefined, [], {}, '', ' ', '{invalid JSON}', JSON.stringify({id: 'receipt.jpg'})])('returns undefined for an invalid native response', (response) => { + expect(getShareTempFile(response)).toBeUndefined(); + }); + + it('normalizes a MIME type that contains parameters', () => { + expect(normalizeShareFileMimeType('IMAGE/JPEG; charset=utf-8')).toBe('image/jpeg'); + }); + + it('returns an empty MIME type when no MIME type is provided', () => { + expect(normalizeShareFileMimeType(undefined)).toBe(''); + }); +}); From 9fc9e9f98b50da9956ab503fa2dddf501de0fb6e Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Sat, 17 Oct 2026 16:54:48 +0430 Subject: [PATCH 11/19] Restore existing Android share temp directory --- .../com/expensify/chat/utils/FileUtils.kt | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt b/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt index 0c22421d9267..d6a0f22c5ea6 100644 --- a/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt +++ b/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt @@ -13,21 +13,18 @@ import java.io.OutputStream object FileUtils { private const val tag = "FileUtils" - private const val shareDirectoryName = "sharedFiles" - private const val legacyShareDirectoryName = "Expensify" + private const val directoryName = "Expensify" - private fun getShareStorageDirectory(context: Context): File { - val shareStorageDirectory = File(context.filesDir.absolutePath, shareDirectoryName) - if (!shareStorageDirectory.exists()) { - shareStorageDirectory.mkdirs() + private fun getInternalStorageDirectory(context: Context): File { + val internalStorageDirectory = File(context.filesDir.absolutePath, directoryName) + if (!internalStorageDirectory.exists()) { + internalStorageDirectory.mkdirs() } - return shareStorageDirectory + return internalStorageDirectory } fun clearInternalStorageDirectory(context: Context) { - val shareStorageDirectory = getShareStorageDirectory(context) - clearStorageDirectory(shareStorageDirectory) - clearStorageDirectory(File(context.filesDir.absolutePath, legacyShareDirectoryName)) + clearStorageDirectory(getInternalStorageDirectory(context)) } private fun clearStorageDirectory(storageDirectory: File) { @@ -91,7 +88,7 @@ object FileUtils { val file: File = File.createTempFile( getUniqueFilePrefix(), fileExtension, - getShareStorageDirectory(context) + getInternalStorageDirectory(context) ) Log.i(tag, "Created a temporary file at" + file.absolutePath) @@ -107,11 +104,15 @@ object FileUtils { */ fun copyUriToStorage(fileUri: Uri, context: Context): String? { val fileName = getFileName(context, fileUri) ?: generateFileName(context, fileUri) - val destinationFile = File(getShareStorageDirectory(context), fileName) + val destinationFile = File(getInternalStorageDirectory(context), fileName) return try { saveFileFromProviderUri(fileUri, destinationFile, context) - if (!destinationFile.exists() || destinationFile.length() == 0L) { + if (!destinationFile.exists()) { + destinationFile.delete() + return null + } + if (destinationFile.length() == 0L) { destinationFile.delete() return null } From 700258b9ac1b03a807ba19158f726d0d1f500c95 Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Tue, 20 Oct 2026 09:21:45 +0430 Subject: [PATCH 12/19] Rename Android share intent helpers --- .../chat/intentHandler/FileIntentHandler.kt | 4 ++-- .../chat/intentHandler/TextIntentHandler.kt | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt index 8481ac2db166..709b360a7c00 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt @@ -10,7 +10,7 @@ class FileIntentHandler(private val context: Context) : AbstractIntentHandler() super.clearTemporaryFiles(context) when(intent.action) { Intent.ACTION_SEND -> { - handleSingleFileIntent(intent, context, shouldLaunchActivity) + saveSharedFile(intent, context, shouldLaunchActivity) // Open the Share screen even if persisting the attachment fails so it can display an error instead of silently discarding the share. if (shouldLaunchActivity) { onCompleted() @@ -21,7 +21,7 @@ class FileIntentHandler(private val context: Context) : AbstractIntentHandler() return false } - private fun handleSingleFileIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean) { + private fun saveSharedFile(intent: Intent, context: Context, shouldLaunchActivity: Boolean) { val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return val mimeType = intent.type ?: try { diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt index 35c30da387b9..630c17b4b992 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt @@ -11,7 +11,7 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() super.clearTemporaryFiles(context) when(intent.action) { Intent.ACTION_SEND -> { - handleTextIntent(intent, context, shouldLaunchActivity) + saveSharedText(intent, context, shouldLaunchActivity) // Open the Share screen even if persisting the attachment fails so it can display an error instead of silently discarding the share. if (shouldLaunchActivity) { onCompleted() @@ -22,17 +22,17 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() return false } - private fun handleTextIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { + private fun saveSharedText(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { when { intent.type == "text/plain" -> { val extras = intent.extras if (extras != null) { return when { extras.containsKey(Intent.EXTRA_STREAM) -> { - handleTextFileIntent(intent, context, shouldLaunchActivity) + saveSharedTextFile(intent, context, shouldLaunchActivity) } extras.containsKey(Intent.EXTRA_TEXT) -> { - handleTextPlainIntent(intent, context, shouldLaunchActivity) + saveSharedTextContent(intent, context, shouldLaunchActivity) } else -> { throw UnsupportedOperationException("Unknown text/plain content") @@ -41,7 +41,7 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() } return false } - Regex("text/.*").matches(intent.type ?: "") -> return handleTextFileIntent(intent, context, shouldLaunchActivity) + Regex("text/.*").matches(intent.type ?: "") -> return saveSharedTextFile(intent, context, shouldLaunchActivity) else -> throw UnsupportedOperationException("Unsupported MIME type: ${intent.type}") } } @@ -58,7 +58,7 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() return editor.commit() } - private fun handleTextFileIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { + private fun saveSharedTextFile(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return false val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return false val mimeType = intent.type ?: try { @@ -70,7 +70,7 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() return saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString(), shouldLaunchActivity) } - private fun handleTextPlainIntent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { + private fun saveSharedTextContent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { val intentTextContent = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return false val shareFileObject = ShareFileObject(intentTextContent, intent.type) return saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString(), shouldLaunchActivity) From 502408f64d137732fb20317a64978c7a0f7aeadd Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Tue, 20 Oct 2026 09:31:57 +0430 Subject: [PATCH 13/19] Clarify Android share intent launch behavior --- .../java/com/expensify/chat/intentHandler/FileIntentHandler.kt | 3 ++- .../java/com/expensify/chat/intentHandler/TextIntentHandler.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt index 709b360a7c00..9e8dc3b7d22b 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt @@ -11,7 +11,8 @@ class FileIntentHandler(private val context: Context) : AbstractIntentHandler() when(intent.action) { Intent.ACTION_SEND -> { saveSharedFile(intent, context, shouldLaunchActivity) - // Open the Share screen even if persisting the attachment fails so it can display an error instead of silently discarding the share. + // Expected persistence failures return from saveSharedFile(), allowing the warm-start Share screen to display an attachment error. + // Cold starts use new-expensify://share/root as the activity's initial URL instead. if (shouldLaunchActivity) { onCompleted() } diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt index 630c17b4b992..a113ae56457a 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt @@ -12,7 +12,8 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() when(intent.action) { Intent.ACTION_SEND -> { saveSharedText(intent, context, shouldLaunchActivity) - // Open the Share screen even if persisting the attachment fails so it can display an error instead of silently discarding the share. + // Expected persistence failures return from saveSharedText(), allowing the warm-start Share screen to display an attachment error. + // Cold starts use new-expensify://share/root as the activity's initial URL instead. if (shouldLaunchActivity) { onCompleted() } From 05beba0397c1af1190e19cfe7e74c8876fd91c97 Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Tue, 20 Oct 2026 09:36:47 +0430 Subject: [PATCH 14/19] Simplify shared file extension fallback --- .../app/src/main/java/com/expensify/chat/utils/FileUtils.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt b/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt index d6a0f22c5ea6..56f2b4dc3724 100644 --- a/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt +++ b/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt @@ -130,11 +130,7 @@ object FileUtils { } catch (e: Exception) { null } - val extension = if (mimeType != null) { - mimeTypeMap.getExtensionFromMimeType(mimeType) - } else { - null - } ?: "bin" + val extension = mimeType?.let(mimeTypeMap::getExtensionFromMimeType) ?: "bin" return "${getUniqueFilePrefix()}.$extension" } From a913deede4e06018bb0e858159c3117622e3eaa5 Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Tue, 20 Oct 2026 09:43:50 +0430 Subject: [PATCH 15/19] Simplify cold share intent handling --- android/app/src/main/java/com/expensify/chat/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/expensify/chat/MainActivity.kt b/android/app/src/main/java/com/expensify/chat/MainActivity.kt index ea7fe0cd8f5b..0c9683b9c175 100644 --- a/android/app/src/main/java/com/expensify/chat/MainActivity.kt +++ b/android/app/src/main/java/com/expensify/chat/MainActivity.kt @@ -43,7 +43,7 @@ class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { val initialIntent = intent val isColdShareIntent = initialIntent?.action == Intent.ACTION_SEND - if (isColdShareIntent && initialIntent != null && handleIntent(initialIntent, shouldLaunchActivity = false)) { + if (isColdShareIntent && handleIntent(initialIntent, shouldLaunchActivity = false)) { setIntent(Intent(Intent.ACTION_VIEW, Uri.parse("new-expensify://share/root")).apply { addCategory(Intent.CATEGORY_BROWSABLE) }) From c00bb00c175556b335eae8c5bdf78fd04eba4abf Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Thu, 22 Oct 2026 10:02:38 +0430 Subject: [PATCH 16/19] Initialize splash before processing cold shares --- android/app/src/main/java/com/expensify/chat/MainActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/expensify/chat/MainActivity.kt b/android/app/src/main/java/com/expensify/chat/MainActivity.kt index 0c9683b9c175..5bf5bbe3bae2 100644 --- a/android/app/src/main/java/com/expensify/chat/MainActivity.kt +++ b/android/app/src/main/java/com/expensify/chat/MainActivity.kt @@ -41,6 +41,8 @@ class MainActivity : ReactActivity() { )) override fun onCreate(savedInstanceState: Bundle?) { + BootSplash.init(this) + val initialIntent = intent val isColdShareIntent = initialIntent?.action == Intent.ACTION_SEND if (isColdShareIntent && handleIntent(initialIntent, shouldLaunchActivity = false)) { @@ -53,7 +55,6 @@ class MainActivity : ReactActivity() { .edit() .putLong(APP_START_TIME_PREFERENCES, System.currentTimeMillis()) .apply() - BootSplash.init(this) super.onCreate(null) // Sets translucent status bar. This code is based on what the react-native StatusBar From d74d002de4e486e0cdcb124b9e7ca8cb75524cbc Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Thu, 22 Oct 2026 10:08:00 +0430 Subject: [PATCH 17/19] Narrow shared file callback type --- src/libs/ShareActionHandlerModule/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ShareActionHandlerModule/index.ts b/src/libs/ShareActionHandlerModule/index.ts index 6c846e64235a..cb461fa391fb 100644 --- a/src/libs/ShareActionHandlerModule/index.ts +++ b/src/libs/ShareActionHandlerModule/index.ts @@ -14,7 +14,7 @@ type ShareActionContent = { // Type for the ShareActionHandler module type ShareActionHandlerModule = { // Method to process files, which takes a callback function - processFiles(callback: (files: ShareActionContent[] | string | ShareActionContent | null | undefined) => void): void; + processFiles(callback: (files: ShareActionContent[] | string | null) => void): void; }; export default ShareActionHandler; From c072f9ebff70e13b7ac39931d7970333171c590c Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Thu, 22 Oct 2026 10:13:40 +0430 Subject: [PATCH 18/19] Use intent MIME type for shared attachments --- .../com/expensify/chat/intentHandler/FileIntentHandler.kt | 7 +------ .../com/expensify/chat/intentHandler/TextIntentHandler.kt | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt index 9e8dc3b7d22b..9ed2d1a5d83d 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt @@ -25,12 +25,7 @@ class FileIntentHandler(private val context: Context) : AbstractIntentHandler() private fun saveSharedFile(intent: Intent, context: Context, shouldLaunchActivity: Boolean) { val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return - val mimeType = intent.type ?: try { - context.contentResolver.getType(fileUri) - } catch (exception: Exception) { - null - } - val shareFileObject = ShareFileObject(resultingPath, mimeType) + val shareFileObject = ShareFileObject(resultingPath, intent.type) val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) val editor = sharedPreferences.edit() diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt index a113ae56457a..7b26fde77630 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt @@ -62,12 +62,7 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() private fun saveSharedTextFile(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return false val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return false - val mimeType = intent.type ?: try { - context.contentResolver.getType(fileUri) - } catch (exception: Exception) { - null - } - val shareFileObject = ShareFileObject(resultingPath, mimeType) + val shareFileObject = ShareFileObject(resultingPath, intent.type) return saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString(), shouldLaunchActivity) } From 716d4da1ff6f7626a5c2bc6970dbcff74fabdc79 Mon Sep 17 00:00:00 2001 From: Nabi Ebrahimi Date: Thu, 22 Oct 2026 10:40:03 +0430 Subject: [PATCH 19/19] Remove unused text share save results --- .../chat/intentHandler/TextIntentHandler.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt index 7b26fde77630..008af17e703b 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt @@ -23,12 +23,12 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() return false } - private fun saveSharedText(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { + private fun saveSharedText(intent: Intent, context: Context, shouldLaunchActivity: Boolean) { when { intent.type == "text/plain" -> { val extras = intent.extras if (extras != null) { - return when { + when { extras.containsKey(Intent.EXTRA_STREAM) -> { saveSharedTextFile(intent, context, shouldLaunchActivity) } @@ -40,36 +40,36 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() } } } - return false + return } - Regex("text/.*").matches(intent.type ?: "") -> return saveSharedTextFile(intent, context, shouldLaunchActivity) + Regex("text/.*").matches(intent.type ?: "") -> saveSharedTextFile(intent, context, shouldLaunchActivity) else -> throw UnsupportedOperationException("Unsupported MIME type: ${intent.type}") } } - private fun saveToSharedPreferences(key: String, value: String, shouldLaunchActivity: Boolean): Boolean { + private fun saveToSharedPreferences(key: String, value: String, shouldLaunchActivity: Boolean) { val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) val editor = sharedPreferences.edit() editor.putString(key, value) if (shouldLaunchActivity) { editor.apply() - return true + return } - return editor.commit() + editor.commit() } - private fun saveSharedTextFile(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { - val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return false - val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return false + private fun saveSharedTextFile(intent: Intent, context: Context, shouldLaunchActivity: Boolean) { + val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) ?: return + val resultingPath = FileUtils.copyUriToStorage(fileUri, context) ?: return val shareFileObject = ShareFileObject(resultingPath, intent.type) - return saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString(), shouldLaunchActivity) + saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString(), shouldLaunchActivity) } - private fun saveSharedTextContent(intent: Intent, context: Context, shouldLaunchActivity: Boolean): Boolean { - val intentTextContent = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return false + private fun saveSharedTextContent(intent: Intent, context: Context, shouldLaunchActivity: Boolean) { + val intentTextContent = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return val shareFileObject = ShareFileObject(intentTextContent, intent.type) - return saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString(), shouldLaunchActivity) + saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString(), shouldLaunchActivity) } override fun onCompleted() {