mob_photos
Photo / video library access for apps built with Mob — extracted from mob core as a plugin:
MobPhotos.pick/2— the system picker. iOS:PHPickerViewController(iOS 14+). Android: the system Photo Picker (PickMultipleVisualMedia). Both run out of process, so no permission dialog is shown.MobPhotos.list_media/2— the newest photos/videos in the library with metadata (AndroidMediaStore, iOSPHAsset), no picking needed. Needs the:mediapermission.MobPhotos.thumbnail/2— a downscaled, upright JPEG of one image plus its metadata: dimensions, capture time, GPS, camera make/model. Synchronous.
Installation
# mix.exs
{:mob_photos, "~> 0.2"}
# mob.exs
config :mob, :plugins, [:mob_photos]
The plugin manifest merges READ_MEDIA_IMAGES / READ_MEDIA_VIDEO /
READ_EXTERNAL_STORAGE / ACCESS_MEDIA_LOCATION into the host
AndroidManifest at build time, and a placeholder
NSPhotoLibraryUsageDescription into Info.plist — replace that string (App
Store review rejects the placeholder on purpose).
Usage
Pick
socket = MobPhotos.pick(socket, max: 5)
def handle_info({:photos, :picked, items}, socket) do
# iOS: %{path: "/tmp/mob_pick_xxx.jpg", type: :image, name: "IMG_0042",
# size: 2_481_233, width: 4032, height: 3024}
# Android: %{path: ".../cache/mob_pick_xxx.jpg", type: "image", name: "1000000021.jpg",
# size: 2_481_233, width: 4032, height: 3024}
end
def handle_info({:photos, :cancelled}, socket), do: ...
List the library
socket = Mob.Permissions.request(socket, :media)
def handle_info({:permission, :media, :granted}, socket) do
{:noreply, MobPhotos.list_media(socket, type: :image, limit: 20)}
end
def handle_info({:media, :listed, items}, socket) do
# newest first; each item:
# %{uri: "content://media/external/images/media/42", # iOS: "ph://<localIdentifier>"
# display_name: "IMG_0042.jpg", size: 2_481_233, mime_type: "image/jpeg",
# date_added: 1_700_000_000, date_taken: 1_699_999_000_123,
# width: 4032, height: 3024, type: "image"}
end
size, date_taken, width and height are left out of an item when the
platform doesn't know them — read them with item[:date_taken].
Thumbnail + metadata
{:ok, info} = MobPhotos.thumbnail(item.uri, max_size: 1280, quality: 80)
# %{path: ".../cache/mob_thumb_3fc29d7f5e251c42.jpg", width: 960, height: 1280,
# orig_width: 3000, orig_height: 4000, mime: "image/jpeg", size: 196_758,
# taken_at: "2024-05-01T12:34:56-07:00",
# latitude: 49.2827, longitude: -123.1207, altitude: 70.5,
# make: "TestCam", model: "Probe 1"}
source can be an absolute path (e.g. a picked item's path), an Android
content:// URI or an iOS ph:// id. The caller waits while the image is
decoded on a native worker thread (never a BEAM scheduler). Errors:
{:error, :not_found | :unsupported | :permission | :timeout} or
{:error, message}.
Limits
- Picker items:
typeis an atom on iOS and a string on Android (inherited from core). Android videos reportwidth/height0; iOS videos omit them. - The
types:option ofpick/2is ignored by both native sides; both pickers show images + videos. - Android's photo picker strips GPS from the files it hands out, so
thumbnail/2of a picked item has no location; list the library withlist_media/2(the:mediagrant includesACCESS_MEDIA_LOCATION) to get it. thumbnail/2handles images only; videos return{:error, :unsupported}.
Self-test
MobPhotos.SelfTest proves on a device that the native side is linked and
answering (no picker, no prompt): run mix mob.selftest from a host app that
depends on mob_photos (mob_dev >= 0.7.17). Its library leg needs the photo
library granted. Without the grant it skips: {:skip, :needs_user} on a
phone, or a skip that names the iOS authorization status on a simulator.
For iOS simulators:
- iOS 27 runtimes:
simctl privacy grant photosworks. mob_dev pre-grants it from the release after 0.7.17 (GenericJam/mob_dev#127; 0.7.17 grants the wrong service). On 0.7.17, runxcrun simctl privacy <udid> grant photos <bundle id>before launching the app. - iOS 26.x runtimes: PhotoKit ignores that grant (the TCC row it writes is
version 1), so the library leg reports
not_determinedand skips.
Development
Clone, then run once:
mix setup
That fetches deps and activates the repo's git hooks (.githooks/pre-push):
mix format --check, mix credo --strict (incl. ExSlop), and mix compile --warnings-as-errors run on every push, plus the full test
suite when mix.exs changes — the same gate CI enforces before publishing.
License
MIT