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
5 changes: 5 additions & 0 deletions .changeset/odd-pianos-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@capawesome/capacitor-live-update": minor
---

feat(android): support Brotli-compressed ZIP artifacts (.br)
4 changes: 2 additions & 2 deletions packages/live-update/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ android {

If needed, you can define the following project variable in your app’s `variables.gradle` file to change the default version of the dependency:

- `$okhttp3Version` version of `com.squareup.okhttp3:okhttp` (default: `5.3.2`)
- `$okhttp3Version` version of `com.squareup.okhttp3:okhttp` and `com.squareup.okhttp3:okhttp-brotli` (default: `5.3.2`)
- `$zip4jVersion` version of `net.lingala.zip4j:zip4j` (default: `2.11.5`)

This can be useful if you encounter dependency conflicts with other plugins in your project.
Expand Down Expand Up @@ -963,7 +963,7 @@ Remove all listeners for this plugin.
| **`bundleId`** | <code>string</code> | The unique identifier of the bundle. **Attention**: The value `public` is reserved and cannot be used as a bundle identifier. | | 5.0.0 |
| **`checksum`** | <code>string</code> | The checksum of the self-hosted bundle as a SHA-256 hash in hex format to verify the integrity of the bundle. **Attention**: Only supported for the `zip` artifact type. | | 7.1.0 |
| **`signature`** | <code>string</code> | The signature of the self-hosted bundle as a signed SHA-256 hash in base64 format to verify the integrity of the bundle. **Attention**: Only supported for the `zip` artifact type. | | 7.1.0 |
| **`url`** | <code>string</code> | The URL of the bundle to download. For the `zip` artifact type, the URL must point to a ZIP file. For the `manifest` artifact type, the URL serves as the base URL to download the individual files. For example, if the URL is `https://example.com/download`, the plugin will download the file with the href `index.html` from `https://example.com/download?href=index.html`. To **verify the integrity** of the file, the server should return a `X-Checksum` header with the SHA-256 hash in hex format. To **verify the signature** of the file, the server should return a `X-Signature` header with the signed SHA-256 hash in base64 format. | | 5.0.0 |
| **`url`** | <code>string</code> | The URL of the bundle to download. For the `zip` artifact type, the URL must point to a ZIP file. On Android, it can also point to a Brotli-compressed ZIP file (`.br`). For the `manifest` artifact type, the URL serves as the base URL to download the individual files. For example, if the URL is `https://example.com/download`, the plugin will download the file with the href `index.html` from `https://example.com/download?href=index.html`. To **verify the integrity** of the file, the server should return a `X-Checksum` header with the SHA-256 hash in hex format. To **verify the signature** of the file, the server should return a `X-Signature` header with the signed SHA-256 hash in base64 format. Bundle downloads also support Brotli-compressed HTTP responses (`Content-Encoding: br`). | | 5.0.0 |


#### FetchChannelsResult
Expand Down
1 change: 1 addition & 0 deletions packages/live-update/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "net.lingala.zip4j:zip4j:$zip4jVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttp3Version"
implementation "com.squareup.okhttp3:okhttp-brotli:$okhttp3Version"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
import okio.Buffer;
import okio.BufferedSource;
import okio.Okio;
import org.brotli.dec.BrotliInputStream;
import org.json.JSONArray;
import org.json.JSONObject;

Expand Down Expand Up @@ -674,6 +675,17 @@ private void deleteFileRecursively(@NonNull File file) {
file.delete();
}

private void decompressBrotliFile(@NonNull File compressedFile, @NonNull File destinationFile) throws IOException {
try (BrotliInputStream input = new BrotliInputStream(new FileInputStream(compressedFile));
FileOutputStream output = new FileOutputStream(destinationFile)) {
byte[] buffer = new byte[8192];
int length;
while ((length = input.read(buffer)) != -1) {
output.write(buffer, 0, length);
}
}
}

private void deleteUnusedBundles() {
String[] bundleIds = getDownloadedBundleIds();
for (String bundleId : bundleIds) {
Expand Down Expand Up @@ -932,11 +944,11 @@ private void downloadBundleOfTypeZip(
@NonNull String downloadUrl,
@NonNull EmptyCallback completionCallback
) {
File file = buildTemporaryZipFile();
File downloadedFile = buildTemporaryZipFile();
// Download the bundle
downloadAndVerifyFile(
downloadUrl,
file,
downloadedFile,
checksum,
signature,
(downloadedBytes, totalBytes) -> {
Expand All @@ -946,21 +958,31 @@ private void downloadBundleOfTypeZip(
new EmptyCallback() {
@Override
public void success() {
File fileToExtract = downloadedFile;
File decompressedFile = null;
try {
if (downloadUrl.toLowerCase().endsWith(".br")) {
decompressedFile = buildTemporaryZipFile();
decompressBrotliFile(downloadedFile, decompressedFile);
fileToExtract = decompressedFile;
}
// Add the bundle
addBundleOfTypeZip(bundleId, file);
// Delete the temporary file
file.delete();
addBundleOfTypeZip(bundleId, fileToExtract);
completionCallback.success();
} catch (Exception e) {
completionCallback.error(e);
} finally {
downloadedFile.delete();
if (decompressedFile != null) {
decompressedFile.delete();
}
}
}

@Override
public void error(@NonNull Exception exception) {
// Delete the temporary file on error
file.delete();
downloadedFile.delete();
completionCallback.error(exception);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.brotli.BrotliInterceptor;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
Expand Down Expand Up @@ -59,6 +60,7 @@ public LiveUpdateHttpClient(@NonNull LiveUpdateConfig config) {

this.okHttpClient = new OkHttpClient.Builder()
.dispatcher(dispatcher)
.addInterceptor(BrotliInterceptor.INSTANCE)
.connectTimeout(httpTimeout, TimeUnit.MILLISECONDS)
.readTimeout(httpTimeout, TimeUnit.MILLISECONDS)
.writeTimeout(httpTimeout, TimeUnit.MILLISECONDS)
Expand Down