Fix Android image share workflow and add local plugin persistence

- Add SafeArea and SharedImage plugins to capacitor.plugins.json
- Create restore-local-plugins.js to persist plugins after cap sync
- Integrate plugin restoration into build scripts
- Improve Android share intent timing with retry logic
- Clean up debug logging while keeping essential error handling
- Add documentation for local plugin management

Fixes issue where SharedImage plugin wasn't recognized, causing
"UNIMPLEMENTED" errors. Image sharing now works correctly on Android.
This commit is contained in:
Jose Olarte III
2025-12-09 21:36:01 +08:00
parent 0c66142093
commit a672c977a8
7 changed files with 207 additions and 35 deletions

View File

@@ -34,5 +34,13 @@
{
"pkg": "@capawesome/capacitor-file-picker",
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
},
{
"pkg": "SafeArea",
"classpath": "app.timesafari.safearea.SafeAreaPlugin"
},
{
"pkg": "SharedImage",
"classpath": "app.timesafari.sharedimage.SharedImagePlugin"
}
]

View File

@@ -23,32 +23,37 @@ public class SharedImagePlugin extends Plugin {
*/
@PluginMethod
public void getSharedImage(PluginCall call) {
SharedPreferences prefs = getSharedPreferences();
String base64 = prefs.getString(KEY_BASE64, null);
String fileName = prefs.getString(KEY_FILE_NAME, null);
if (base64 == null || fileName == null) {
// No shared image exists - return null (not an error)
try {
SharedPreferences prefs = getSharedPreferences();
String base64 = prefs.getString(KEY_BASE64, null);
String fileName = prefs.getString(KEY_FILE_NAME, null);
if (base64 == null || fileName == null) {
// No shared image exists - return null values (not an error)
JSObject result = new JSObject();
result.put("base64", (String) null);
result.put("fileName", (String) null);
call.resolve(result);
return;
}
// Clear the shared data after reading
SharedPreferences.Editor editor = prefs.edit();
editor.remove(KEY_BASE64);
editor.remove(KEY_FILE_NAME);
editor.remove(KEY_READY);
editor.apply();
// Return the shared image data
JSObject result = new JSObject();
result.put("base64", null);
result.put("fileName", null);
result.put("base64", base64);
result.put("fileName", fileName);
call.resolve(result);
return;
} catch (Exception e) {
android.util.Log.e("SharedImagePlugin", "Error in getSharedImage()", e);
call.reject("Error getting shared image: " + e.getMessage());
}
// Clear the shared data after reading
SharedPreferences.Editor editor = prefs.edit();
editor.remove(KEY_BASE64);
editor.remove(KEY_FILE_NAME);
editor.remove(KEY_READY);
editor.apply();
// Return the shared image data
JSObject result = new JSObject();
result.put("base64", base64);
result.put("fileName", fileName);
call.resolve(result);
}
/**

View File

@@ -27,8 +27,8 @@
"auto-run:android": "./scripts/auto-run.sh --platform=android",
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
"build:capacitor:sync": "npm run build:capacitor && npx cap sync",
"build:native": "vite build && npx cap sync && npx capacitor-assets generate",
"build:capacitor:sync": "npm run build:capacitor && npx cap sync && node scripts/restore-local-plugins.js",
"build:native": "vite build && npx cap sync && node scripts/restore-local-plugins.js && npx capacitor-assets generate",
"assets:config": "npx tsx scripts/assets-config.ts",
"assets:validate": "npx tsx scripts/assets-validator.ts",
"assets:validate:android": "./scripts/build-android.sh --assets-only",

View File

@@ -0,0 +1,60 @@
# Restore Local Capacitor Plugins
## Overview
The `restore-local-plugins.js` script ensures that local custom Capacitor plugins (`SafeArea` and `SharedImage`) are automatically restored to `android/app/src/main/assets/capacitor.plugins.json` after running `npx cap sync android`.
## Why This Is Needed
The `capacitor.plugins.json` file is auto-generated by Capacitor during `npx cap sync` and gets overwritten, removing any manually added local plugins. This script automatically restores them.
## Usage
### Automatic (Recommended)
The script is automatically run by:
- `./scripts/build-android.sh` (after `cap sync`)
- `npm run build:capacitor:sync`
- `npm run build:native`
### Manual
If you run `npx cap sync android` directly, you can restore plugins manually:
```bash
node scripts/restore-local-plugins.js
```
## What It Does
1. Reads `android/app/src/main/assets/capacitor.plugins.json`
2. Checks if local plugins (`SafeArea` and `SharedImage`) are present
3. Adds any missing local plugins
4. Preserves the existing JSON format
## Local Plugins
The following local plugins are automatically restored:
- **SafeArea**: `app.timesafari.safearea.SafeAreaPlugin`
- **SharedImage**: `app.timesafari.sharedimage.SharedImagePlugin`
## Adding New Local Plugins
To add a new local plugin, edit `scripts/restore-local-plugins.js` and add it to the `LOCAL_PLUGINS` array:
```javascript
const LOCAL_PLUGINS = [
// ... existing plugins ...
{
pkg: 'YourPluginName',
classpath: 'app.timesafari.yourpackage.YourPluginClass'
}
];
```
## Notes
- The script is idempotent - running it multiple times won't create duplicates
- The script preserves the existing JSON formatting (tabs, etc.)
- If the plugins file doesn't exist, the script will exit with an error (run `npx cap sync android` first)

View File

@@ -385,6 +385,7 @@ fi
if [ "$SYNC_ONLY" = true ]; then
log_info "Sync-only mode: syncing with Capacitor"
safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6
safe_execute "Restoring local plugins" "node scripts/restore-local-plugins.js" || exit 7
log_success "Sync completed successfully!"
exit 0
fi
@@ -472,6 +473,9 @@ fi
# Step 8: Sync with Capacitor
safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6
# Step 8.5: Restore local plugins (capacitor.plugins.json gets overwritten by cap sync)
safe_execute "Restoring local plugins" "node scripts/restore-local-plugins.js" || exit 7
# Step 9: Generate assets
safe_execute "Generating assets" "npx capacitor-assets generate --android" || exit 7

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env node
/**
* Restore Local Capacitor Plugins
*
* This script ensures that local custom plugins (SafeArea and SharedImage)
* are present in capacitor.plugins.json after `npx cap sync` runs.
*
* The capacitor.plugins.json file is auto-generated by Capacitor and gets
* overwritten during sync, so we need to restore our local plugins.
*
* Usage:
* node scripts/restore-local-plugins.js
*
* This should be run after `npx cap sync android` or `npx cap sync ios`
*/
const fs = require('fs');
const path = require('path');
const PLUGINS_FILE = path.join(__dirname, '../android/app/src/main/assets/capacitor.plugins.json');
// Local plugins that need to be added
const LOCAL_PLUGINS = [
{
pkg: 'SafeArea',
classpath: 'app.timesafari.safearea.SafeAreaPlugin'
},
{
pkg: 'SharedImage',
classpath: 'app.timesafari.sharedimage.SharedImagePlugin'
}
];
function restoreLocalPlugins() {
try {
// Read the current plugins file
if (!fs.existsSync(PLUGINS_FILE)) {
console.error(`❌ Plugins file not found: ${PLUGINS_FILE}`);
console.error(' Run "npx cap sync android" first to generate the file.');
process.exit(1);
}
const content = fs.readFileSync(PLUGINS_FILE, 'utf8');
let plugins = JSON.parse(content);
if (!Array.isArray(plugins)) {
console.error(`❌ Invalid plugins file format: expected array, got ${typeof plugins}`);
process.exit(1);
}
// Check which local plugins are missing
const existingPackages = new Set(plugins.map(p => p.pkg));
const missingPlugins = LOCAL_PLUGINS.filter(p => !existingPackages.has(p.pkg));
if (missingPlugins.length === 0) {
console.log('✅ All local plugins are already present in capacitor.plugins.json');
return;
}
// Add missing plugins
plugins.push(...missingPlugins);
// Write back to file with proper formatting (matching existing style)
const formatted = JSON.stringify(plugins, null, '\t');
fs.writeFileSync(PLUGINS_FILE, formatted + '\n', 'utf8');
console.log('✅ Restored local plugins to capacitor.plugins.json:');
missingPlugins.forEach(p => {
console.log(` - ${p.pkg} (${p.classpath})`);
});
} catch (error) {
console.error('❌ Error restoring local plugins:', error.message);
process.exit(1);
}
}
// Run the script
restoreLocalPlugins();

View File

@@ -149,16 +149,22 @@ async function checkAndStoreNativeSharedImage(): Promise<{
return { success: false };
}
const platform = Capacitor.getPlatform();
logger.debug(
`[Main] Checking for ${platform} shared image via SharedImage plugin`,
);
// Use SharedImage plugin to get shared image data directly from native layer
// No file I/O or polling needed - direct native-to-JS communication
const result = await SharedImage.getSharedImage();
let result;
try {
result = await SharedImage.getSharedImage();
} catch (error) {
logger.error("[Main] Error calling SharedImage.getSharedImage():", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
isProcessingSharedImage = false;
return { success: false };
}
if (result && result.base64) {
// Check if we have valid image data (base64 must be non-null and non-empty)
if (result && result.base64 && result.base64.trim().length > 0) {
const fileName = result.fileName || "shared-image.jpg";
// Store in temp database using extracted method
@@ -394,13 +400,24 @@ app.mount("#app");
logger.info(`[Main] ✅ App mounted successfully`);
// Check for shared image on initial load (in case app was launched from share sheet)
// On Android, share intents are processed in MainActivity.onCreate, so we need to check
// after a delay to ensure the native code has finished processing
if (
Capacitor.isNativePlatform() &&
(Capacitor.getPlatform() === "ios" || Capacitor.getPlatform() === "android")
) {
setTimeout(async () => {
await checkForSharedImageAndNavigate();
}, 1000); // Small delay to ensure router is ready
// Use multiple checks with increasing delays to handle timing issues
// Android share intent processing happens in onCreate, which may complete after JS loads
const checkDelays =
Capacitor.getPlatform() === "android"
? [500, 1500, 3000] // Android needs more time for share intent processing
: [1000]; // iOS is faster
checkDelays.forEach((delay) => {
setTimeout(async () => {
await checkForSharedImageAndNavigate();
}, delay);
});
}
// Listen for app state changes to detect when app becomes active