|
|
|
|
@@ -61,7 +61,7 @@ External App (Photos, Safari, etc.)
|
|
|
|
|
│ SharedImagePlugin.getSharedImage() │
|
|
|
|
|
│ → SharedImageUtility │
|
|
|
|
|
│ .getSharedImageData() │
|
|
|
|
|
│ (reads file, deletes native data) │
|
|
|
|
|
│ (read-only; leaves native data intact) │
|
|
|
|
|
└─────────────────────────────────────┘
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
@@ -103,7 +103,7 @@ External App (Photos, Safari, etc.)
|
|
|
|
|
|
|
|
|
|
| File | Method | Responsibility |
|
|
|
|
|
|------|--------|----------------|
|
|
|
|
|
| `ios/App/App/SharedImageUtility.swift` | `getSharedImageData()` | Reads file from App Group, returns base64 + fileName; **deletes metadata and file** |
|
|
|
|
|
| `ios/App/App/SharedImageUtility.swift` | `getSharedImageData()` | Read-only: reads file from App Group, returns base64 + fileName; leaves metadata and file intact (Phase 1C) |
|
|
|
|
|
| | `hasSharedImage()` | Non-destructive existence check (metadata + file on disk) |
|
|
|
|
|
| | `isSharedPhotoReady()` | Reads `sharedPhotoReady` flag |
|
|
|
|
|
| | `clearSharedPhotoReadyFlag()` | Removes `sharedPhotoReady` key |
|
|
|
|
|
@@ -171,16 +171,29 @@ External App (Photos, Safari, etc.)
|
|
|
|
|
| File | Method | Keys Removed |
|
|
|
|
|
|------|--------|--------------|
|
|
|
|
|
| `ShareViewController.swift` | `storeImageData(_:fileName:)` | `sharedPhotoBase64` (legacy cleanup) |
|
|
|
|
|
| `SharedImageUtility.swift` | `getSharedImageData()` | `sharedPhotoFilePath`, `sharedPhotoFileName` |
|
|
|
|
|
| `SharedImageUtility.swift` | `clearSharedPhotoReadyFlag()` | `sharedPhotoReady` |
|
|
|
|
|
| `AppDelegate.swift` | `checkForSharedImageOnActivation()` | `sharedPhotoReady` (via `clearSharedPhotoReadyFlag()`) |
|
|
|
|
|
|
|
|
|
|
**Removed in Phase 1C** (previously in `SharedImageUtility.getSharedImageData()`):
|
|
|
|
|
|
|
|
|
|
| Keys / files | Mechanism |
|
|
|
|
|
|--------------|-----------|
|
|
|
|
|
| `sharedPhotoFilePath` | `userDefaults.removeObject(forKey:)` |
|
|
|
|
|
| `sharedPhotoFileName` | `userDefaults.removeObject(forKey:)` |
|
|
|
|
|
| Image file at `sharedPhotoFilePath` | `FileManager.removeItem(at:)` |
|
|
|
|
|
| `userDefaults.synchronize()` after deletion | Called after metadata/file removal |
|
|
|
|
|
|
|
|
|
|
### Deletes — Shared Image Files (App Group Container)
|
|
|
|
|
|
|
|
|
|
| File | Method | Details |
|
|
|
|
|
|------|--------|---------|
|
|
|
|
|
| `ShareViewController.swift` | `storeImageData(_:fileName:)` | Removes existing file at target path before write |
|
|
|
|
|
| `SharedImageUtility.swift` | `getSharedImageData()` | `FileManager.removeItem(at: fileURL)` after successful read |
|
|
|
|
|
| `ShareViewController.swift` | `storeImageData(_:fileName:shareId:)` | Removes previous pending share file at prior `sharedPhotoFilePath` before write |
|
|
|
|
|
|
|
|
|
|
**Removed in Phase 1C** (previously in `SharedImageUtility.getSharedImageData()`):
|
|
|
|
|
|
|
|
|
|
| Operation | Mechanism |
|
|
|
|
|
|-----------|-----------|
|
|
|
|
|
| Delete image file after successful read | `FileManager.removeItem(at: fileURL)` |
|
|
|
|
|
|
|
|
|
|
### Secondary Storage (Post-Native Consumption)
|
|
|
|
|
|
|
|
|
|
@@ -230,11 +243,16 @@ After native read, image data lives in SQLite `temp` table under key `shared-pho
|
|
|
|
|
|
|
|
|
|
| When | File | Method | What is deleted |
|
|
|
|
|
|------|------|--------|-----------------|
|
|
|
|
|
| Before overwrite | `ShareViewController.swift` | `storeImageData` | Existing file at same filename |
|
|
|
|
|
| Before new share write | `ShareViewController.swift` | `storeImageData` | Previous pending share file at prior `sharedPhotoFilePath` |
|
|
|
|
|
| Legacy cleanup on write | `ShareViewController.swift` | `storeImageData` | `sharedPhotoBase64` UserDefaults key |
|
|
|
|
|
| On successful read | `SharedImageUtility.swift` | `getSharedImageData` | `sharedPhotoFilePath`, `sharedPhotoFileName`, image file |
|
|
|
|
|
| On app activation (flag only) | `AppDelegate.swift` | `checkForSharedImageOnActivation` | `sharedPhotoReady` flag |
|
|
|
|
|
|
|
|
|
|
**Removed in Phase 1C** (no longer deleted on retrieve):
|
|
|
|
|
|
|
|
|
|
| When | File | Method | What was deleted |
|
|
|
|
|
|------|------|--------|------------------|
|
|
|
|
|
| On successful read | `SharedImageUtility.swift` | `getSharedImageData` | `sharedPhotoFilePath`, `sharedPhotoFileName`, image file |
|
|
|
|
|
|
|
|
|
|
### Temp Database (JS)
|
|
|
|
|
|
|
|
|
|
| When | File | Method | What is deleted |
|
|
|
|
|
@@ -242,13 +260,13 @@ After native read, image data lives in SQLite `temp` table under key `shared-pho
|
|
|
|
|
| Before storing new share | `main.capacitor.ts` | `storeSharedImageInTempDB` | Prior `shared-photo-base64` temp row |
|
|
|
|
|
| After view loads image | `SharedPhotoView.vue` | `loadSharedImage` | `shared-photo-base64` temp row |
|
|
|
|
|
|
|
|
|
|
**Important:** Native image file and metadata survive until `getSharedImageData()` succeeds. The `sharedPhotoReady` flag is cleared independently and earlier by `AppDelegate`.
|
|
|
|
|
**Important:** Native image file and metadata persist after `getSharedImageData()` (Phase 1C). Cleanup is deferred to a later phase. The `sharedPhotoReady` flag is still cleared independently by `AppDelegate` on activation.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Potential Race Conditions
|
|
|
|
|
|
|
|
|
|
1. **Multiple JS detection paths, single destructive read.** `applicationDidBecomeActive`, the 1000ms startup timer, `appStateChange`, and `appUrlOpen` can all invoke `checkAndStoreNativeSharedImage()` close together. `getSharedImageData()` deletes the file and metadata on first successful read; subsequent calls return null. The `isProcessingSharedImage` JS lock reduces but does not eliminate races between separate async entry points that start before the lock is set.
|
|
|
|
|
1. **Multiple JS detection paths, repeatable native read.** `applicationDidBecomeActive`, the 1000ms startup timer, `appStateChange`, and `appUrlOpen` can all invoke `checkAndStoreNativeSharedImage()` close together. Since Phase 1C, `getSharedImageData()` is read-only and returns the same data on every call until a new share overwrites metadata or explicit cleanup is added. The `isProcessingSharedImage` JS lock still reduces duplicate temp-DB writes and navigations.
|
|
|
|
|
|
|
|
|
|
2. **Deep-link listener registered 2 seconds after mount.** The share extension opens `timesafari://` immediately. If Capacitor does not buffer the launch URL until the `appUrlOpen` listener is registered (at T+2000ms), the deep-link path may be missed on cold start. The 1000ms startup check and `appStateChange` paths partially compensate.
|
|
|
|
|
|
|
|
|
|
@@ -264,7 +282,7 @@ After native read, image data lives in SQLite `temp` table under key `shared-pho
|
|
|
|
|
|
|
|
|
|
8. **Stale comment implies polling that does not exist.** `handleDeepLink` comments reference internal polling in `checkAndStoreNativeSharedImage`, but no retry loop exists. A single failed read at the wrong moment is not retried on iOS (unlike Android's multi-delay startup checks).
|
|
|
|
|
|
|
|
|
|
9. **`hasSharedImage()` unused.** A non-destructive pre-check is available natively but JS always calls destructive `getSharedImage()` directly, making timing-sensitive false negatives more likely if called before the extension finishes writing.
|
|
|
|
|
9. **`hasSharedImage()` unused.** A non-destructive pre-check is available natively but JS always calls `getSharedImage()` directly. Since Phase 1C both methods are non-destructive on the native layer.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
@@ -292,21 +310,22 @@ All log lines use the prefix `[ShareTarget]` and include `shareId=<id>`:
|
|
|
|
|
| share received | `ShareViewController.swift` | `processSharedImage` | UUID generated before `loadItem` |
|
|
|
|
|
| file stored | `ShareViewController.swift` | `storeImageData` | After successful `imageData.write(to:)` |
|
|
|
|
|
| metadata stored | `ShareViewController.swift` | `storeImageData` | After UserDefaults `synchronize()` |
|
|
|
|
|
| share retrieved | `SharedImageUtility.swift` | `getSharedImageData` | After successful file read (only if `sharedPhotoShareId` is present) |
|
|
|
|
|
| share retrieved | `SharedImageUtility.swift` | `getSharedImageData` | After successful file read (Phase 1C log format) |
|
|
|
|
|
|
|
|
|
|
Example log sequence for a single share:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
[ShareTarget] share received shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890
|
|
|
|
|
[ShareTarget] file stored shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890
|
|
|
|
|
[ShareTarget] metadata stored shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890
|
|
|
|
|
[ShareTarget] share retrieved shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890
|
|
|
|
|
[ShareTarget] file stored shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890 originalFilename=vacation.jpg storedFilename=A1B2C3D4-E5F6-7890-ABCD-EF1234567890.jpg
|
|
|
|
|
[ShareTarget] metadata stored shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890 originalFilename=vacation.jpg storedFilename=A1B2C3D4-E5F6-7890-ABCD-EF1234567890.jpg
|
|
|
|
|
[ShareTarget] shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890 retrieved
|
|
|
|
|
[ShareTarget] shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890 left intact after retrieval
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Phase 1A Scope (Intentionally Unchanged)
|
|
|
|
|
|
|
|
|
|
- `getSharedImageData()` still returns only `base64` and `fileName` to JavaScript
|
|
|
|
|
- `sharedPhotoShareId` is **not** deleted on retrieve (deletion deferred to a later phase)
|
|
|
|
|
- `sharedPhotoShareId` is not deleted on retrieve (cleanup deferred to a later phase)
|
|
|
|
|
- `hasSharedImage()`, `isSharedPhotoReady()`, and JS consumption paths are unchanged
|
|
|
|
|
- Android code is unchanged
|
|
|
|
|
|
|
|
|
|
@@ -358,17 +377,65 @@ Store and retrieve events include all three identifiers:
|
|
|
|
|
```
|
|
|
|
|
[ShareTarget] file stored shareId=<id> originalFilename=<name> storedFilename=<shareId>.<ext>
|
|
|
|
|
[ShareTarget] metadata stored shareId=<id> originalFilename=<name> storedFilename=<shareId>.<ext>
|
|
|
|
|
[ShareTarget] share retrieved shareId=<id> originalFilename=<name> storedFilename=<shareId>.<ext>
|
|
|
|
|
[ShareTarget] shareId=<id> retrieved
|
|
|
|
|
[ShareTarget] shareId=<id> left intact after retrieval
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Phase 1B Scope (Intentionally Unchanged)
|
|
|
|
|
|
|
|
|
|
- `getSharedImageData()` still returns only `base64` and original `fileName` to JavaScript
|
|
|
|
|
- Retrieval timing, deletion keys, and `hasSharedImage()` behavior are unchanged
|
|
|
|
|
- Android code is unchanged
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Non-Destructive Retrieval
|
|
|
|
|
|
|
|
|
|
**Implemented:** 2026-06-24 (Phase 1C)
|
|
|
|
|
|
|
|
|
|
Phase 1C makes native shared-content retrieval read-only. `getSharedImageData()` and `SharedImagePlugin.getSharedImage()` no longer delete App Group metadata or image files after a successful read. Explicit cleanup is deferred to a later phase.
|
|
|
|
|
|
|
|
|
|
### Behavior Change
|
|
|
|
|
|
|
|
|
|
| Aspect | Before Phase 1C | After Phase 1C |
|
|
|
|
|
|--------|-----------------|----------------|
|
|
|
|
|
| `sharedPhotoFilePath` after retrieve | Removed | Retained |
|
|
|
|
|
| `sharedPhotoFileName` after retrieve | Removed | Retained |
|
|
|
|
|
| `sharedPhotoShareId` after retrieve | Retained (since Phase 1A) | Retained |
|
|
|
|
|
| Image file after retrieve | Deleted | Retained |
|
|
|
|
|
| Return value to JS | `{ base64, fileName }` | Unchanged |
|
|
|
|
|
| Repeat `getSharedImage()` calls | Return `null` after first success | Return same data until overwritten or cleaned up |
|
|
|
|
|
|
|
|
|
|
### Logging
|
|
|
|
|
|
|
|
|
|
After a successful read:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
[ShareTarget] shareId=<id> retrieved
|
|
|
|
|
[ShareTarget] shareId=<id> left intact after retrieval
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Removed Deletion Paths
|
|
|
|
|
|
|
|
|
|
All removal logic was in `SharedImageUtility.getSharedImageData()`:
|
|
|
|
|
|
|
|
|
|
| # | What was deleted | Code removed |
|
|
|
|
|
|---|------------------|--------------|
|
|
|
|
|
| 1 | `sharedPhotoFilePath` UserDefaults key | `userDefaults.removeObject(forKey: sharedPhotoFilePathKey)` |
|
|
|
|
|
| 2 | `sharedPhotoFileName` UserDefaults key | `userDefaults.removeObject(forKey: sharedPhotoFileNameKey)` |
|
|
|
|
|
| 3 | Image file at `{container}/{sharedPhotoFilePath}` | `FileManager.default.removeItem(at: fileURL)` |
|
|
|
|
|
| 4 | Post-deletion UserDefaults flush | `userDefaults.synchronize()` after removals |
|
|
|
|
|
|
|
|
|
|
`SharedImagePlugin.getSharedImage(_:)` delegated to `getSharedImageData()` and had no independent deletion logic. Comment updated to reflect read-only behavior.
|
|
|
|
|
|
|
|
|
|
### Phase 1C Scope (Intentionally Unchanged)
|
|
|
|
|
|
|
|
|
|
- No new cleanup or purge APIs added
|
|
|
|
|
- `clearSharedPhotoReadyFlag()` and share-extension write-side file removal unchanged
|
|
|
|
|
- JS temp DB deletion in `main.capacitor.ts` and `SharedPhotoView.vue` unchanged
|
|
|
|
|
- Android code unchanged
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Configuration References
|
|
|
|
|
|
|
|
|
|
| Resource | Value |
|
|
|
|
|
|