Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.2.1 / 2026-10-02

- Fixed the published POM, which declared no dependencies. Apps no longer have to add the Java SDK, the tus clients, and WorkManager by hand ([#23](https://github.com/transloadit/android-sdk/issues/23)).
- Assembly status polling now reads the returned `assembly_ssl_url` directly instead of asking the `SignatureProvider` to sign it, so a backend that only signs Assembly creation works with `waitForCompletion(true)`.
- WorkManager uploads succeed right away when the Assembly has already completed, and fail right away for aborted, canceled, or errored Assemblies instead of retrying. Failed work keeps the available Assembly identifiers in its output.
- The README's WorkManager example now uses a signing endpoint instead of the Auth Secret, and signing is documented as available since Java SDK 2.1.0.

## 0.2.0 / 2025-10-28

Below 1.0 SemVer allows us to make breaking changes, and we have shipped a number of them in this release, please review carefully.
Expand Down
31 changes: 21 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ implementation 'com.transloadit.android.sdk:transloadit-android:0.2.0'
</dependency>
```

> ℹ️ Signature-based authentication requires `com.transloadit.sdk:transloadit` version **2.2.3** or newer. When developing locally alongside the Java SDK, place both repositories next to each other (`../java-sdk`) and the Gradle build will automatically use the local java-sdk project via dependency substitution.
> ℹ️ Signature-based authentication was introduced in `com.transloadit.sdk:transloadit` version **2.1.0** ([java-sdk#95](https://github.com/transloadit/java-sdk/pull/95)). Android SDK 0.2 uses **2.2.4** for its updated SSE handling. When developing locally alongside the Java SDK, place both repositories next to each other (`../java-sdk`) and set `ANDROID_SDK_USE_LOCAL_JAVA_SDK=1` to use the local java-sdk project via dependency substitution.

## Usage

Expand Down Expand Up @@ -67,14 +67,20 @@ SignatureProvider signatureProvider = new SignatureProvider() {
HttpURLConnection conn = (HttpURLConnection) new URL("https://your-backend.com/sign").openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json");
conn.getOutputStream().write(paramsJson.getBytes());

// Read the signature from your backend's response
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String signature = reader.readLine();
reader.close();

return signature; // Should return something like "sha384:..."
// Read the complete JSON response from your backend
StringBuilder responseJson = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
char[] buffer = new char[1024];
int length;
while ((length = reader.read(buffer)) != -1) {
responseJson.append(buffer, 0, length);
}
}

return new JSONObject(responseJson.toString()).getString("signature"); // Returns "sha384:..."
}
};

Expand All @@ -88,7 +94,7 @@ Your backend should implement an endpoint that:
2. Signs the parameters using your Transloadit secret
3. Returns the signature

Example backend implementation (Node.js):
Example backend implementation (Node.js), with `req.body` containing the unchanged UTF-8 request text supplied by your text/raw-body middleware:

```javascript
const crypto = require('crypto')
Expand All @@ -100,7 +106,7 @@ app.post('/sign', authenticate, (req, res) => {
.update(Buffer.from(paramsJson, 'utf-8'))
.digest('hex')

res.send(`sha384:${signature}`)
res.json({ signature: `sha384:${signature}` })
})
```

Expand Down Expand Up @@ -214,15 +220,20 @@ assembly.useDirectCallbacks(); // run callbacks on the calling thread

```java
AndroidAssemblyWorkConfig config = AndroidAssemblyWorkConfig
.newBuilder("TRANSLOADIT_KEY", "TRANSLOADIT_SECRET")
.newBuilder("TRANSLOADIT_KEY")
.signatureProvider("https://your-backend.com/sign")
.addSignatureProviderHeader("Authorization", "Bearer " + sessionToken)
.paramsJson(paramsJsonString)
.preferenceName("my_transloadit_store")
.addFile(new File(context.getCacheDir(), "photo.jpg"), "image")
.waitForCompletion(true)
.build();

WorkManager.getInstance(context).enqueue(config.toWorkRequest());
```

The WorkManager signing endpoint must return a JSON object with a `signature` field, as in the backend example above. The session token must be valid when the worker requests the signature.

`AndroidAssemblyUploadWorker` waits for uploads (and, optionally, SSE completion) on a background thread so your app can survive process death or move long-running jobs out of the foreground.

## Example
Expand Down
52 changes: 46 additions & 6 deletions transloadit-android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
plugins {
id 'maven-publish'
id 'signing'
}

Expand Down Expand Up @@ -40,16 +41,20 @@ android {
targetCompatibility JavaVersion.VERSION_11
}
namespace 'com.transloadit.sdk'
publishing {
singleVariant('release')
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.transloadit.sdk:transloadit:2.2.4'
api 'com.transloadit.sdk:transloadit:2.2.4'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'io.tus.android.client:tus-android-client:0.1.10'
implementation 'io.tus.java.client:tus-java-client:0.5.1'
implementation 'org.jetbrains:annotations:23.0.0'
implementation 'androidx.work:work-runtime:2.9.0'
api 'androidx.work:work-runtime:2.9.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:4.8.0'
testImplementation 'org.mock-server:mockserver-junit-rule:5.15.0'
Expand Down Expand Up @@ -119,16 +124,13 @@ publishing {
publications {
androidRelease(MavenPublication) {
afterEvaluate {
from components.findByName('release')
from components.release
groupId project.getGroup()
artifactId 'transloadit-android'
description project.getDescription()
version project.getVersion()
artifact sourcesJar
artifact javadocJar
artifact (project.layout.buildDirectory.dir('outputs/aar/transloadit-android-release.aar')) {
builtBy assemble
}
pom.withXml {
def root = asNode()
root.appendNode('description', project.getDescription())
Expand All @@ -146,3 +148,41 @@ signing {
useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
sign publishing.publications.androidRelease
}

afterEvaluate {
def generatePom = tasks.named('generatePomFileForAndroidReleasePublication')
def verifyPublishedPom = tasks.register('verifyPublishedPom') {
group = 'verification'
description = 'Checks the generated release POM includes all runtime dependencies.'
dependsOn generatePom
doLast {
def pom = new XmlSlurper().parse(generatePom.get().destination)
def actual = pom.dependencies.dependency.collect {
[it.groupId.text(), it.artifactId.text(), it.version.text(), it.scope.text()]
}.toSet()
def expected = configurations.api.dependencies
.withType(ExternalModuleDependency).collect {
[it.group, it.name, it.version, 'compile']
}.toSet()
expected.addAll(configurations.implementation.dependencies
.withType(ExternalModuleDependency).collect {
[it.group, it.name, it.version, 'runtime']
})
if (actual != expected) {
throw new GradleException("Published POM dependencies differ: expected ${expected}, got ${actual}")
}
if (pom.packaging.text() != 'aar') {
throw new GradleException('Published POM must declare aar packaging')
}
def aarArtifacts = publishing.publications.androidRelease.artifacts.findAll {
it.extension == 'aar' && !it.classifier
}
if (aarArtifacts.size() != 1) {
throw new GradleException('Release publication must contain exactly one unclassified AAR')
}
}
}
tasks.named('check') {
dependsOn verifyPublishedPom
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@
return handleFailure(e);
}

if (config.shouldWaitForCompletion()) {
// A finished creation response does not start SSE or emit a completion callback.
if (config.shouldWaitForCompletion() && !initial.isFinished()) {
try {
boolean finished = completionLatch.await(config.getCompletionTimeoutMillis(), TimeUnit.MILLISECONDS);
if (!finished) {
Expand All @@ -180,6 +181,14 @@

AssemblyResponse finalResponse = completionResponse.get() != null ? completionResponse.get() : initial;
JSONObject json = finalResponse.json();
if (finalResponse.isFinished() && !finalResponse.isCompleted()) {
return Result.failure(new Data.Builder()
.putString("error", json.optString("error", json.optString("ok", "Assembly did not complete")))
.putString(OUTPUT_ASSEMBLY_ID, json.optString("assembly_id", null))
.putString(OUTPUT_ASSEMBLY_URL, json.optString("assembly_url", null))
.putString(OUTPUT_SSL_URL, json.optString("assembly_ssl_url", null))
.build());
}
Data output = new Data.Builder()
.putString(OUTPUT_ASSEMBLY_ID, finalResponse.getId())
.putString(OUTPUT_ASSEMBLY_URL, json.optString("assembly_url"))
Expand Down Expand Up @@ -277,7 +286,7 @@
/**
* Factory for creating assemblies; overridden in tests.
*/
protected AndroidAssembly createAssembly(AndroidTransloadit transloadit, AndroidAssemblyListener listener) {

Check warning on line 289 in transloadit-android/src/main/java/com/transloadit/android/sdk/AndroidAssemblyUploadWorker.java

View workflow job for this annotation

GitHub Actions / Gradle (Java 17)

Check warning on line 289 in transloadit-android/src/main/java/com/transloadit/android/sdk/AndroidAssemblyUploadWorker.java

View workflow job for this annotation

GitHub Actions / Gradle (Java 17)

no @PARAM for listener

Check warning on line 289 in transloadit-android/src/main/java/com/transloadit/android/sdk/AndroidAssemblyUploadWorker.java

View workflow job for this annotation

GitHub Actions / Gradle (Java 17)

no @PARAM for transloadit
return transloadit.newAssembly(listener, getApplicationContext());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@
import androidx.annotation.Nullable;

import com.transloadit.sdk.SignatureProvider;
import com.transloadit.sdk.exceptions.LocalOperationException;
import com.transloadit.sdk.exceptions.RequestException;
import com.transloadit.sdk.response.AssemblyResponse;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.Random;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
* Android-friendly extension of the core {@link com.transloadit.sdk.Transloadit} client.
*/
public class AndroidTransloadit extends com.transloadit.sdk.Transloadit {
private static final OkHttpClient ASSEMBLY_HTTP_CLIENT = new OkHttpClient();

/**
* Creates a client using inline credentials and a custom host.
Expand Down Expand Up @@ -102,6 +111,44 @@ public AndroidAssembly newAssembly(AndroidAssemblyListener listener, Context con
return new AndroidAssembly(this, listener, context);
}

/**
* Fetches status directly from an Assembly's {@code assembly_ssl_url}, which needs no signature.
*
* @param url Assembly status URL, absolute or relative to the configured host
* @return current Assembly status
* @throws RequestException if the status request fails
* @throws LocalOperationException if reading the response or waiting to retry fails
*/
@Override
public AssemblyResponse getAssemblyByUrl(String url) throws RequestException, LocalOperationException {
String fullUrl = url.startsWith("https://") || url.startsWith("http://") ? url : getHostUrl() + url;
Request request = new Request.Builder().url(fullUrl)
.header("Transloadit-Client", versionInfo).build();
int retriesLeft = getRetryAttemptsRequestException();
while (true) {
try (Response response = ASSEMBLY_HTTP_CLIENT.newCall(request).execute()) {
return new AssemblyResponse(response);
} catch (IOException e) {
boolean qualified = false;
for (String error : getQualifiedErrorsForRetry()) {
if (e.toString().contains(error)) {
qualified = true;
break;
}
}
if (retriesLeft-- <= 0 || !qualified) {
throw new RequestException(e);
}
try {
Thread.sleep(getRetryDelay() + new Random().nextInt(1000));
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new LocalOperationException(interrupted);
}
}
}
}

/**
* Internal helper used by tests to inspect the configured API key.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version="0.2.0"
version="0.2.1"
description="An Android Integration of the Transloadit's(https://transloadit.com) file uploading and encoding service."
group='com.transloadit.android.sdk'
Loading
Loading