diff --git a/dewemoji-capacitor/android/app/build.gradle b/dewemoji-capacitor/android/app/build.gradle
index cc21dd0..b1de05d 100644
--- a/dewemoji-capacitor/android/app/build.gradle
+++ b/dewemoji-capacitor/android/app/build.gradle
@@ -7,8 +7,8 @@ android {
applicationId "com.dewemoji.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 1
- versionName "1.0"
+ versionCode 3
+ versionName "1.0.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/dewemoji-capacitor/android/app/src/main/AndroidManifest.xml b/dewemoji-capacitor/android/app/src/main/AndroidManifest.xml
index e8f2973..340e7df 100644
--- a/dewemoji-capacitor/android/app/src/main/AndroidManifest.xml
+++ b/dewemoji-capacitor/android/app/src/main/AndroidManifest.xml
@@ -38,5 +38,4 @@
-
diff --git a/dewemoji-capacitor/android/app/src/main/java/com/dewemoji/app/MainActivity.java b/dewemoji-capacitor/android/app/src/main/java/com/dewemoji/app/MainActivity.java
index ddcb914..cb171b3 100644
--- a/dewemoji-capacitor/android/app/src/main/java/com/dewemoji/app/MainActivity.java
+++ b/dewemoji-capacitor/android/app/src/main/java/com/dewemoji/app/MainActivity.java
@@ -1,26 +1,16 @@
package com.dewemoji.app;
import android.app.AlertDialog;
-import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.DialogInterface;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
-import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
-import androidx.annotation.Nullable;
-import androidx.core.content.FileProvider;
-import androidx.core.content.ContextCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
@@ -35,41 +25,20 @@ import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.util.Locale;
public class MainActivity extends BridgeActivity {
private static final String TAG = "DewemojiUpdater";
private static final String VERSION_URL = "https://dewemoji.com/downloads/version.json";
- private static final int CONNECT_TIMEOUT_MS = 10_000;
- private static final int READ_TIMEOUT_MS = 15_000;
-
- @Nullable
- private DownloadManager downloadManager;
- private long activeDownloadId = -1L;
- @Nullable
- private String activeExpectedSha = null;
- @Nullable
- private BroadcastReceiver downloadReceiver = null;
+ private static final int CONNECT_TIMEOUT_MS = 10000;
+ private static final int READ_TIMEOUT_MS = 15000;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hideSystemBars();
- downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
- registerDownloadReceiver();
checkForUpdates(false);
}
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (downloadReceiver != null) {
- unregisterReceiver(downloadReceiver);
- downloadReceiver = null;
- }
- }
-
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
@@ -95,6 +64,7 @@ public class MainActivity extends BridgeActivity {
if (metadata == null) {
return;
}
+
long installedVersion = getInstalledVersionCode();
if (metadata.versionCode <= installedVersion) {
if (manual) {
@@ -104,6 +74,7 @@ public class MainActivity extends BridgeActivity {
}
return;
}
+
runOnUiThread(() -> showUpdateDialog(metadata));
} catch (Exception ex) {
Log.w(TAG, "Update check failed", ex);
@@ -116,7 +87,6 @@ public class MainActivity extends BridgeActivity {
}).start();
}
- @Nullable
private UpdateMetadata fetchVersionMetadata() throws Exception {
HttpURLConnection conn = (HttpURLConnection) new URL(VERSION_URL).openConnection();
conn.setRequestMethod("GET");
@@ -159,14 +129,11 @@ public class MainActivity extends BridgeActivity {
if (!metadata.notes.isEmpty()) {
message.append("\n\n").append(metadata.notes);
}
- if (metadata.force) {
- message.append("\n\nThis update is required.");
- }
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle("Update Dewemoji")
.setMessage(message.toString())
- .setPositiveButton("Update", (dialog, which) -> startApkDownload(metadata))
+ .setPositiveButton("Download", (dialog, which) -> openApkDownloadPage(metadata.apkUrl))
.setCancelable(!metadata.force);
if (!metadata.force) {
@@ -176,150 +143,23 @@ public class MainActivity extends BridgeActivity {
builder.show();
}
- private void startApkDownload(UpdateMetadata metadata) {
- if (downloadManager == null) {
- Toast.makeText(this, "Download manager unavailable", Toast.LENGTH_SHORT).show();
- return;
- }
-
- Uri uri = Uri.parse(metadata.apkUrl);
- DownloadManager.Request request = new DownloadManager.Request(uri);
- request.setTitle("Dewemoji update");
- request.setDescription("Downloading version " + metadata.versionName);
- request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
- request.setDestinationInExternalPublicDir(
- Environment.DIRECTORY_DOWNLOADS,
- "dewemoji-latest.apk"
- );
- request.setMimeType("application/vnd.android.package-archive");
-
- activeExpectedSha = metadata.sha256.toLowerCase(Locale.US);
- activeDownloadId = downloadManager.enqueue(request);
- Toast.makeText(this, "Downloading update...", Toast.LENGTH_SHORT).show();
- }
-
- private void registerDownloadReceiver() {
- downloadReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (!DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
- return;
- }
- long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L);
- if (downloadId <= 0 || downloadId != activeDownloadId) {
- return;
- }
- verifyAndInstallDownloadedApk(downloadId);
- }
- };
-
- ContextCompat.registerReceiver(
- this,
- downloadReceiver,
- new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
- ContextCompat.RECEIVER_NOT_EXPORTED
- );
- }
-
- private void verifyAndInstallDownloadedApk(long downloadId) {
- if (downloadManager == null) {
- return;
- }
-
- DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
- try (Cursor cursor = downloadManager.query(query)) {
- if (cursor == null || !cursor.moveToFirst()) {
- showUpdateError("Download record not found");
- return;
- }
-
- int statusCol = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
- int uriCol = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
- int reasonCol = cursor.getColumnIndex(DownloadManager.COLUMN_REASON);
- int status = statusCol >= 0 ? cursor.getInt(statusCol) : DownloadManager.STATUS_FAILED;
- String localUri = uriCol >= 0 ? cursor.getString(uriCol) : null;
- int reason = reasonCol >= 0 ? cursor.getInt(reasonCol) : -1;
-
- if (status != DownloadManager.STATUS_SUCCESSFUL || localUri == null || localUri.isEmpty()) {
- showUpdateError("Download failed (" + reason + ")");
- return;
- }
-
- Uri apkUri = Uri.parse(localUri);
- String localSha = computeSha256(apkUri);
- if (localSha == null || activeExpectedSha == null || !localSha.equalsIgnoreCase(activeExpectedSha)) {
- showUpdateError("Checksum mismatch");
- return;
- }
-
- installApk(apkUri);
- } catch (Exception ex) {
- Log.e(TAG, "Failed to verify update APK", ex);
- showUpdateError("Update verification failed");
- }
- }
-
- @Nullable
- private String computeSha256(Uri uri) {
- try (InputStream input = getContentResolver().openInputStream(uri)) {
- if (input == null) {
- return null;
- }
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
- byte[] buffer = new byte[8192];
- int read;
- while ((read = input.read(buffer)) > 0) {
- digest.update(buffer, 0, read);
- }
- byte[] bytes = digest.digest();
- StringBuilder out = new StringBuilder(bytes.length * 2);
- for (byte b : bytes) {
- out.append(String.format(Locale.US, "%02x", b));
- }
- return out.toString();
- } catch (Exception ex) {
- Log.e(TAG, "Failed to compute checksum", ex);
- return null;
- }
- }
-
- private void installApk(Uri downloadUri) {
+ private void openApkDownloadPage(String apkUrl) {
try {
- Uri installUri = downloadUri;
- if ("file".equals(downloadUri.getScheme())) {
- installUri = FileProvider.getUriForFile(
- this,
- getPackageName() + ".fileprovider",
- new java.io.File(downloadUri.getPath())
- );
- }
-
- Intent installIntent = new Intent(Intent.ACTION_VIEW);
- installIntent.setDataAndType(installUri, "application/vnd.android.package-archive");
- installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- startActivity(installIntent);
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkUrl));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
} catch (ActivityNotFoundException ex) {
- showUpdateError("No installer found");
+ Toast.makeText(this, "No browser found", Toast.LENGTH_SHORT).show();
} catch (Exception ex) {
- Log.e(TAG, "Failed to launch APK installer", ex);
- showUpdateError("Cannot open installer");
+ Log.e(TAG, "Failed to open APK URL", ex);
+ Toast.makeText(this, "Cannot open update URL", Toast.LENGTH_SHORT).show();
}
}
- private void showUpdateError(String message) {
- runOnUiThread(() -> new AlertDialog.Builder(this)
- .setTitle("Update failed")
- .setMessage(message)
- .setPositiveButton("OK", (DialogInterface dialog, int which) -> dialog.dismiss())
- .show());
- }
-
private static class UpdateMetadata {
final String versionName;
final long versionCode;
final String apkUrl;
- final String sha256;
final String notes;
final boolean force;
@@ -327,14 +167,12 @@ public class MainActivity extends BridgeActivity {
String versionName,
long versionCode,
String apkUrl,
- String sha256,
String notes,
boolean force
) {
this.versionName = versionName;
this.versionCode = versionCode;
this.apkUrl = apkUrl;
- this.sha256 = sha256;
this.notes = notes;
this.force = force;
}
@@ -343,15 +181,14 @@ public class MainActivity extends BridgeActivity {
String versionName = obj.optString("versionName", "");
long versionCode = obj.optLong("versionCode", 0);
String apkUrl = obj.optString("apkUrl", "");
- String sha256 = obj.optString("sha256", "");
String notes = obj.optString("notes", "");
boolean force = obj.optBoolean("force", false);
- if (versionName.isEmpty() || versionCode <= 0 || apkUrl.isEmpty() || sha256.isEmpty()) {
+ if (versionName.isEmpty() || versionCode <= 0 || apkUrl.isEmpty()) {
throw new IllegalStateException("Invalid version metadata payload");
}
- return new UpdateMetadata(versionName, versionCode, apkUrl, sha256, notes, force);
+ return new UpdateMetadata(versionName, versionCode, apkUrl, notes, force);
}
}
}
diff --git a/scripts/apk/publish-r2.sh b/scripts/apk/publish-r2.sh
index 77a21ba..5228419 100755
--- a/scripts/apk/publish-r2.sh
+++ b/scripts/apk/publish-r2.sh
@@ -20,6 +20,9 @@ Required env:
Optional env:
R2_PUBLIC_BASE_URL (example: https://downloads.dewemoji.com)
DEWEMOJI_APK_URL (default: https://dewemoji.com/downloads/dewemoji-latest.apk)
+ APK_VERSIONED_CACHE_CONTROL (default: public,max-age=31536000,immutable)
+ APK_LATEST_CACHE_CONTROL (default: no-store,max-age=0,must-revalidate)
+ APK_VERSION_JSON_CACHE_CONTROL (default: no-store,max-age=0,must-revalidate)
USAGE
}
@@ -82,6 +85,9 @@ latest_key="apk/dewemoji-latest.apk"
version_json_key="apk/version.json"
apk_url="${DEWEMOJI_APK_URL:-https://dewemoji.com/downloads/dewemoji-latest.apk}"
+versioned_cache_control="${APK_VERSIONED_CACHE_CONTROL:-public,max-age=31536000,immutable}"
+latest_cache_control="${APK_LATEST_CACHE_CONTROL:-no-store,max-age=0,must-revalidate}"
+version_json_cache_control="${APK_VERSION_JSON_CACHE_CONTROL:-no-store,max-age=0,must-revalidate}"
version_json_path="${tmp_dir}/version.json"
"${MAKE_VERSION_SCRIPT}" \
--version-name "${version_name}" \
@@ -94,13 +100,19 @@ version_json_path="${tmp_dir}/version.json"
--out "${version_json_path}"
echo "== Upload versioned APK =="
-aws --endpoint-url "${endpoint}" s3 cp "${apk}" "s3://${R2_BUCKET}/${versioned_key}" --content-type application/vnd.android.package-archive
+aws --endpoint-url "${endpoint}" s3 cp "${apk}" "s3://${R2_BUCKET}/${versioned_key}" \
+ --content-type application/vnd.android.package-archive \
+ --cache-control "${versioned_cache_control}"
echo "== Upload latest APK alias =="
-aws --endpoint-url "${endpoint}" s3 cp "${apk}" "s3://${R2_BUCKET}/${latest_key}" --content-type application/vnd.android.package-archive
+aws --endpoint-url "${endpoint}" s3 cp "${apk}" "s3://${R2_BUCKET}/${latest_key}" \
+ --content-type application/vnd.android.package-archive \
+ --cache-control "${latest_cache_control}"
echo "== Upload version metadata =="
-aws --endpoint-url "${endpoint}" s3 cp "${version_json_path}" "s3://${R2_BUCKET}/${version_json_key}" --content-type application/json --cache-control no-store
+aws --endpoint-url "${endpoint}" s3 cp "${version_json_path}" "s3://${R2_BUCKET}/${version_json_key}" \
+ --content-type application/json \
+ --cache-control "${version_json_cache_control}"
echo "Published to R2 bucket: ${R2_BUCKET}"
echo "Versioned APK key: ${versioned_key}"