tauri-plugin-android-installer-api
v0.1.0
Published
JS bindings for tauri-plugin-android-installer — install Android APKs from a Tauri v2 app.
Maintainers
Readme
tauri-plugin-android-installer
I was working on a mobile app and found myself having to handle internal distribution. Manual APK sharing is painful and I was not in the mood to fight with Play Store either. I decided to wire up a simple update system in the app. Instead of a plain .apk download I wanted the flow to be smooth and contained in the application itself. As such, this plugin was born.
NOTE: Plugin coded with AI, review before using.
Install an Android .apk from a local file path in a Tauri v2 app. Use it for
in-app self-updates or sideloading a downloaded APK.
It does one thing: install(path). Downloading is not handled here. Pair it with
@tauri-apps/plugin-upload
or any other way you get the file onto disk.
What it can and can't tell you
Android never lets an app install another app silently. The system installer UI ("Do you want to install this app?") always shows. That shapes the API:
- Installer launched. Tracked:
install()resolves. - Failed to launch (file missing, bad path, no permission). Tracked:
install()rejects with a reason. - User cancelled / install failed. Not tracked by this plugin. Detecting
those needs
PackageInstallersessions, which this plugin doesn't use (yet). - Install succeeded. Can't be observed for a self-update. Android replaces
your APK and kills the process, so no callback fires. Check on the next launch
with
getVersion()from@tauri-apps/api/app.
Design your flow around "did the installer launch," not "did it succeed."
Platform support
| Platform | Behavior |
| --- | --- |
| Android | Full support. |
| Desktop / iOS | canInstall() returns false; install() and requestInstallPermission() reject with "only supported on Android". |
This means you can call canInstall() anywhere as a capability check without a
try/catch.
Setup
The Tauri CLI does the whole thing in one step:
tauri add android-installertauri add recognizes the third-party naming convention, so it adds the
tauri-plugin-android-installer crate and the tauri-plugin-android-installer-api
npm package, registers .plugin(tauri_plugin_android_installer::init()) in your
builder, and adds the android-installer:default permission to your capability
file. The rest of this section is what it does by hand.
Add the Rust crate:
cargo add tauri-plugin-android-installerRegister it in your Tauri builder (src-tauri/src/lib.rs):
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_android_installer::init())
// ...
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Add the JS bindings:
pnpm add tauri-plugin-android-installer-api
# npm install / yarn add also workGrant the permission in your capability file (src-tauri/capabilities/default.json):
{
"permissions": ["android-installer:default"]
}android-installer:default enables all three commands. To allow a subset, list
android-installer:allow-install, android-installer:allow-can-install, and
android-installer:allow-request-install-permission individually.
The REQUEST_INSTALL_PACKAGES permission and the FileProvider are declared in
the plugin's own manifest and merge into your app automatically. You don't add
anything to your app's AndroidManifest.xml.
Google Play note
Because of that auto-merge, adding this plugin puts REQUEST_INSTALL_PACKAGES
into your app's manifest. Google Play treats it as a sensitive permission:
apps that declare it have to qualify for an approved use case (app
distribution / device-and-app discovery) or risk rejection. The plugin is meant
for sideloaded or self-hosted builds. If you ship through Play, confirm you meet
the policy before pulling it in.
Usage
Download an APK into the app cache, make sure the install permission is granted, then launch the installer:
import { download } from "@tauri-apps/plugin-upload";
import {
install,
canInstall,
requestInstallPermission,
} from "tauri-plugin-android-installer-api";
import { appCacheDir, join } from "@tauri-apps/api/path";
async function update(url: string) {
const path = await join(await appCacheDir(), "update.apk");
await download(url, path, ({ progressTotal, total }) => {
console.log(`${Math.round((progressTotal / total) * 100)}%`);
});
if (!(await canInstall())) {
await requestInstallPermission();
if (!(await canInstall())) return; // user declined
}
await install(path);
// The OS install prompt appears. On success the app is replaced and this
// process is killed, so nothing after install() runs in that case.
}API
install(path: string): Promise<void>
Launches the system installer for the APK at path. Resolves once the installer
UI is launched. Rejects if it couldn't launch.
path must be an absolute path inside the app's cache or files directory (see
Where to put the APK). appCacheDir() from
@tauri-apps/api/path is the safe default.
Rejection reasons:
No APK path provided— thepathargument was empty.APK not found or not a regular file at path: ...— the path is missing or is a directory.APK path is not inside a shareable directory: ...— the file is outside every configured FileProvider root.No installer available to handle the APK— no activity resolved the install intent (unusual; effectively never happens on a real device).
canInstall(): Promise<boolean>
Whether the app may currently install unknown apps
(PackageManager.canRequestPackageInstalls()).
- Returns
truebelow Android 8, where there's no per-app gate. - Returns
falseon desktop and iOS.
requestInstallPermission(): Promise<void>
Opens the system "install unknown apps" settings screen for your app. Resolves when the user returns from settings.
It does not guarantee the user granted anything, so re-check canInstall()
afterwards. If permission is already granted it resolves immediately without
opening settings. Rejects on desktop and iOS.
Where to put the APK
install() shares the file through a FileProvider, which only works for files
under a configured root. The plugin configures all four of an app's private
storage dirs:
getCacheDir()— this is whatappCacheDir()from@tauri-apps/api/pathresolves to, and the recommended default.getFilesDir()getExternalCacheDir()getExternalFilesDir(null)
Download into one of these (appCacheDir() covers the first). The two external
roots only resolve when external storage is mounted, so internal appCacheDir()
is the most reliable choice. If you download somewhere else, such as a public
Downloads folder or the SD card root, install() rejects with "APK path is not
inside a shareable directory."
Install-unknown-apps permission (Android 8+)
Declaring REQUEST_INSTALL_PACKAGES isn't enough on Android 8 and up. The user
also has to toggle "allow from this source" for your app once. The flow:
canInstall()— already allowed?- If not,
requestInstallPermission()— sends the user to the right settings screen. canInstall()again after they return — did they enable it?
Skipping this is the most common reason a sideload "does nothing": the installer launches but the OS blocks it because the source isn't trusted.
Troubleshooting
getUriForFile/ "not inside a shareable directory". The APK isn't under a FileProvider root. Download toappCacheDir().- Installer launches but the install is blocked. The install-unknown-apps
permission isn't granted. Run the
requestInstallPermission()flow above. - "App not installed" on an emulator when reinstalling. The new APK is signed
with a different key than the installed one.
adb uninstall <applicationId>first, or sign both builds with the same key. - Nothing happens after a successful install. Expected. A self-update replaces
the app and the process is killed. Confirm the new version on next launch with
getVersion().
Example
A runnable demo lives in examples/tauri-app. It downloads
an APK from a URL and installs it, and has buttons for the permission flow.
cd examples/tauri-app
pnpm install
pnpm tauri android devLicense
MIT or Apache-2.0, at your option.
