A production-grade Android library for resumable file uploads to Google Cloud Storage signed session URIs, implementing the full GCS resumable upload protocol: chunked PUTs with Content-Range, server-authoritative resume via 308 Resume Incomplete + Range parsing, automatic status-query (bytes */TOTAL) on every resume path, and retry with exponential backoff for transient failures.
- Spec-compliant resumable uploads. Each non-final chunk is a 256 KiB multiple, sent with
Content-Range: bytes start-end/total. The engine never assumes how much the server has — every resume path issues a status-query PUT first and trusts the server'sRangeheader. - Explicit upload state machine. Single enum (
UploadState) with validated transitions. No boolean soup, no resurrection of terminal sessions. - Single-threaded engine. All state mutations happen on one serial executor. OkHttp callbacks, network-change events, and consumer commands all funnel through it, so there are no races.
- Smooth, monotonic progress. Updates fire continuously as each chunk streams (not only at chunk boundaries), throttled to one event per integer-percent change so a 1 GB upload emits ~100 events regardless of chunk size. A
CAS-guarded high-water mark ensures progress never regresses on retry, even when a chunk fails mid-flight. - Resilient cancellation. Generation-counted in-flight calls absorb late OkHttp callbacks; intentional cancels never leak as
onFailure. - Pause / resume / cancel with correct lifecycle callbacks.
- Configurable retry. Exponential backoff with full jitter, bounded by max delay and max attempts. Distinguishes retryable (408/429/5xx) from fatal (other 4xx, 410 Gone → session expired).
- Main-thread callbacks by default. Listener calls land on the main looper unless you supply a different
Executor. - Multi-transport network awareness. Tracks every connected network with INTERNET capability; treats the device as online so long as at least one transport (WiFi or cellular) is up. Transient losses during WiFi/cellular handover don't trip a false
NETWORK_LOST.
- Android 7.0 (API 24) or above
- Kotlin
- A resumable session URI obtained from a
POST .../o?uploadType=resumable&x-goog-resumable=start(or the equivalent signed URL flow). See FastPix's Direct Upload API for credential setup.
dependencies {
implementation("io.fastpix:uploads:2.0.0")
}val uploader = FastPixUploader.Builder(context)
.file(localFile)
.sessionUri(gcsResumableSessionUri)
.chunkSize(8L * 1024 * 1024) // 8 MiB; must be a multiple of 256 KiB
.maxRetries(5)
.retryBaseDelay(2_000L)
.retryMaxDelay(30_000L)
.listener(object : UploadListener {
override fun onStateChange(state: UploadState) { /* drive UI */ }
override fun onProgress(bytesUploaded: Long, totalBytes: Long, percentage: Double) { /* … */ }
override fun onPrepared(totalChunks: Int, totalBytes: Long, chunkSize: Long) { /* … */ }
override fun onChunkUploaded(chunkIndex: Int, totalChunks: Int, bytesAcked: Long) { /* … */ }
override fun onRetryScheduled(attempt: Int, delayMillis: Long, cause: UploadError) { /* … */ }
override fun onNetworkStateChange(online: Boolean) { /* … */ }
override fun onSuccess(elapsedMillis: Long) { /* done */ }
override fun onFailure(error: UploadError, elapsedMillis: Long) { /* terminal */ }
override fun onCancelled(elapsedMillis: Long) { /* terminal */ }
})
.build()
uploader.start()
uploader.pause()
uploader.resume()
uploader.cancel()Every listener method has a no-op default — you only override what you need.
| Method | Required | Default | Notes |
|---|---|---|---|
file(File) |
yes | — | Must exist, be readable, non-empty. |
sessionUri(String) |
yes | — | GCS resumable session URI (not a one-shot signed URL). |
chunkSize(Long) |
no | 8 MiB | Bytes. Must be a multiple of 256 KiB, in [5 MiB, 500 MiB]. |
maxRetries(Int) |
no | 5 | Per-failure retry budget. |
retryBaseDelay(Long) |
no | 2000 ms | Initial backoff. |
retryMaxDelay(Long) |
no | 30000 ms | Cap on a single delay regardless of attempt. |
listener(UploadListener) |
no | none | Receives lifecycle events. |
callbackExecutor(Executor) |
no | main looper | Where listener callbacks run. |
debugLogging(Boolean) |
no | false | Installs HttpLoggingInterceptor at BASIC level. |
IDLE → PREPARING → UPLOADING ↔ {PAUSED, RETRYING, NETWORK_LOST, QUERYING_STATUS} → COMPLETED | FAILED | CANCELLED
COMPLETED, FAILED, and CANCELLED are terminal — no further callbacks fire.
Typed, sealed:
InvalidConfiguration,FileNotFound,FileNotReadable,FileEmpty,FileReadFailureNetworkFailure— transport error, retries exhaustedSessionExpired— HTTP 410, the session URI is dead; mint a new oneClientError(statusCode)— 4xx other than retryableServerError(statusCode)— 5xx after exhausting retriesRetryLimitExceeded(attempts)UnexpectedResponse
The 1.x FastPixUploadSdk / FastPixUploadCallbacks / UploadExceptions API has been replaced.
| 1.x | 2.x |
|---|---|
FastPixUploadSdk.Builder |
FastPixUploader.Builder |
.setSignedUrl(url) |
.sessionUri(uri) (renamed — it's a session URI, not a signed URL) |
.setFile(file) |
.file(file) |
.setChunkSize(bytes) |
.chunkSize(bytes) (now enforces 256 KiB multiple) |
.setMaxRetries(n) |
.maxRetries(n) |
.setRetryDelay(ms) |
.retryBaseDelay(ms) + .retryMaxDelay(ms) |
.callback(cb) |
.listener(cb) |
.build().startUpload() |
.build().start() |
pauseUploading() / resumeUploading() / abort() |
pause() / resume() / cancel() |
FastPixUploadCallbacks interface |
UploadListener interface, no-op defaults, typed errors |
UploadExceptions subclasses |
UploadError sealed hierarchy |