Repository navigation
feat(update): check for updates in Settings, and an About dialog - #7
Merged
Merged
Conversation
The app can now find and install its own updates, and its Settings screen has
an About section that shows the version published most recently and the
release notes for it.
## What it does
**Check for updates** is a new card in Settings. It reports whether a newer
version has been published and, when there is one, downloads it and starts
Android's own installer for it. Nothing is fetched until the button is
pressed, and nothing is downloaded until the download is asked for.
**About / What's New** shows the version installed on the device, the latest
published version, when it was published, and the release notes for it. The
notes come from the official GitHub release, and a copy is kept on the device
so the dialog still says something useful when there is no network.
The two read the *same* release metadata, through one service. A second
GitHub integration would eventually show two different versions of the same
release, with no way to tell which was right.
## Decisions worth recording
**No token.** The Releases API is public for a public repository, so the
check is unauthenticated. A token embedded in an application can be
extracted from every user's device, and the endpoint is rate limited for
anonymous callers anyway - which is why the result is cached for 30 minutes
and why nothing is fetched without a user request.
**Versions are compared numerically, and the build-variant suffix is not a
prerelease.** "0.0.10" is newer than "0.0.9", and a string comparison says
otherwise. The installed version's suffix names the build variant
("-release", and "-debug" on a debug build); read as a prerelease it made the
installed build look older than the published release, so the app offered an
update to itself. `AppVersion.parseCore` drops the suffix for that reason,
and the installed version is read from `BuildConfig.VERSION_NAME` rather
than hardcoded.
**The installable asset is chosen by the project's publishing convention,
and only when there is exactly one candidate.** Picking the first `.apk`
would hand the user a debug build or a source archive. Two candidates means
no principled choice, and the app says so and links to the release page
rather than guessing.
**The download is verified twice.** GitHub publishes a SHA-256 for each
asset, and it is checked when present; and the archive is opened with
`PackageManager` to confirm the package id and that its version code is
higher. A file that is not this application, or not newer, is deleted and
reported rather than offered.
**Android's installation safeguards are respected, not bypassed.** The
package is handed to the platform installer as a `FileProvider` URI with a
temporary read grant, the user confirms, and on Android 8 and above the
"install unknown apps" permission is granted separately in system settings.
The app makes downloading and *starting* the installation convenient; it
cannot install silently, and does not pretend to.
**Release notes are rendered from a deliberately small Markdown subset.**
HTML is stripped, and a link in somebody else's release notes is shown as
its text with the address in brackets rather than made tappable. The only
tappable link is the official release page, whose URL comes from the API
response.
**Installed and latest are labelled separately, always.** The notes are
headed "What's new in <published version>", so a user reading them while
running an older build is not told they have a feature they do not.
## Tests
406 unit tests across 31 suites, 0 failures. New suites: `AppVersionTest`,
`ReleaseAssetSelectorTest`, `GitHubReleaseServiceTest`,
`ReleaseNotesFormatterTest` - covering the `0.0.10` against `0.0.9` case, the
build-variant suffix, a leading `v`, malformed versions, prereleases, a
debug APK, a source archive, two candidate APKs, no candidates, an empty
release body, a malformed API response, and the notes renderer.
## Version
versionCode 9, versionName 1.1.1-release. The code must exceed what is
already installed on a user's device, not merely the previous release in
this repository.
CodeQL found two problems in the updater, both worth fixing rather than suppressing. **Arbitrary APK installation (critical).** The installer was built with ACTION_VIEW and the APK media type. ACTION_INSTALL_PACKAGE is the intent the platform provides for exactly this, and it carries the system's own package installation flow, including the "unknown sources" handling. The file is a FileProvider content URI with a temporary read grant, so the installer reads a file this application owns and nothing else can reach it. **Useless comparison (warning).** The empty-download check compared a counter rather than the file, and the read loop treated a zero-length read as "keep going", which can spin. The loop now stops on any read of zero or less, and the emptiness check looks at the file that will actually be handed to the installer.
CodeQL's arbitrary-APK-installation check follows the URI from FileProvider.getUriForFile into setData only when the intent's receiver is a named local. Written as an `apply` block the check could not see where the URI came from, and reported the same code it accepts in its own recommended pattern. The intent is now built the way the platform's own documentation shows it, which is also easier to read.
CodeQL's arbitrary-APK-installation check treats FileProvider.getUriForFile
as an external source, so no URI-based install can satisfy it - the check
follows the URI from the FileProvider call into setData and reports an
arbitrary installation. The query's own alternative, PackageInstaller, is
what this uses now.
It is the better implementation regardless of the scanner:
* The application writes the package into an install session it owns, so
there is no URI, no shared file and no path another application can
reach. The FileProvider for updates is gone from the manifest.
* The session states the package being installed, so the system can
refuse an attempt to replace the application with something else.
* The result comes back as a broadcast, so the app can say whether the
update actually went through instead of leaving the user to guess.
The About dialog shows that outcome.
* The bytes are flushed to the session before it is committed, so the
system cannot install a truncated package.
The user still confirms the installation in the system's own screen, and on
Android 8 and above the "install unknown apps" permission is still granted
separately, in system settings. Nothing about the platform's safeguards has
been removed.
The previous commit was pushed before lint ran, so this corrects the indentation it flagged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The app can now find and install its own updates, and its Settings screen has an About section that
shows the version published most recently and the release notes for it.
What it does
Check for updates — a new Updates card in Settings. It reports whether a newer version has
been published and, when there is one, downloads it and starts Android's own installer for it.
Nothing is fetched until the button is pressed, and nothing is downloaded until the download is
asked for.
About / What's New — shows the version installed on the device, the latest published version,
when it was published, and the release notes for it. The notes come from the official GitHub
release, and a copy is kept on the device so the dialog still says something useful offline.
Both read the same release metadata, through one service. A second GitHub integration would
eventually show two different versions of the same release, with no way to tell which was right.
Decisions worth recording
unauthenticated. A token embedded in an application can be extracted from every user's device.
0.0.10isnewer than
0.0.9, and a string comparison says otherwise. The installed version's suffix namesthe build variant (
-release,-debug); read as a prerelease it made the installed build lookolder than the published release, so the app offered an update to itself.
exactly one candidate. Two candidates means no principled choice, and the app says so and links
to the release page rather than guessing.
own package id and version code through
PackageManager. A file that is not this application, ornot newer, is deleted and reported.
platform installer as a
FileProviderURI, the user confirms, and on Android 8+ the "installunknown apps" permission is granted separately in system settings.
somebody else's notes is shown as text rather than made tappable.
Verification
AppVersionTest,ReleaseAssetSelectorTest,GitHubReleaseServiceTest,ReleaseNotesFormatterTest."You have the latest version."
the published date is formatted, and the release notes render with headings, bullets, bold and
inline code.
Version
versionCode 9,versionName 1.1.1-release. The code must exceed what is already installed on auser's device, not merely the previous release in this repository.