- Remove double top padding on add-app and import-from-URL-list pages:
content area no longer adds MediaQuery.padding.top since the
AppBar/SliverAppBar already accounts for system bar insets.
- Fix source names showing raw translation keys in group headers and
filter dropdown. Eight sources used name = tr(key) in their
constructor, which resolved before translations loaded at startup.
Changed to @override String get name => tr(key) so resolution is
always lazy. Store sourceIdentifier (stable) instead of name in
AppInMemory.sourceType; resolve to display name at presentation
time via a lookup map built from SourceProvider.sources.
- Remove redundant crowdsourced-app-configs button from the settings
page app bar (an ActionChip on the add-app page already provides
the same link).
- Memoize findStandardFormatsForVersion with bounded 4096-entry cache,
and deduplicate the 436 generated version regex patterns. This
collapses the per-GitHub-app sort from ~500K regex evaluations to
~100, and caches results across all version reconciliation calls.
- Optimize GitHub _sortGitHubReleases: date sort (the default) now
skips regex computation entirely; smartname sorts precompute formats
once per release instead of re-evaluating per sort-comparison pair.
- Inline app list computation into AppsPage.build() instead of using
a three-layer caching system (context.select, pipelineSignature,
_computeListData). With memoization making rebuilds ~2ms, the
complexity of signature hashing and cache field management is
unnecessary.
- Cache getAppsDir() to avoid redundant platform-channel calls per
app during saveApps batches.
- Improved update check UI performance (#3043)
- Improved app list loading UI (#2666)
- Other minor UI tweaks
- Improve install result detection for external installers in foreground
- apps.dart: cache filtered/sorted/grouped app list results keyed by
pipeline signature, avoiding O(n) recomputation on every rebuild
when only progress ticks or selection changes fire
- add_app.dart: skip parent rebuild when URL text changes without
switching the detected source; track _urlValid separately so the
Add button correctly enables/disables; _prevValid prevents early
return from skipping setState when validity flips
- generated_form_renderer.dart: replace broken validateTextField
which couldn't find TextFormField inside TypeAheadField wrappers
with a _fieldKeys list for direct FormFieldState.isValid checks
- apps_provider_lifecycle.dart: preload app icons from disk cache
after initial loadApps() so scrolling never triggers disk I/O
All form-item list fields (additionalSourceAppSpecificSettingFormItems,
sourceConfigSettingFormItems, searchQuerySettingFormItems) and
_commonAppSettingFormItems now use getters or computed keys instead of
frozen tr() at construction time, so labels pick up the current locale
on every access via the renderer's tr() wrapping.
- _commonAppSettingFormItems: final field -> getter
- additionalSourceAppSpecificSettingFormItems: mutable field -> getter (12 sources)
- sourceConfigSettingFormItems: mutable field -> getter (GitHub, GitLab)
- searchQuerySettingFormItems: mutable field -> getter (GitHub, Codeberg)
- HTML's intermediateFormItems/commonFormItems/finalStepFormitems: var -> getters
- FDroidRepo's runOnAddAppInputChange: list mutation -> _appIdFromUrl bool
- Source names: tr('fdroid') etc. -> raw key string; display sites wrap with tr()
- completeInstallationNotification: module-level final -> getter
- GitHubStars.name/requiredArgs: fields -> getters; MassAppUrlSource -> abstract getters
- Removed memoization caches (_combinedItemsCache, _hasAppSpecificSettingsCache,
_flatCombinedFormItemsCache) now superseded by getter rebuilding
- Renderer: tr() wrapping on dropdown option values
- BG install polling: increase from 8s (16×500ms) to 10s (20×500ms).
Remove rollback of installedVersion on poll timeout — optimistic
pre-set now sticks since foreground resume syncs from device truth.
Add logging for confirmed/timed out with baseline and current state.
- Installer ownership check now supports debug suffix (.debug) and
fdroid suffix (.fdroid) in addition to base obtainiumId in both
stock_installer.dart and apps_provider_install.dart.
- Manual BG check button ("Run background check now") now passes
forceAll:true — bypasses per-app lastUpdateCheck interval filter and
enableBackgroundUpdates/interval==0 gate, so it unconditionally
checks all apps.
- Bump pinned android_package_installer fork to include fix for
IllegalStateException crash when multiple session-based installs
fire SESSION_API_PACKAGE_INSTALLED intents in one cycle.
- Remove all APPS REFRESH debug logs from apps.dart and
apps_provider_updates.dart (trace logging for refresh diagnostics no
longer needed)
- Remove getInstalledInfo error log entirely: "app not installed" is
always an expected condition, not an error. Remove printErr parameter
and all printErr: false at call sites
- Fix RefreshIndicator spinner disappearing immediately after pull-to-refresh:
changed onRefresh from a new closure (identity changes every build) to a
stable direct method reference, preventing RefreshIndicator.didUpdateWidget
from cancelling the in-progress animation on parent rebuilds
- Fix refresh checking 0 apps: the per-app lastUpdateCheck interval filter
was being applied to user-triggered foreground refreshes. Added forceAll
parameter to checkUpdates so the foreground path checks all apps regardless
of when they were last checked (the interval filter is for BG periodic
idle checks only). BG retry path also simplified to use specificIds directly
- Remove LinearProgressIndicator and refreshingSince field: the determinate
bar was always stuck at 0% (lastUpdateCheck not in pipelineSignature) and
even with that fixed it would jump 0→100% in one frame since all apps are
fetched in parallel. RefreshIndicator spinner provides the only feedback now
- Fix switchToPage(0) ~2s delay: removed stale waitUntil call that waited
for apps page state to become null — was a holdover from main's 4-tab
architecture where pages were created/destroyed; unnecessary with 2 permanent
tabs on dev
- Fix deep link Add page URL field empty: timing race where GeneratedForm's
init notification (isBuilding) overwrote userInput before processInitialUrl
could set the URL. Moved userInput assignment inside if (!isBuilding) guard.
Also: restore showError in linkFn, add query-param extraction, soften
getSource gate in interpretLink
- Add trace logging throughout refresh → checkUpdates flow
- Replace background_fetch + flutter_foreground_task with workmanager (^0.9.0+3)
- Fixed 15-min periodic task with ExistingPeriodicWorkPolicy.keep — no
dynamic scheduling, no cancelAll, no settings listener
- Single callbackDispatcher replaces two entry points (headless task +
foreground service TaskHandler)
- Removed lastCompletedBGCheckTime — per-app lastUpdateCheck with
interval-based filter determines which apps are due
- Removed batch-of-8 chunking — single Future.wait on all apps
- _bgRunUpdateCheck returns ({updates, errors, toThrow}) record —
notification dispatch and install mode handled by bgUpdateCheck
- Retries via one-off WorkManager task with OS-enforced initialDelay +
WiFi/charging constraints (no blocking, no recursion)
- _runBGInstallMode takes explicit List<String> of app IDs — no
auto-discovery ambiguity. Stale pending installs from prior cycles
discovered via findAppIdsWithPendingUpdates + dedup
- Removed canInstallSilentlyInBackground checks from install mode —
performed once in notification loop, passed explicitly
- Removed flutter_secure_storage and _migrateSecureCreds (was never
populated on any release, blocked DNS in bg isolates)
- Merged exemptToNotify into toNotify (fixes notification collision bug)
- Error notification IDs now in safe range 105–10000 (above fixed IDs)
- Retry task names include random suffix (collision-proof)
- Added await initializeSettings() before settings reads (race fix)
- Improved logging: entry, gate-checks, notifications summary, retry
decisions, install counts, pre-existing vs new, completion
- Added "Run background check now" button to Updates settings card
Removed: foreground service declaration, FOREGROUND_SERVICE_SPECIAL_USE
permission, useFGService, showBatteryOptimizationPrompt, 4 orphaned
translation keys, ~100 lines of dead code.
THIS IS EXPERIMENTAL AND MAY NEED TO BE REVERTED.
- Replace background_fetch (^1.7.0) and flutter_foreground_task (^9.2.2)
with workmanager (^0.9.0+3)
- Single callbackDispatcher replaces both the old headless task and the
foreground service TaskHandler — simplifies to one code path
- WorkManager constraints enforce WiFi-only and charging-only at the OS
level instead of checking in Dart after waking the app
- Fix: enableBackgroundUpdates toggle now properly cancels/resumes
WorkManager tasks (previously the toggle was UI-only and didn't
actually stop background checks)
- Add "Run background check now" button to the bottom of the Updates
settings card for manual testing
- Detailed logging at every stage: WorkManager init, task registration,
callback invocation, completion, and crashes
- Removed: foreground service declaration, FOREGROUND_SERVICE_SPECIAL_USE
permission, useFGService setting, showBatteryOptimizationPrompt setting,
and 4 orphaned translation keys
Known risk: workmanager was previously tried and abandoned in Nov 2022
(PR #97) due to fluttercommunity/flutter_workmanager#415 and #363 (tasks
stopping after app closed / after 3-5 days). These issues remain open
but the package has had 4 years of updates since then.
- Remove flutter_secure_storage dependency (introduced and removed within
this branch, never populated on any release — no migration needed)
- Drop _migrateSecureCreds which was causing "Software caused connection
abort" errors in background isolates by accessing Android Keystore
and blocking DNS resolution in the headless isolate
- Fix all flutter analyze warnings:
- HeadlessTask -> HeadlessEvent (deprecated API)
- Add final to bg task local variables
- Add unawaited() to registerHeadlessTask call
FlutterSecureStorage uses platform channels (Android Keystore) that can block
indefinitely in a background Flutter isolate, starving the event loop and
preventing DNS resolution - causing 'SocketException: Failed host lookup' on
every background check. Revert to plain SharedPreferences like main. A one-shot
migration moves any existing Keystore-stored creds into prefs on first launch.
Dev narrowed the redirect-following range to 301-308 (excluding 303 See Other
and other valid 3xx codes). Restore the full 300-399 range matching main, so
standard HTTP redirects are followed correctly.
HttpService.createHttpClient set connectionTimeout: 30s on every HttpClient.
In the background isolate on real Android devices, DNS resolution can exceed
this when the network stack is cold, causing 'SocketException: Failed host
lookup' on every background update check. Restore main's behavior of no
timeout (null), which allows DNS to resolve at the system's pace.
The retryAfterXSeconds value was computed and logged as an expected inter-invocation
interval, and is already enforced via lastCompletedBGCheckTime. Sleeping within a
BackgroundFetch task would consume the limited execution window. Match main's
behavior: log the expected delay but don't actually sleep.
Main's background-task path (forceParallelDownloads: true) used serial downloads
to be gentle on network/memory; dev's inverted condition ran them in parallel.
Restore main's semantics: parallel-only when forceParallelDownloads is false
AND the user's parallelDownloads setting is on, serial otherwise.
bgUpdateCheck unconditionally set lastCompletedBGCheckTime after
_bgRunUpdateCheck returned, even when _bgRunUpdateCheck bailed early on the
enoughTimePassed guard. With the foreground-service interval at 15 min, every
fire reset the clock, so (lastCompleted + interval).isBefore(now) perpetually
failed and background checks never ran.
Move the enoughTimePassed guard to the outer bgUpdateCheck so the early return
skips the timestamp update, matching main where the guard and timestamp gate
the same block.
- _loadSecureCache now wraps secure-storage read/write in try/catch and falls
back to the legacy prefs value, so an unavailable Keystore/Keychain can no
longer break settings initialization (a failure mode absent on main).
- getExportDir no longer swallows SAF exceptions or auto-disables the export
directory after N errors; exceptions propagate as on main, so a transient
SAF glitch can't silently drop a valid export dir.
_bgRunUpdateCheck's enoughTimePassed guard was inverted: with updateInterval == 0
('Never / manual only') it short-circuited to true and proceeded with the check,
whereas main returned early. Restore main's 'updateInterval != 0 && ...' so a
background task that fires while auto-checks are disabled skips the check.
The download-progress setup and notify() ran before the try whose finally
clears the cancellation token, so a throw there leaked the token (blocking a
later cancel for the same app). Move the setup inside the try.
Two changes combined to silently, permanently drop apps on load:
- App.fromJson threw a fatal error if appJSONCompatibilityModifiers failed
(e.g. getSource throws for a URL no longer matching any source, like the
removed Mullvad source); restore main's fallback to the unmigrated JSON.
- loadApps renamed the file to .corrupt on ANY error; restore main's behavior
of only doing so for FormatException (genuine corruption) and otherwise
skipping while keeping the file.
Also drop the compatVersion gating/stamp and run the (idempotent) legacy
migrations unconditionally like main, removing the gating fragility.
Download notification IDs were bounded to [100, 9099] via % 9000, causing
birthday-paradox collisions when many apps download concurrently (now common
since background downloads run in parallel) - colliding apps share a
notification, so progress updates and the per-notification cancel affect the
wrong one. Widen the modulo to ~2^31 so collisions are as rare as a raw
hashCode, while keeping IDs positive and clear of the fixed IDs.
- installApkDir now matches only .apk files in an extracted bundle (matches
main); a nested container is no longer handed to the installer
- the post-download cleanup loop keys off the resolved app id rather than the
pre-change (possibly placeholder) id
downloadFile forced any container URL (.xapk/.apkm/.apks) to a .apk filename,
so downloadApp treated the bundle as a plain APK and fed it to the APK parser
instead of extracting it (couldNotGetIdFromApk). Preserve the real container
extension; only 'attachment' content-disposition still defaults to .apk.
The install harness deleted the downloaded file for cancelled (null) and
already-installed/pending (code 3) outcomes; main preserved it so a retry could
reuse it without re-downloading. Only delete on success/error now.
Several call sites used getBool('versionDetection', defaultValue: true), so apps
missing the key (e.g. DirectAPKLink / legacy imports) were treated as standard
version detection - hiding mark-as-updated and running install-status
reconciliation main skipped. Default to false to match main's == true / != true
semantics.
getApp defaulted a new app's preferredApkIndex to 0 (first APK); main used the
last (apkUrls.length - 1). Restore main's default while still preserving an
existing app's chosen index on update.
Repo-rename detection still paused updates via hasPendingRepoRename, but the
accept flow/UI was removed, leaving renamed apps stuck permanently. Reinstate
acceptRepoRename and an M3-Expressive banner (built from ConnectedCard) on the
app detail page to adopt the new URL or dismiss it.
An import whitelist with wrong key names (installerMode vs installMethod,
exportAppSettings vs exportSettings) and ~20 missing keys silently dropped most
settings on import. Restore main's behavior of importing every exported key.
saveApps only fetched installed info/icon/name when attemptToCorrectInstallStatus
was true; the false path (background-install workaround, uninstall) nulled them
and deleted the cached icon, making app icons vanish. Restore main's behavior:
always fetch info/icon/name, gate only the install-status correction.
Two regressions from the immutable App refactor left newly added apps stuck
with a temporary placeholder ID (and crashed later installs on apps[tempId]!):
- isTempId only matched all-numeric IDs, but generateTempID now returns a
sha256-hex prefix, so temp IDs weren't detected and the APK-download step
that resolves the real package ID was skipped
- handleAPKIDChange copyWith'd its local App param (relying on the old in-place
mutation) and returned only the File, so the resolved ID was dropped; it now
returns the updated App and downloadApp uses it for the DownloadedApk/Dir
canInstallSilently conflated two concepts: whether an app can be installed
without a prompt (device/app capability) and whether background updates are
enabled/allowed. This gated foreground silent installs on the background-update
toggle. Split into:
- canInstallSilently: pure capability (single APK + active installer's rules)
- canInstallSilentlyInBackground: layers the global toggle and per-app
exemption on top, used only by the background flow
Also move the target-SDK requirement into StockInstaller, since it is specific
to the session installer and does not apply to Shizuku/external installers.
- Move the logs viewer from a modal dialog to its own page
- Render logs in a virtualized SliverList (was one giant Text) for smooth
scrolling with many entries; keep text selectable via SelectionArea
- Consolidate day filter, share, clear, and jump-to-top/bottom into a single
M3 Expressive floating toolbar docked via a Stack (no FAB entrance animation)
- Stop logging expected non-matches during source auto-detection in getSource,
which spammed 'Source standardize error' lines on every refresh
- Attach the offending URL to ObtainiumError and surface it via toString
so logs and error messages identify the app/URL involved
- Propagate URL context at source/install choke points (getApp, getSource,
standardizeUrlWithRegex, download and install error paths)
- Log a specific reason whenever an app cannot be installed silently
- Log when install completion is detected via fallback polling instead of
an installer return code (external installer)