diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1d8ad70d11..46e22c040a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -27,6 +27,20 @@ + + + + + + + + + + + + + + imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + if (imageUris != null && !imageUris.isEmpty()) { + processSharedImage(imageUris.get(0), null); + } + } + } + + /** + * Process a shared image: read it, convert to base64, and write to temp file + */ + private void processSharedImage(Uri imageUri, String fileName) { + try { + // Read image data from URI + InputStream inputStream = getContentResolver().openInputStream(imageUri); + if (inputStream == null) { + Log.e(TAG, "Failed to open input stream for shared image"); + return; + } + + // Read image bytes + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] data = new byte[8192]; + int nRead; + while ((nRead = inputStream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + byte[] imageBytes = buffer.toByteArray(); + inputStream.close(); + + // Convert to base64 + String base64String = Base64.encodeToString(imageBytes, Base64.NO_WRAP); + + // Extract filename from URI or use default + String actualFileName = fileName; + if (actualFileName == null || actualFileName.isEmpty()) { + String path = imageUri.getPath(); + if (path != null) { + int lastSlash = path.lastIndexOf('/'); + if (lastSlash >= 0 && lastSlash < path.length() - 1) { + actualFileName = path.substring(lastSlash + 1); + } + } + if (actualFileName == null || actualFileName.isEmpty()) { + actualFileName = "shared-image.jpg"; + } + } + + // Write to temp file in app's internal files directory + // JavaScript will read this file using Capacitor's Filesystem plugin + writeSharedImageToTempFile(base64String, actualFileName); + + Log.d(TAG, "Successfully processed shared image: " + actualFileName); + } catch (IOException e) { + Log.e(TAG, "Error processing shared image", e); + } catch (Exception e) { + Log.e(TAG, "Unexpected error processing shared image", e); + } + } + + /** + * Write shared image data to temp JSON file for JavaScript to read + * File is written to app's internal files directory (accessible via Capacitor Filesystem plugin) + */ + private void writeSharedImageToTempFile(String base64, String fileName) { + try { + // Get app's internal files directory + File filesDir = getFilesDir(); + File tempFile = new File(filesDir, TEMP_FILE_NAME); + + // Create JSON object + JSONObject jsonData = new JSONObject(); + jsonData.put("base64", base64); + jsonData.put("fileName", fileName); + + // Write to file + FileWriter writer = new FileWriter(tempFile); + writer.write(jsonData.toString()); + writer.close(); + + Log.d(TAG, "Wrote shared image data to temp file: " + tempFile.getAbsolutePath()); + } catch (Exception e) { + Log.e(TAG, "Error writing shared image to temp file", e); + } + } } \ No newline at end of file diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index 402c7fbbf4..1bc7f08ef7 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -63,12 +63,14 @@ let isProcessingSharedImage = false; * More reliable than hardcoded timeout - checks if file actually exists * * @param filePath - Path to the file to check + * @param directory - Directory to check (default: Directory.Documents) * @param maxRetries - Maximum number of retry attempts (default: 5) * @param initialDelay - Initial delay in milliseconds (default: 100) * @returns Promise - true if file exists, false if max retries reached */ async function pollForFileExistence( filePath: string, + directory: Directory = Directory.Documents, maxRetries: number = 5, initialDelay: number = 100, ): Promise { @@ -76,7 +78,7 @@ async function pollForFileExistence( try { await Filesystem.stat({ path: filePath, - directory: Directory.Documents, + directory: directory, }); // File exists return true; @@ -176,12 +178,19 @@ async function checkAndStoreNativeSharedImage(): Promise<{ isProcessingSharedImage = true; try { - if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "ios") { + if ( + !Capacitor.isNativePlatform() || + (Capacitor.getPlatform() !== "ios" && + Capacitor.getPlatform() !== "android") + ) { isProcessingSharedImage = false; return { success: false }; } - logger.debug("[Main] Checking for iOS shared image from App Group"); + const platform = Capacitor.getPlatform(); + logger.debug( + `[Main] Checking for ${platform} shared image from native layer`, + ); // Use Capacitor's native bridge to call the ShareImagePlugin // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -194,16 +203,22 @@ async function checkAndStoreNativeSharedImage(): Promise<{ } // WORKAROUND: Since the plugin isn't being auto-discovered, use a temp file bridge - // AppDelegate writes the shared image data to a temp file, and we read it here + // Native layer (AppDelegate on iOS, MainActivity on Android) writes the shared image data to a temp file, and we read it here const tempFilePath = "timesafari_shared_photo.json"; + // Use platform-specific directory: + // - iOS: Directory.Documents (AppDelegate writes to Documents directory) + // - Android: Directory.Data (MainActivity writes to getFilesDir() which maps to Data) + const fileDirectory = + platform === "android" ? Directory.Data : Directory.Documents; + // Check if file exists first (more reliable than hardcoded timeout) - const fileExists = await pollForFileExistence(tempFilePath); + const fileExists = await pollForFileExistence(tempFilePath, fileDirectory); if (fileExists) { try { const fileContent = await Filesystem.readFile({ path: tempFilePath, - directory: Directory.Documents, + directory: fileDirectory, encoding: Encoding.UTF8, }); @@ -223,7 +238,7 @@ async function checkAndStoreNativeSharedImage(): Promise<{ try { await Filesystem.deleteFile({ path: tempFilePath, - directory: Directory.Documents, + directory: fileDirectory, }); logger.debug("[Main] Deleted temp file after reading"); } catch (deleteError) { @@ -492,7 +507,10 @@ const registerDeepLinkListener = async () => { * This is called when app becomes active (from share extension or app launch) */ async function checkForSharedImageAndNavigate() { - if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "ios") { + if ( + !Capacitor.isNativePlatform() || + (Capacitor.getPlatform() !== "ios" && Capacitor.getPlatform() !== "android") + ) { return; } @@ -540,15 +558,21 @@ logger.log("[Capacitor] 🚀 Mounting app"); app.mount("#app"); logger.info(`[Main] ✅ App mounted successfully`); -// Check for shared image on initial load (in case app was launched from share extension) -if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === "ios") { +// Check for shared image on initial load (in case app was launched from share sheet) +if ( + Capacitor.isNativePlatform() && + (Capacitor.getPlatform() === "ios" || Capacitor.getPlatform() === "android") +) { setTimeout(async () => { await checkForSharedImageAndNavigate(); }, 1000); // Small delay to ensure router is ready } // Listen for app state changes to detect when app becomes active -if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === "ios") { +if ( + Capacitor.isNativePlatform() && + (Capacitor.getPlatform() === "ios" || Capacitor.getPlatform() === "android") +) { CapacitorApp.addListener("appStateChange", async ({ isActive }) => { if (isActive) { logger.debug("[Main] 📱 App became active, checking for shared image");