Direct answer
A document URI is a provider contract, not a filesystem path. When a runtime needs model bytes, first identify the input contract it actually documents: sequential stream, whole-file descriptor, descriptor plus offset and length, or a seekable path. Android ContentResolver.openFileDescriptor() opens through ContentProvider.openFile(); its read-only descriptor may be a pipe or socket for streaming, so it is not necessarily seekable. It also cannot represent providers that expose a subsection of a backing file. For a provider asset that may be a subsection, openAssetFileDescriptor() is the relevant API because AssetFileDescriptor carries both the underlying ParcelFileDescriptor and the asset start offset and declared length. The basic openFileDescriptor overload is API 1 and the CancellationSignal overload is API 19; openAssetFileDescriptor is API 3, with its cancellation overload at API 19. Check availability against the app minimum rather than copying an overload blindly. Do not mistake the descriptor length fields for a generic file-size query. AssetFileDescriptor.getDeclaredLength() reports what the provider declared; UNKNOWN_LENGTH is -1, meaning no length was declared, not zero bytes and not permission to read without a bound. getLength() may use ParcelFileDescriptor.getStatSize() when the declared length is unknown, but it can still report UNKNOWN_LENGTH. Provider metadata such as a display name, MIME type, or queried size is a separate observation from this descriptor contract. A provider can also use a typed asset path for a read-only content URI, so validate the actual returned bytes rather than assuming a name identifies the byte representation. If a parser accepts a sequential stream, AssetFileDescriptor.createInputStream() is the documented bounded wrapper for the asset. It accounts for the AFD offset and extent; do not manually skip startOffset again. The API says it returns an auto-close stream for a whole file or a subsection and should be called only once per asset. Consume it within its ownership scope and close it deterministically. A pipe fixture should start at byte zero of the selected asset. If a provider stream cannot expose the selected range, copying bytes from the start of the parent container does not repair it; request a stream that begins at the selected bytes or reject the input. If a native reader accepts a descriptor, pass an offset and length only when that exact runtime API documents those parameters and their meaning. A bare file descriptor does not describe an AssetFileDescriptor subrange to an API that only accepts a path or descriptor. An offset-aware consumer may support the selected slice without copying, but keep the ParcelFileDescriptor alive until native consumption finishes and follow the runtime’s ownership and close contract. Do not invent a LiteRT-LM, TFLite, or Cove API from the fact that Android exposes a descriptor. The repository’s downloader writes its own temporary download; that does not establish that Cove imports model URIs or feeds provider descriptors to native inference. Use two independent range fixtures. First, wrap sentinel bytes in a seekable backing file and advertise a non-zero startOffset with a known length; verify that the consumer receives only that range, not the header or trailer. Second, make a stream or pipe whose first byte is already the selected asset, so startOffset is zero, and test both a known length and UNKNOWN_LENGTH. The AssetFileDescriptor contract requires startOffset zero when length is UNKNOWN_LENGTH. Treat negative offsets, negative lengths other than UNKNOWN_LENGTH, non-zero offset with unknown length, overflow in offset-plus-length, and early EOF for a known range as invalid. A valid zero-offset UNKNOWN_LENGTH stream may continue to EOF only under an independent byte cap; unknown does not mean empty or unlimited, and is not a reason by itself to reject it or copy a parent file. If seek capability must be checked, prefer the non-moving Os.lseek(fd, 0, SEEK_CUR) query on a descriptor with known ownership; the Os wrapper and SEEK_CUR constant are API 21. A pipe will report that it cannot seek. A seek to another position changes the shared open-file-description offset, so never probe while the stream or native consumer is using it. Android documents that ParcelFileDescriptor.dup(FileDescriptor) is API 13 and instance dup() is API 14; both share file position with the original, so dup is not an independent probe handle. If a moving probe is unavoidable, use a separately opened handle with no active reader and restore its saved position before handing it off. Never request read-write access solely to obtain seeking. Keep access, byte identity, runtime compatibility, cancellation, and resource lifetime as separate results. ACTION_OPEN_DOCUMENT grants access to a user-selected URI; persist that grant only when later access is needed and the returned flags allow it. A CancellationSignal passed to an open operation can cancel that provider operation, but does not promise to interrupt an arbitrary blocking stream read or native consumer. A copy loop can observe cancellation between reads; keep the descriptor and candidate alive until the active operation actually returns or a documented runtime cancellation finishes. Then close owned resources and delete only app-created temporary bytes. Never delete, rewrite, rename, or claim ownership of the provider source. Do not claim trusted hash verification without an independently trusted expected digest and a completed comparison. Test the boundary with deterministic fixtures: a seekable non-zero-offset known-length backing slice, an offset-zero pipe carrying exactly selected bytes with known and unknown lengths, a complete-file descriptor, and an incompatible non-zero-offset pipe that must fail rather than copy its parent container. Also cover early EOF, negative and overflowing ranges, the byte cap, provider-open cancellation, a read that does not promptly unblock, provider failure, and descriptor lifetime through native completion. Assert that the source is unchanged, partial app-owned copies are removed only after their writer exits, and failed reads never replace the active model. This is a generic compatibility method, not a claim that Cove currently imports model URIs.
A file slice and a provider stream start at different bytes
One test fixture is a regular backing file with a header, a selected slice at a non-zero offset, a known length, and a trailer. A separate provider returns a pipe whose first byte is the selected asset. The native runtime requires seek, so the team must not pretend that pipe can skip the parent-file header.
Decision
Test the known-offset file slice separately from the offset-zero stream. Follow the exact runtime contract or copy only bytes the provider actually exposes; reject an incompatible range instead of copying the whole container.
Expected outcome
The runtime receives only verified selected bytes or a clear input-capability error; the source document and active model remain unchanged.
Evidence to collect
The claim becomes reviewable only when the following evidence is attached to the same artifact and test run:
- Descriptor range and read-only access identity
- Seekable slice and non-seekable pipe fixtures
- Bounded copy, EOF, cancellation and ownership records
A completed byte stream is not enough. Final placement, integrity, runtime load, and recovery must also pass.
Implementation workflow
- Pin the runtime input contract. Read the exact runtime version documentation and determine whether the model loader accepts a sequential stream, whole descriptor, descriptor plus offset and length, or a seekable path. Do not infer support from Android APIs or another runtime.
- Open the provider object with the right API. Use the granted content URI with ContentResolver. Prefer openAssetFileDescriptor when a provider may expose a subrange; use openFileDescriptor only when the provider and runtime contract agree on a whole-file descriptor.
- Record and validate the range. Separate provider metadata from AFD startOffset and declared length. Accept UNKNOWN_LENGTH only with offset zero; reject other negative values, non-zero offset plus unknown length, and overflowing ranges while enforcing the product byte cap.
- Choose stream, direct descriptor, or bounded copy. Use createInputStream when sequential access is enough; it already applies the AFD offset, so do not skip twice. A pipe must begin with the selected bytes. Copy only bytes actually exposed or reject the parent-file subsection.
- Protect seek and cancellation ownership. Use Os.lseek(fd, 0, SEEK_CUR) only as a non-moving query; a positional seek mutates shared offset and ParcelFileDescriptor.dup shares it. CancellationSignal may cancel provider open, but not arbitrary blocking reads or native work.
- Verify cleanup and failure boundaries. Test non-zero-offset known-length file slices separately from offset-zero streams, early EOF, overflow, the byte cap, provider cancellation, delayed read return, and native completion. Keep owners alive until the active operation ends safely.
Keep each transition observable. A failure should identify the stage, artifact, runtime, and recovery action without logging private user content.
Acceptance criteria
- The selected runtime input form is documented and tested for the exact version; a bare FD is not treated as a path or as a slice-aware API.
- Only the seekable fixture tests a non-zero known range; pipe fixtures start at selected byte zero, and invalid negatives, overflow, or non-zero offset plus UNKNOWN_LENGTH fail closed.
- UNKNOWN_LENGTH is accepted at offset zero and read to EOF under an independent cap; an incompatible non-seekable parent subsection is rejected instead of copied from byte zero.
- Cancellation is limited to documented open/provider behavior; keep descriptors owned until blocking reads or native consumption actually finish, then clean only app-owned copies.
Decision table
| Dimension | Choice or evidence | How to interpret it |
|---|---|---|
| Known-offset AFD slice | Use a non-zero offset and known length for a seekable backing-file subsection. | createInputStream applies the asset offset; do not manually skip it a second time. |
| Offset-zero provider stream | Use when the pipe itself begins with the selected asset bytes. | Known length bounds the slice; UNKNOWN_LENGTH is read to EOF only under a separate cap. |
| Native FD plus offset and length | Use only when the exact runtime API explicitly documents slice parameters. | Keep the descriptor alive through use and pass the AFD range; do not assume a plain FD implies either value. |
| App-owned temporary file | Copy when the provider exposes the right bytes but the runtime requires a path or seek. | Copy cannot recover a hidden non-zero slice from a pipe; request a stream of the selected bytes or reject. |
Failure patterns to prevent
- Treating a content URI as a normal path or seekable file
- Reading the entire backing file instead of the declared slice
- Requesting write access just to obtain seeking
Also prevent silent fallback, unpinned artifacts, missing cancellation, and conclusions that combine unlike configurations. Store unsuccessful runs alongside successful ones.
Minimum reproducibility record
| Layer | Record |
|---|---|
| Device | Manufacturer, model, chipset, RAM class, operating-system build |
| Software | Application version and git commit |
| Model | Family, variant, revision, format, file length, hash, quantization |
| Runtime | Name, revision, requested backend, observed backend evidence |
| Workload | Fixture revision, input hash, prompt hash, output policy |
| Outcome | Completed, failed, cancelled, fallback, and privacy-safe diagnostics |
Release checklist
- ☐ The primary query is answered without an unsupported number.
- ☐ Every artifact and runtime is pinned.
- ☐ The representative task and failure policy are explicit.
- ☐ Lifecycle, cancellation, cleanup, and fallback are tested.
- ☐ User-content and network boundaries are documented.
- ☐ Result wording applies only to the recorded configuration.
- ☐ The page links to raw method or evidence when results are added.
Sources and related evidence
- Android privacy and security
- Android API — ContentResolver.openFileDescriptor and openAssetFileDescriptor
- Android API — AssetFileDescriptor range, length, and stream ownership
- Android API — Os.lseek and file-position operations
- Android API — OsConstants seek-origin flags
- Android API — ParcelFileDescriptor.dup shared file-position semantics
- Android API — CancellationSignal operation cancellation
- Android Developers — Access documents with the Storage Access Framework
Chinese deployment and troubleshooting content is organized in the .奇连 AI 端侧专题.
Frequently asked questions
Does createInputStream need another manual skip for startOffset?
No. The AssetFileDescriptor stream applies its asset range. Test the known-offset seekable fixture; if a provider pipe cannot expose exact selected bytes, reject it instead of copying the parent container.
Can UNKNOWN_LENGTH describe a valid selected stream?
Yes, when the AssetFileDescriptor startOffset is zero. It means unknown, not empty; read to EOF only under a separate cap and reject non-zero offset paired with UNKNOWN_LENGTH.
Will CancellationSignal stop a blocked native model read?
Not by itself. It can cancel a documented provider open operation, but arbitrary stream reads or native calls may not respond; keep resources alive until the actual owner safely finishes.
Can dup() isolate a seekability probe from the native reader?
No. Android documents that duplicated ParcelFileDescriptors share file position. Os.lseek(fd, 0, SEEK_CUR) is a non-moving query; moving probes require exclusive ownership.