Version
3.10.0
Platforms
Android
Device Model
Android Emulator, Pixel 9 Pro (API 36); reproducible on any AVD
flutter info
[✓] Flutter (Channel stable, 3.44.4, on macOS 26.5.2 darwin-arm64)
• Dart version 3.12.2
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
• Emulator version 36.2.12.0
How to reproduce?
- Call
PhotoManager.addChangeCallback(cb) and PhotoManager.startChangeNotify().
- Drag an image from the host desktop into a running Android Emulator window (or, more generally, cause any insert that MediaStore announces on the
external_primary volume URI only).
- The image is indexed by MediaStore and appears in
getAssetPathList results after a manual refresh — but cb is never invoked.
Root cause
PhotoManagerNotifyChannel.kt registers its three ContentObservers only on the legacy external volume URIs:
private val imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI // content://media/external/images/media
private val videoUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
private val audioUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
Since API 29, MediaStore also exposes collections under the external_primary volume (MediaStore.VOLUME_EXTERNAL_PRIMARY, i.e. content://media/external_primary/...). Some insert paths — emulator drag-and-drop being the easiest to reproduce — post their change notification only on the external_primary URI. notifyForDescendants = true does not help, because content://media/external_primary/images/media is a sibling of content://media/external/images/media, not a descendant.
Note this is distinct from #1117 / #1301 (limited-permission re-selection, fixed by #1357 via an explicit notify): here the observer registration itself never covers the volume, so no permission flow is involved.
Suggested fix
On API 29+, additionally register the same observers on the external_primary collections:
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
Most real-device writes notify both volumes, so this will produce duplicate callbacks for a single insert; the plugin may want to debounce/coalesce internally (cf. #1219), or document that callers should. We currently work around the gap in our app with a host-side ContentObserver on external_primary bridged over an EventChannel, merged with the plugin callback behind a debounce — happy to turn the suggested fix into a PR if you're open to it.
Example code (optional)
PhotoManager.addChangeCallback((call) => debugPrint('change: $call'));
await PhotoManager.startChangeNotify();
// Drag a JPEG from the desktop into the emulator window: nothing is printed.
Version
3.10.0
Platforms
Android
Device Model
Android Emulator, Pixel 9 Pro (API 36); reproducible on any AVD
flutter info
[✓] Flutter (Channel stable, 3.44.4, on macOS 26.5.2 darwin-arm64) • Dart version 3.12.2 [✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0) • Emulator version 36.2.12.0How to reproduce?
PhotoManager.addChangeCallback(cb)andPhotoManager.startChangeNotify().external_primaryvolume URI only).getAssetPathListresults after a manual refresh — butcbis never invoked.Root cause
PhotoManagerNotifyChannel.ktregisters its threeContentObservers only on the legacyexternalvolume URIs:Since API 29, MediaStore also exposes collections under the
external_primaryvolume (MediaStore.VOLUME_EXTERNAL_PRIMARY, i.e.content://media/external_primary/...). Some insert paths — emulator drag-and-drop being the easiest to reproduce — post their change notification only on theexternal_primaryURI.notifyForDescendants = truedoes not help, becausecontent://media/external_primary/images/mediais a sibling ofcontent://media/external/images/media, not a descendant.Note this is distinct from #1117 / #1301 (limited-permission re-selection, fixed by #1357 via an explicit notify): here the observer registration itself never covers the volume, so no permission flow is involved.
Suggested fix
On API 29+, additionally register the same observers on the
external_primarycollections:Most real-device writes notify both volumes, so this will produce duplicate callbacks for a single insert; the plugin may want to debounce/coalesce internally (cf. #1219), or document that callers should. We currently work around the gap in our app with a host-side
ContentObserveronexternal_primarybridged over anEventChannel, merged with the plugin callback behind a debounce — happy to turn the suggested fix into a PR if you're open to it.Example code (optional)