npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tauri-plugin-android-installer-api

v0.1.0

Published

JS bindings for tauri-plugin-android-installer — install Android APKs from a Tauri v2 app.

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 PackageInstaller sessions, 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-installer

tauri 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-installer

Register 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 work

Grant 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 — the path argument 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 true below Android 8, where there's no per-app gate.
  • Returns false on 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 what appCacheDir() from @tauri-apps/api/path resolves 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:

  1. canInstall() — already allowed?
  2. If not, requestInstallPermission() — sends the user to the right settings screen.
  3. 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 to appCacheDir().
  • 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 dev

License

MIT or Apache-2.0, at your option.