Address several gaps in the app export feature (#1132):
- Bundle shared-library APKs and OBB/patch files, preserving their
relative layout (libraries/<pkg>/ and Android/obb/<pkg>/).
- Fall back to the installed app's on-device APKs when the cached
download was cleared (e.g. auto-deleted after install), guarded by an
exact version match so a different installed version is never exported.
- Guard against the download row being replaced by a manual download of
another version between enqueue and execution.
- Fail (and delete the partial/empty document) instead of reporting
success when there is nothing to bundle or an error occurs.
- Store entries uncompressed and use larger buffers, so multi-GB exports
are no longer bottlenecked on pointless re-compression.
- Report progress through the foreground notification (alert once, then
silent) and dedupe concurrent exports for the same app via unique work.
- Check free space before downloading (only the not-yet-fetched bytes) and, on
Android O+, use StorageManager.getAllocatableBytes/allocateBytes so the system
can evict its cache; fail fast with a clear message instead of dying mid-write.
SessionInstaller sets a SessionParams size hint so staging can reserve space
too.
- Verify each APK at most once: files already verified during the download pass
are skipped in the final verification gate. Prefer SHA-256 and log SHA-1
fallback.
- An expired download URL (403/410) now clears the stored file lists and retries,
re-purchasing fresh URLs instead of repeatedly failing on the dead one.
- Cancel promptly mid-file rather than only between files, and cancel copyTo's
progress timer in a finally to avoid leaking the timer thread.
- Download.canInstall now requires a real .apk on disk; DownloadStatus
finished/running are Sets.
A download row previously stopped at COMPLETED and never reflected whether the
app actually installed. Add INSTALLING/INSTALLED states driven by a central
installer-event observer in DownloadHelper:
- COMPLETED -> INSTALLING on the installer's first progress event
- -> INSTALLED on success (row kept so the APK can still be exported)
- a failed install reverts INSTALLING -> COMPLETED so it can be re-installed
without re-downloading
Consumers that branched on COMPLETED are updated (App Details state, MicroG
status mapping, Downloads list icon). downloadStatus is stored as TEXT so no
schema migration is needed.
Cancelling a download cancelled the worker and marked the row CANCELLED but
never abandoned a PackageInstaller session that had already been staged for
install, leaking it. Add IInstaller.cancelInstall (no-op default, implemented by
SessionInstaller) and call it from DownloadHelper.cancelDownload.
Cross-process session persistence/reconciliation (committing a session staged
before a restart) is left as a follow-up; the startup session cleanup remains,
and is now cheap to recover from since CacheWorker keeps the downloaded files.
- CacheWorker no longer purges the files of a download that is still in-flight
or downloaded and awaiting install, so missing the system install prompt for
longer than the cache window no longer forces a re-download.
- observeDownloads now starts the next queued download only once nothing else
is purchasing/downloading/verifying, instead of merely checking for a
DOWNLOADING row. This serializes downloads so concurrent workers can't clobber
the shared foreground/progress notification.
- enqueue() no longer resets an active/verifying download back to QUEUED, so
the periodic update check and repeated taps can't trigger needless
re-downloads; when files are already downloaded & verified it installs
directly instead of re-running the pipeline.
- DownloadWorker only purges partial files on a genuine user cancellation;
system-initiated stops (lost connectivity, quota) keep partials and retry,
fixing downloads appearing to restart on a flaky network.
- Add NetworkType.CONNECTED constraint + exponential backoff and return
Result.retry() for transient/network failures (capped) instead of always
succeeding.
- Validate HTTP 206 before resuming a .tmp (overwrite on 200) and drop corrupt
files on verification failure so retries restart clean.
Fold Aurora Store's self-update back into the existing update pipeline
instead of a dedicated sheet/viewmodel/helper. The feed entry is mapped
to a regular App and added to the update list, reusing the standard
download + install path; it is never auto-installed silently.
- Add a dedicated "Self-update" section in the Updates tab with the
app-details navigation disabled (it's served from the Aurora OSS feed).
- Gate eligibility via PackageUtil.isSelfUpdateSupported(): vanilla/preload
flavors, not debug, not F-Droid (huawei excluded by flavor).
- Add a build-aware Settings toggle as the opt-out, removing the row
immediately when disabled.
- Exempt nightly self-updates from deleteInvalidUpdates (static version
code) and drop any stale self-update row when none is offered, so a
phantom update doesn't linger after a self-install.
Stream/ExpandedStream/Review/Search paging VMs were swallowing exceptions
inside manualPager and returning emptyList, so the pager never entered
LoadState.Error and the error-placeholder retry buttons were dead. Let
non-Auth exceptions propagate; keep the AuthException → SessionExpired
branch so 401 still hands off to Splash.
Also drop supervisorScope wrappers from CategoryStream/DevProfile/TopChart
VMs that launch no children — the surrounding try/catch already handles
everything.
Long-backgrounded sessions (especially on aggressive vendors like Huawei)
could leave the Play token invalid and OkHttp's connection pool full of
dead sockets, producing a perpetual loading spinner with no error.
- ComposeActivity#onStart probes the saved token via isSavedAuthDataValid;
on failure evicts the OkHttp pool and refreshes anonymous auth.
- AuthProvider gains authReady StateFlow, awaitReady(), and a mutex-guarded
refreshAnonymousAuth() so concurrent callers don't double-fetch.
- Play-backed ViewModels (AppDetails, DevProfile, MoreApps, Reviews, Search,
ExpandedStreamBrowse, Category) await refresh completion before issuing
helper calls, closing the race between resume-time refresh and screen
LaunchedEffect fetches.
- AppDetailsScreen rendered Error as Loading when app was null; reorder so
Error wins and wire a Retry button through Placeholder.
- Stream/Review/Search/DevProfile/CategoryBrowse error placeholders gain
Retry actions wired to PagingItems.retry() or the VM's fetch.
- CategoryViewModel was logging-only on failure; now emits ViewState.Error
with a matching Retry path in CategoriesPage.
Share a sort/filter sheet between InstalledScreen and BlacklistScreen via
new commons module (SortFilterState + SortFilterSheet + applyFilter/applySort
on InstalledAppMeta), persisted per-screen. InstalledScreen renders a new
InstalledAppListItem that surfaces size, last-update, and installer instead
of Play-listing extras. Blacklisted rows now desaturate their icons.
- AppUpdateItem now shows a disabled "Installing" button while the
download is COMPLETED and awaiting install, instead of briefly
reverting to "Update" between download-done and InstallerEvent.Installed.
- Section header replaces all-or-nothing "Update all" toggle with an
any-active check, so "Cancel all" appears as soon as a single update
is in flight. Same treatment for the ownership-approval section.
- Empty placeholder no longer hides the ignored-updates list when there
are no live updates.
- DownloadHelper.cancelAll also flips COMPLETED -> CANCELLED so the
Cancel-all button gives immediate feedback for pending-install rows.
- DownloadListItem: surface the failed icon for FAILED downloads too.
Collect NetworkProvider.status once at the activity root and expose it via
LocalNetworkStatus so any screen can read it without injecting the provider.
Drop the redundant networkProvider field from MainViewModel and AuthViewModel.
NetworkSheet becomes a full-screen NetworkScreen with the same content.
- New SourceFiltersScreen with All toggle + add/remove of specific
installer package names. Backed by PREFERENCE_FILTER_INSTALLERS
(Set<String>); PREFERENCE_FILTER_AURORA_ONLY preserved as the All
switch for backward compat.
- UpdateWorker reads installer via getInstallSourceInfo (R+) / falls
back to deprecated getInstallerPackageName below.
- UpdatesPreferenceScreen: "Filter F-Droid apps" moved to Advanced;
"Filter apps from other sources" becomes a navigation row to the
new sub-screen with a state summary.
Replace the 1-24h slider with a SingleChoiceDialog listing 7 presets
(3h, 6h, 12h, 24h, 3d, 7d, 15d). Storage stays as an Int hour count
under the same preference key, so old values keep working; off-preset
values display as the nearest preset but are only overwritten when the
user picks something new.
- Group updates into main / approval-required / incompatible / ignored
- New ignored_update table (v8) with per-version skip support
- isIncompatible flag on pristine system apps for HyperOS / GrapheneOS (v7)
- requiresOwnershipTransfer() runtime check on Android 14+
- Rename AppMenuSheet -> AppUpdateSheet; drop MinimalApp, nav carries Update
- Trim AppUpdateItem (no changelog dropdown / long-click)
- Fix DownloadHelper.downloadsList recomposition flicker (stable StateFlow)
- ExportWorker is now download-only
Drops every Fragment, DialogFragment and BottomSheet in view/ui plus
the flavoured SplashFragment variants. All call sites now route
through compose/ui equivalents and ComposeActivity.
Drops the Epoxy carousel controllers, view models, shimmer placeholders,
custom layouts (ActionHeader, StateButton, UpdateButton) and bespoke
preference helpers, all now superseded by the Compose composables.
CategoryBrowseScreen and ExpandedStreamBrowseScreen are added; the
details / dev / dispenser / downloads / favourite / installed /
search / spoof / blacklist / accounts / about screens, StreamBrowse,
the onboarding MicroG page, the AppDetailsScreen plus the affected
viewmodels (AuthViewModel, ExpandedStreamBrowse, CategoryStream,
TopChart) are reworked to feed the new Compose entry points.
Also fixes CategoryBrowse pagination and the cluster-header
navigation regressions surfaced during migration.
SplashScreen reacts to NetworkProvider and ditches the separate
SplashButton class for inline buttons. Google sign-in moves into the
dedicated GoogleLoginScreen reachable from the splash flow.
NetworkSheet replaces the legacy NetworkDialogSheet with a persistent
modal that reacts to NetworkProvider status. MoreSheet hosts the
account / settings / about entry points. AppMenuSheet provides the
per-app long-click menu (blacklist, uninstall, export, info).
Settings, UI, Installation, Network and Updates preference panes are
now native Compose. Replaces the old PreferenceFragment XML hierarchy
with explicit ListItem / Switch / dialog rows.
MainScreen hosts the bottom-nav between Apps / Games / Updates with a
HorizontalPager. The Apps/Games host (AppsGamesScreen) carries the
For You / Top Charts / Categories sub-tabs implemented as separate
pages.
ComposeActivity becomes the sole launch target; the legacy MainActivity
is removed. Destination / Screen pair drives navigation3, NavDisplay
handles deep links plus microG-aware resume rechecks, and Theme.kt
is tightened for edge-to-edge.
Bridges Flows whose values have broken equals() (the gplayapi
StreamBundle/StreamCluster id-only-equals quirk) into Compose state by
forcing recomposition via neverEqualPolicy.
StreamCarousel for cluster-based home/category lists, CategoryItem grid
tile, and AppUpdateItem for the updates list. AnimatedAppIcon plus the
existing AppListItem and LargeAppListItem are adjusted to fit the new
call sites.
The VM was calling PackageUtil.getAllValidPackages(context) eagerly in its
constructor, which Hilt creates on the main thread. The PackageManager IPC
plus a per-app loadLabel() for sorting froze the screen long enough that
the existing ContainedLoadingIndicator never got a chance to render.