From cc0412ec9e60d0ca59af3dc7cdf80788f8440586 Mon Sep 17 00:00:00 2001 From: pjgowtham Date: Fri, 8 May 2026 18:46:07 +0530 Subject: [PATCH 1/6] Updater: Fix duplicated and truncated date for local updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When importing a local update, the date was appended to the version string. This caused the date to appear twice in the UI—once in the title and again on the line below. The change removes the appended date so that truncated text is not shown. Change-Id: Ifd206242be8b7485a3845703f17bb3f285a519fd --- app/src/main/java/org/lineageos/updater/UpdateImporter.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/org/lineageos/updater/UpdateImporter.java b/app/src/main/java/org/lineageos/updater/UpdateImporter.java index a557851b..3aab6d7a 100644 --- a/app/src/main/java/org/lineageos/updater/UpdateImporter.java +++ b/app/src/main/java/org/lineageos/updater/UpdateImporter.java @@ -24,7 +24,6 @@ import android.util.Log; import org.lineageos.updater.controller.UpdaterController; -import org.lineageos.updater.misc.StringGenerator; import org.lineageos.updater.misc.Utils; import org.lineageos.updater.model.Update; import org.lineageos.updater.model.UpdateStatus; @@ -36,7 +35,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.text.DateFormat; import java.util.Enumeration; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -143,8 +141,6 @@ private File importFile(Uri uri) throws IOException { private Update buildLocalUpdate(File file) { final long timeStamp = getTimeStamp(file); - final String buildDate = StringGenerator.getDateLocalizedUTC( - activity, DateFormat.MEDIUM, timeStamp); final String name = activity.getString(R.string.local_update_name); final Update update = new Update(); update.setAvailableOnline(false); @@ -155,7 +151,7 @@ private Update buildLocalUpdate(File file) { update.setTimestamp(timeStamp); update.setStatus(UpdateStatus.VERIFIED); update.setPersistentStatus(UpdateStatus.Persistent.VERIFIED); - update.setVersion(String.format("%s (%s)", name, buildDate)); + update.setVersion(name); return update; } From 238a0b39ed0ba351ce1af95cb8b24ec7113edd32 Mon Sep 17 00:00:00 2001 From: pjgowtham Date: Sun, 5 Oct 2025 18:53:18 +0530 Subject: [PATCH 2/6] Updater: Fix pending reboot state not shown When the 'Auto delete updates' preference is enabled or with local updates, the updater removes the update record from the database immediately after a successful installation. This leads to pending reboot state not shown. To resolve this, this change defers the automatic deletion of the update record until after the device has rebooted successfully. Change-Id: I3f4037cbd8f4fd5145d14ab7301b0e99913ddbe0 --- .../lineageos/updater/UpdaterReceiver.java | 25 +++++++---- .../updater/controller/UpdaterService.java | 41 +++++++++++++++---- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/lineageos/updater/UpdaterReceiver.java b/app/src/main/java/org/lineageos/updater/UpdaterReceiver.java index 1fb2c5a0..25e16ff2 100644 --- a/app/src/main/java/org/lineageos/updater/UpdaterReceiver.java +++ b/app/src/main/java/org/lineageos/updater/UpdaterReceiver.java @@ -28,6 +28,7 @@ import androidx.core.app.NotificationCompat; import androidx.preference.PreferenceManager; +import org.lineageos.updater.controller.UpdaterService; import org.lineageos.updater.misc.BuildInfoUtils; import org.lineageos.updater.misc.Constants; import org.lineageos.updater.misc.StringGenerator; @@ -42,18 +43,15 @@ public class UpdaterReceiver extends BroadcastReceiver { private static final String INSTALL_ERROR_NOTIFICATION_CHANNEL = "install_error_notification_channel"; - private static boolean shouldShowUpdateFailedNotification(Context context) { + private static boolean isUpdateSuccessful(Context context) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - // We can't easily detect failed re-installations - if (preferences.getBoolean(Constants.PREF_INSTALL_AGAIN, false) || - preferences.getBoolean(Constants.PREF_INSTALL_NOTIFIED, false)) { - return false; - } - long buildTimestamp = SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0); long lastBuildTimestamp = preferences.getLong(Constants.PREF_INSTALL_OLD_TIMESTAMP, -1); - return buildTimestamp == lastBuildTimestamp; + // We can't easily detect failed re-installations. + boolean isReinstall = preferences.getBoolean(Constants.PREF_INSTALL_AGAIN, false); + + return isReinstall || buildTimestamp != lastBuildTimestamp; } private static void showUpdateFailedNotification(Context context) { @@ -91,9 +89,18 @@ public void onReceive(Context context, Intent intent) { pm.reboot(null); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); + String downloadId = pref.getString(Constants.PREF_NEEDS_REBOOT_ID, null); pref.edit().remove(Constants.PREF_NEEDS_REBOOT_ID).apply(); - if (shouldShowUpdateFailedNotification(context)) { + if (downloadId != null && isUpdateSuccessful(context)) { + Intent cleanupIntent = new Intent(context, UpdaterService.class); + cleanupIntent.setAction(UpdaterService.ACTION_POST_REBOOT_CLEANUP); + cleanupIntent.putExtra(UpdaterService.EXTRA_DOWNLOAD_ID, downloadId); + context.startService(cleanupIntent); + } + + if (!pref.getBoolean(Constants.PREF_INSTALL_NOTIFIED, false) + && !isUpdateSuccessful(context)) { pref.edit().putBoolean(Constants.PREF_INSTALL_NOTIFIED, true).apply(); showUpdateFailedNotification(context); } diff --git a/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java b/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java index ab8657f1..4441ccc2 100644 --- a/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java +++ b/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java @@ -62,6 +62,8 @@ public class UpdaterService extends Service { public static final String ACTION_INSTALL_SUSPEND = "action_install_suspend"; public static final String ACTION_INSTALL_RESUME = "action_install_resume"; + public static final String ACTION_POST_REBOOT_CLEANUP = "action_post_reboot_cleanup"; + private static final String ONGOING_NOTIFICATION_CHANNEL = "ongoing_notification_channel"; @@ -181,6 +183,10 @@ public int onStartCommand(Intent intent, int flags, int startId) { mUpdaterController); installer.reconnect(); } + } else if (ACTION_POST_REBOOT_CLEANUP.equals(intent.getAction())) { + String downloadId = intent.getStringExtra(EXTRA_DOWNLOAD_ID); + handlePostRebootCleanup(downloadId); + tryStopSelf(); } else if (ACTION_DOWNLOAD_CONTROL.equals(intent.getAction())) { String downloadId = intent.getStringExtra(EXTRA_DOWNLOAD_ID); int action = intent.getIntExtra(EXTRA_DOWNLOAD_CONTROL, -1); @@ -194,6 +200,10 @@ public int onStartCommand(Intent intent, int flags, int startId) { } else if (ACTION_INSTALL_UPDATE.equals(intent.getAction())) { String downloadId = intent.getStringExtra(EXTRA_DOWNLOAD_ID); UpdateInfo update = mUpdaterController.getUpdate(downloadId); + if (update == null) { + Log.e(TAG, "Update not found: " + downloadId); + return START_NOT_STICKY; + } if (update.getPersistentStatus() != UpdateStatus.Persistent.VERIFIED) { throw new IllegalArgumentException(update.getDownloadId() + " is not verified"); } @@ -411,14 +421,6 @@ private void handleUpdateStatusChange(UpdateInfo update) { mNotificationBuilder.setAutoCancel(true); mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); - boolean deleteUpdate = pref.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, false); - boolean isLocal = Update.LOCAL_ID.equals(update.getDownloadId()); - // Always delete local updates - if (deleteUpdate || isLocal) { - mUpdaterController.deleteUpdate(update.getDownloadId()); - } - tryStopSelf(); break; } @@ -540,4 +542,27 @@ private PendingIntent getResumeInstallationPendingIntent() { return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } + + private void handlePostRebootCleanup(String downloadId) { + if (downloadId == null) { + return; + } + + UpdateInfo update = mUpdaterController.getUpdate(downloadId); + if (update == null) { + Log.w(TAG, "Update not found during post-reboot cleanup: " + downloadId); + return; + } + + Log.d(TAG, "Post-reboot cleanup for: " + downloadId); + + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); + boolean deleteUpdate = pref.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, false); + + // Always delete local updates + boolean isLocal = Update.LOCAL_ID.equals(downloadId); + if (deleteUpdate || isLocal) { + mUpdaterController.deleteUpdate(downloadId); + } + } } From 778c27a73317324c113304826d1e8e5ff32a5e4c Mon Sep 17 00:00:00 2001 From: pjgowtham Date: Tue, 24 Mar 2026 21:05:25 +0530 Subject: [PATCH 3/6] Updater: Clear notification actions on download completion The VERIFIED and VERIFICATION_FAILED notification states were missing mNotificationBuilder.mActions.clear(), causing pause and cancel buttons added during DOWNLOADING to persist in the completion notification. All other states (PAUSED, PAUSED_ERROR, VERIFYING, INSTALLING) already cleared actions correctly. Change-Id: Ib7003781a363c738ea7bc124b23a2253d60add4f --- .../java/org/lineageos/updater/controller/UpdaterService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java b/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java index 4441ccc2..1c2adb4a 100644 --- a/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java +++ b/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java @@ -356,6 +356,7 @@ private void handleUpdateStatusChange(UpdateInfo update) { } case VERIFIED: { stopForeground(STOP_FOREGROUND_DETACH); + mNotificationBuilder.mActions.clear(); mNotificationBuilder.setStyle(null); mNotificationBuilder.setSmallIcon(R.drawable.ic_system_update); mNotificationBuilder.setProgress(0, 0, false); @@ -370,6 +371,7 @@ private void handleUpdateStatusChange(UpdateInfo update) { } case VERIFICATION_FAILED: { stopForeground(STOP_FOREGROUND_DETACH); + mNotificationBuilder.mActions.clear(); mNotificationBuilder.setStyle(null); mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_warning); mNotificationBuilder.setProgress(0, 0, false); From 4672b7aaa24e117723b655e263db4547322e8210 Mon Sep 17 00:00:00 2001 From: pjgowtham Date: Wed, 8 Apr 2026 21:29:17 +0530 Subject: [PATCH 4/6] Updater: Increase download chunk size to 32 KiB Replace the inline 8192-byte buffer with a named CHUNK_SIZE constant (32 * 1024), matching the value used by Firefox Android's AbstractFetchDownloadService. Larger chunks reduce syscall overhead during gigabyte-scale OTA downloads. Change-Id: Ia735770a0591e76dd8f3d216e73da21e6a54a2e0 --- .../lineageos/updater/download/HttpURLConnectionClient.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java b/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java index 63067616..c6525169 100644 --- a/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java +++ b/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java @@ -36,6 +36,9 @@ public class HttpURLConnectionClient implements DownloadClient { private final static String TAG = "HttpURLConnectionClient"; + // Ref: mozilla-mobile/firefox-android AbstractFetchDownloadService.CHUNK_SIZE + private static final int CHUNK_SIZE = 32 * 1024; + private HttpURLConnection mClient; private final File mDestination; @@ -283,7 +286,7 @@ public void run() { OutputStream outputStream = new FileOutputStream(mDestination, mResume) ) { mTotalBytes = mClient.getContentLengthLong() + mTotalBytesRead; - byte[] b = new byte[8192]; + byte[] b = new byte[CHUNK_SIZE]; int count; while (!isInterrupted() && (count = inputStream.read(b)) > 0) { outputStream.write(b, 0, count); From 5267c86ffc199709cd238dbf8f99efec7f5200ff Mon Sep 17 00:00:00 2001 From: pjgowtham Date: Wed, 8 Apr 2026 22:39:18 +0530 Subject: [PATCH 5/6] Updater: apply smoothSeconds() to ETA calculation Port Firefox's asymmetric exponential smoothing to calculateEta(). Trust 30% of a decrease and only 10% of an increase, with a nudge for tiny changes to keep forward progress visible. Reset mLastEta on resume alongside mSpeed. Floor at 1s while still downloading. Ref: mozilla-central DownloadsCommon.sys.mjs smoothSeconds() Change-Id: Ic5efc626270e021847cb8f8a07b07d3ec1564d36 --- .../download/HttpURLConnectionClient.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java b/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java index c6525169..a2f39434 100644 --- a/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java +++ b/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java @@ -136,6 +136,7 @@ private class DownloadThread extends Thread { private long mLastMillis = 0; private long mSpeed = -1; private long mEta = -1; + private double mLastEta = -1; private final boolean mResume; @@ -150,6 +151,7 @@ private void calculateSpeed(boolean justResumed) { // ETA since the delta will grow, resulting in a very low speed mLastMillis = millis; mSpeed = -1; // we don't want the moving avg with values from who knows when + mLastEta = -1; // reset smoothed ETA so the first post-resume value is accepted as-is // need to do this as well, otherwise the second time we call calculateSpeed(), // the difference (mTotalBytesRead - mCurSampleBytes) will be larger than expected, @@ -171,10 +173,31 @@ private void calculateSpeed(boolean justResumed) { } } + // Ref: mozilla-central DownloadsCommon.sys.mjs smoothSeconds() private void calculateEta() { - if (mSpeed > 0) { - mEta = (mTotalBytes - mTotalBytesRead) / mSpeed; + if (mSpeed <= 0) return; + + double rawSeconds = (double) (mTotalBytes - mTotalBytesRead) / mSpeed; + + // Apply smoothing only when the new value is more than half the previous; + // large drops (e.g. after resume) are accepted immediately. + if (mLastEta >= 0 && rawSeconds > mLastEta / 2) { + double diff = rawSeconds - mLastEta; + // Asymmetric: trust 30% of a decrease, only 10% of an increase. + rawSeconds = mLastEta + (diff < 0 ? 0.3 : 0.1) * diff; + + // If the change is tiny (< 5 s or < 5%), nudge by a small amount + // so the display shows forward progress rather than freezing. + diff = rawSeconds - mLastEta; + double diffPct = (diff / mLastEta) * 100; + if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5) { + rawSeconds = mLastEta - (diff < 0 ? 0.4 : 0.2); + } } + + // Never show zero seconds while still downloading. + mLastEta = Math.max(rawSeconds, 1.0); + mEta = (long) mLastEta; } private void changeClientUrl(URL newUrl) throws IOException { From 1e33aa1d6d293110a90016f8d3bfdc0021633e0e Mon Sep 17 00:00:00 2001 From: pjgowtham Date: Sat, 11 Apr 2026 09:33:58 +0530 Subject: [PATCH 6/6] Updater: Restrict duplicate link URL capture to exclude '>' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `.+` with `[^>]+` to stop the URL capture group from matching past the closing `>` delimiter. Example: Input: ; rel=duplicate; pri=1 `.+` → can match `https://example.com/file.zip>` `[^>]+` → correctly matches `https://example.com/file.zip` Change-Id: I9ae5f537cebfdb0da43069351925758507297120 --- .../org/lineageos/updater/download/HttpURLConnectionClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java b/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java index a2f39434..7042949d 100644 --- a/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java +++ b/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java @@ -230,7 +230,7 @@ private DuplicateLink(String url, int priority) { // https://tools.ietf.org/html/rfc6249 // https://tools.ietf.org/html/rfc5988#section-5 - String regex = "(?i)<(.+)>\\s*;\\s*rel=duplicate(?:.*pri=([0-9]+).*|.*)?"; + String regex = "(?i)<([^>]+)>\\s*;\\s*rel=duplicate(?:.*pri=([0-9]+).*|.*)?"; Pattern pattern = Pattern.compile(regex); for (String field : entry.getValue()) { Matcher matcher = pattern.matcher(field);