Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .deepsource.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ version = 1
[[analyzers]]
name = "kotlin"

[analyzers.meta]
runtime_version = "1.8"
language_version = "1.7"
[analyzers.meta]
runtime_version = "1.8"
language_version = "1.7"

[[analyzers]]
name = "java"

[analyzers.meta]
runtime_version = "11"
[analyzers.meta]
runtime_version = "11"
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
*.iml
.gradle
/local.properties
local.properties
.idea/
.DS_Store
/build
build/
/captures
.externalNativeBuild
.cxx
local.properties
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright [yyyy] [name of copyright owner]
Copyright 2025 FastPix, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ uploader.cancel()

Every listener method has a no-op default — you only override what you need.

## Example apps

The [`examples/`](examples) folder has three runnable sample apps — the same upload flow (background
upload, pause/resume/cancel, progress notification) in different stacks:

- [`kotlin-xml`](examples/kotlin-xml) — Kotlin with XML views
- [`kotlin-compose`](examples/kotlin-compose) — Kotlin with Jetpack Compose
- [`java-xml`](examples/java-xml) — Java with XML views

See [`examples/README.md`](examples/README.md) for setup and how the background upload works.

## Public API

### `FastPixUploader.Builder`
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/res/xml/backup_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Auto-backup rules for Android 6–11 (API 23–30). Backup is already off via
android:allowBackup="false"; these excludes make the intent explicit and satisfy lint.
Android 12+ (API 31+) uses data_extraction_rules.xml instead.
-->
<full-backup-content>
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="." />
</full-backup-content>
22 changes: 22 additions & 0 deletions app/src/main/res/xml/data_extraction_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Backup is disabled outright by android:allowBackup="false" (covers API 30 and below).
On Android 12+ (API 31+) this file is the modern mechanism: it excludes every data domain
from both cloud backup and device-to-device transfer, so no app data leaves the device.
-->
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
</cloud-backup>
<device-transfer>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
</device-transfer>
</data-extraction-rules>
38 changes: 38 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Example apps

Three small apps that upload a file to FastPix with the [`:uploader`](../uploader) SDK. They all do
the same thing — pick a file, create an upload, and push it in the background with pause/resume/cancel
— just in different stacks, so copy whichever one matches your project:

- **[kotlin-xml](kotlin-xml)** — Kotlin, XML views
- **[kotlin-compose](kotlin-compose)** — Kotlin, Jetpack Compose
- **[java-xml](java-xml)** — Java, XML views

## Setup

Add your FastPix credentials to the project-root `local.properties` (it's gitignored, so they stay
out of the repo):

```properties
fastpix.token=YOUR_ACCESS_TOKEN_ID
fastpix.secretKey=YOUR_SECRET_KEY
```

You'll find them in the [FastPix dashboard](https://dashboard.fastpix.com/) under Access Tokens. For
a real app, create the upload from your own backend instead, so the secret key never ships in the APK.

## Running

