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}"