System file picker. Opens the OS document picker (Files app on iOS, SAF on Android).
No permission required — the user explicitly selects files.
Results arrive as:
handle_info({:files, :picked, items}, socket)
handle_info({:files, :cancelled}, socket)Each item in items is:
%{path: "/tmp/mob_file_xxx.pdf", name: "report.pdf",
mime: "application/pdf", size: 102400}iOS: UIDocumentPickerViewController. Android: OpenMultipleDocuments.
Filtering by file type
Pass :types to pick/2 to limit what the picker offers:
Mob.Files.pick(socket, types: ["livemd"]) # one extension
Mob.Files.pick(socket, types: [:images, :pdf]) # semantic groups
Mob.Files.pick(socket, types: [{:mime, "application/pdf"}])Each entry is one of:
- an extension string —
"livemd"or".livemd"(the leading dot is optional). This is the common case and matches how apps think about the files they own. - a MIME string — any value containing a slash, e.g.
"application/pdf"or a wildcard"text/*". - a semantic atom —
:images,:video,:audio,:pdf,:text. {:extension, ext}/{:mime, type}/{:uti, id}for an explicit kind.{:uti, "dev.livebook.livemd"}targets an iOS Uniform Type Identifier directly.:any(the default) — offer everything.
Platform asymmetry — read this before relying on it
The two platforms filter differently, and a custom extension exposes the gap:
- iOS filters by
UTType, which it can derive from an extension even for an unregistered custom type. Sotypes: ["livemd"]strictly limits the picker to.livemdfiles. - Android SAF filters by MIME type only — it has no extension
filter. A custom extension with no registered MIME (
.livemd) cannot be narrowed at the picker, so the picker stays wide and the user can still tap the "wrong" file.
Because of this, enforce the filter on the result too. pick/2 narrows
the picker where the OS allows; accept/2 rejects anything that slipped
through where it doesn't, giving consistent semantics on both platforms:
def handle_info({:files, :picked, items}, socket) do
case Mob.Files.accept(items, ["livemd"]) do
[%{path: path} | _] -> {:noreply, open(socket, path)}
[] -> {:noreply, put_flash(socket, :error, "Please choose a .livemd file")}
end
endaccept/2 matches on the result's name/mime, so it enforces extensions,
MIME types, and semantic groups. A {:uti, _} spec can't be checked from the
result and is treated as already-enforced by the iOS picker.
"Open with" — files handed to us by another app
When the user opens a file into the app from elsewhere — e.g. a .livemd
emailed to them and tapped — the OS launches (or foregrounds) the app with
that file, provided the app declares the document type. This is a separate,
build-time mechanism from the runtime :types picker filter above:
- iOS:
CFBundleDocumentTypes(+ an imported UTI) inInfo.plist, and anapplication:openURL:options:handler that callsmob_handle_opened_url. - Android: an
<intent-filter>forACTION_VIEW/ACTION_SENDmatching the mime type / extension; the Mob activity forwards it automatically.
Retrieve it with take_opened_document/0 from your root screen's mount/3.
Summary
Types
A single entry in the :types list. See the moduledoc for the full forms.
Functions
Keep only the items in items that satisfy types (see matches?/2).
True if a picked/opened item map satisfies types.
Normalize a :types value into the canonical envelope sent to the native
picker — a list of %{"kind" => kind, "value" => value} maps.
Open the system document picker.
Return the document another app asked us to open, or :none.
Types
Functions
Keep only the items in items that satisfy types (see matches?/2).
Use this in your {:files, :picked, items} handler to enforce a type filter
the picker could not (notably a custom extension on Android SAF).
True if a picked/opened item map satisfies types.
Returns true when types is empty/:any, or when none of the specs are
checkable from the result (e.g. only {:uti, _} hints, which rely on the iOS
picker having already filtered). Otherwise the item must match at least one
spec by extension, MIME, or semantic group.
Normalize a :types value into the canonical envelope sent to the native
picker — a list of %{"kind" => kind, "value" => value} maps.
:any (or "*/*", anywhere in the list) collapses the whole filter to [],
meaning "offer everything". Exposed so the wire contract with the iOS/Android
native layers is testable and documented in one place.
@spec pick( Mob.Socket.t(), keyword() ) :: Mob.Socket.t()
Open the system document picker.
Pass types: [...] to limit what's offered (see the moduledoc). Defaults to
:any. Results arrive asynchronously as {:files, :picked, items} /
{:files, :cancelled} to the calling process.
@spec take_opened_document() :: map() | :none
Return the document another app asked us to open, or :none.
Call once from your root screen's mount/3. The item has the same shape as
pick/2 results:
%{path: "/tmp/demo.livemd", name: "demo.livemd",
mime: "text/markdown", size: 1234}The copied file lives in the app's tmp dir, so read or move it promptly. This call also registers the calling process to receive any file opened later while the app is already running, delivered as:
handle_info({:files, :opened, item}, socket)Returns :none off-device or when nothing is pending. See the moduledoc for
the platform manifest/Info.plist wiring "open with" requires.