Open the project in Android Studio, pick one of the example modules from the run dropdown, and hit
Run. (The command-line `./gradlew` needs JDK 17 or 21 — Gradle 8.11 won't run on newer JDKs.)

## How the background upload works

Each app keeps its uploader in a small singleton (`UploadManager`) instead of the Activity, and runs
a foreground service (`UploadService`) while an upload is in flight. That's what lets the upload keep
going when you leave the screen or rotate the device, with progress shown in a notification.

It's deliberately not persisted — if the app's process is killed, the upload stops with it. If you
need uploads to survive that, move the work into WorkManager and recreate the session from a saved
session URL and file path; the SDK re-checks the server's offset on resume, so it continues from where
it left off.
25 changes: 25 additions & 0 deletions examples/java-xml/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# java-xml

The upload flow in Java with XML views. The app is pure Java (no Kotlin plugin) and talks to the
Kotlin `:uploader` SDK directly, so it's a handy reference if your codebase is Java.

## Running

1. Put your `fastpix.token` and `fastpix.secretKey` in the project-root `local.properties`
(see [../README.md](../README.md)).
2. Open the project in Android Studio, select the `java-xml` module, and Run.
3. Pick a file, tap Start, then use Pause / Resume / Abort. Send the app to the background and the
upload keeps going, with progress in a notification.

## Where things live

- `MainActivity.java` — the screen; observes `UploadManager` between `onStart` and `onStop`.
- `UploadManager.java` — holds the running upload and notifies observers.
- `UploadService.java` — foreground service that keeps the upload alive in the background.
- `FastPixApi.java` — the "create upload" API call.
- `Config.java` — credentials and chunk size.

One Java quirk worth knowing: the SDK's `UploadListener` is a Kotlin interface, so the anonymous
implementation has to override all nine callbacks — Java doesn't see Kotlin's default methods.

See [../README.md](../README.md) for how the background upload works.
55 changes: 55 additions & 0 deletions examples/java-xml/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import java.util.Properties

plugins {
alias(libs.plugins.android.application)
}

// Credentials live in local.properties (gitignored) so they never reach source control.
val localProps = Properties().apply {
rootProject.file("local.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) }
}

android {
namespace = "io.fastpix.uploads.java"
compileSdk = 35

defaultConfig {
applicationId = "io.fastpix.uploads.java"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"

buildConfigField("String", "FASTPIX_TOKEN", "\"${localProps.getProperty("fastpix.token", "")}\"")
buildConfigField("String", "FASTPIX_SECRET_KEY", "\"${localProps.getProperty("fastpix.secretKey", "")}\"")
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

buildFeatures {
viewBinding = true
buildConfig = true
}
}

dependencies {
implementation(project(":uploader"))

implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.constraintlayout)
implementation(libs.okhttp)
}
1 change: 1 addition & 0 deletions examples/java-xml/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Add project specific ProGuard rules here.
36 changes: 36 additions & 0 deletions examples/java-xml/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:allowBackup="false"
android:usesCleartextTraffic="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.FastPixJavaSample">

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- Keeps the upload running while the app is backgrounded. -->
<service
android:name=".UploadService"
android:exported="false"
android:foregroundServiceType="dataSync" />

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.fastpix.uploads.java;

// Credentials come from local.properties (see README). In production, sign uploads on your
// backend so the secret key never ships in the APK.
public final class Config {
private Config() {}

public static final String TOKEN = BuildConfig.FASTPIX_TOKEN;
public static final String SECRET_KEY = BuildConfig.FASTPIX_SECRET_KEY;

/** 8 MiB — must be a multiple of 256 KiB (GCS requirement, enforced by the SDK). */
public static final long CHUNK_SIZE = 8L * 1024 * 1024;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.fastpix.uploads.java;

import android.os.Handler;
import android.os.Looper;
import android.util.Base64;

import androidx.annotation.NonNull;

import org.json.JSONObject;

import java.io.IOException;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

// Calls the FastPix API to create an upload; delivers the result on the main thread.
public final class FastPixApi {
private FastPixApi() {}

public interface UrlCallback {
void onUrl(String url);
void onError(String message);
}

private static final String ENDPOINT = "https://api.fastpix.com/v1/on-demand/upload";
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
private static final String BODY =
"{\"corsOrigin\":\"*\",\"pushMediaSettings\":{\"accessPolicy\":\"public\",\"maxResolution\":\"2160p\"}}";

private static final OkHttpClient client = new OkHttpClient();
private static final Handler main = new Handler(Looper.getMainLooper());

public static void createUpload(UrlCallback cb) {
String credentials = Config.TOKEN + ":" + Config.SECRET_KEY;
String auth = "Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP);
Request request = new Request.Builder()
.url(ENDPOINT)
.header("Authorization", auth)
.header("Content-Type", "application/json")
.post(RequestBody.create(JSON, BODY))
.build();
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(@NonNull Call call, @NonNull IOException e) {
main.post(() -> cb.onError(e.getMessage()));
}

@Override public void onResponse(@NonNull Call call, @NonNull Response response) {
try (Response r = response) {
String body = r.body() != null ? r.body().string() : "";
if (!r.isSuccessful()) {
main.post(() -> cb.onError("HTTP " + r.code() + ": " + body));
return;
}
String url = new JSONObject(body).getJSONObject("data").getString("url");
main.post(() -> cb.onUrl(url));
} catch (Exception e) {
main.post(() -> cb.onError(e.getMessage()));
}
}
});
}
}
Loading