PDFTools can be deployed as a native Android app using Capacitor — the same toolchain used for iOS. The React web app runs inside an Android WebView, and Capacitor plugins give access to native APIs (filesystem, share sheet, notifications).
| Requirement | Notes |
|---|---|
| Android Studio Hedgehog (2023.1+) | Free at developer.android.com/studio |
| JDK 17+ | Bundled with Android Studio |
| Android SDK API 34 | Install via Android Studio SDK Manager |
| Node.js 20+ | For Capacitor CLI |
| Google Play Developer account | One-time $25 USD registration fee |
Android Studio runs on Windows, macOS, and Linux — no Mac required!
React + TypeScript (web app)
│
│ npm run build → frontend/dist/
│
Capacitor CLI
│
│ npx cap add android
│ npx cap sync
│
android/ folder (Gradle project)
│
│ Android Studio → Build → Generate Signed Bundle/APK
│
.aab file → Google Play Console
│
Google Play → Users
If not already done from the iOS guide:
# From repository root
npm install @capacitor/core @capacitor/cli
npm install @capacitor/android \
@capacitor/browser \
@capacitor/share \
@capacitor/filesystem \
@capacitor/local-notifications
cd frontend
VITE_API_URL=https://api.yourdomain.com npm run build
cd ..
Run this every time you update the frontend before syncing.
# Add Android project (run once)
npx cap add android
# Copy web assets + install plugins into Android project
npx cap sync android
This creates an android/ directory — a standard Gradle/Android Studio project.
npx cap open android
Android Studio opens. Gradle will sync (takes 1–3 minutes on first open).
Edit android/app/build.gradle:
android {
defaultConfig {
applicationId "com.yourcompany.pdftools" // ← change this
minSdkVersion 24
targetSdkVersion 34
versionCode 1
versionName "1.0.0"
}
}
Edit android/app/src/main/res/values/strings.xml:
<resources>
<string name="app_name">PDFTools</string>
<string name="title_activity_main">PDFTools</string>
<string name="package_name">com.yourcompany.pdftools</string>
<string name="custom_url_scheme">com.yourcompany.pdftools</string>
</resources>
app/src/main/res → New → Image Asset.This generates all required densities (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi).
Edit android/app/src/main/AndroidManifest.xml to add any needed permissions:
<!-- Internet (required) -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Save files to Downloads -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- Post notifications (Android 13+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Edit android/app/src/main/res/values/styles.xml:
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
<item name="android:background">@drawable/splash</item>
</style>
Place your splash.png in android/app/src/main/res/drawable/.
npx cap run android # first connected device
npx cap run android --target emulator-5554 # specific emulator
Google Play requires an Android App Bundle (.aab) — it’s smaller than an APK because Play generates optimised APKs per device.
keytool -genkey -v \
-keystore pdftools-release.keystore \
-alias pdftools \
-keyalg RSA \
-keysize 2048 \
-validity 10000
# You'll be prompted for: keystore password, name, org, location
⚠️ Store this keystore file and passwords safely. You cannot update your app on Google Play without the same keystore. Keep a backup in a password manager and secure cloud storage.
Edit android/app/build.gradle:
android {
signingConfigs {
release {
storeFile file('../pdftools-release.keystore')
storePassword System.getenv("KEYSTORE_PASSWORD") ?: "yourpassword"
keyAlias "pdftools"
keyPassword System.getenv("KEY_PASSWORD") ?: "yourpassword"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}
In Android Studio:
.aab at android/app/release/app-release.aabOr via command line:
cd android
./gradlew bundleRelease
# Output: android/app/build/outputs/bundle/release/app-release.aab
app-release.aab.| Asset | Size |
|---|---|
| App icon | 512×512 px PNG (no transparency) |
| Feature graphic | 1024×500 px |
| Phone screenshots | Min 2, max 8 (320–3840 px, 16:9 or 9:16) |
| 7” tablet screenshots | Optional but recommended |
| Short description | Max 80 characters |
| Full description | Max 4000 characters |
Complete the questionnaire in Policy → App content → Content rating. PDFTools is rated Everyone.
In Policy → App content → Data safety:
If you don’t want to use Google Play, you can distribute a plain APK directly:
cd android
./gradlew assembleRelease
# Output: android/app/build/outputs/apk/release/app-release.apk
Host the .apk file on your website and link to it. Users must enable “Install from Unknown Sources” in their device settings.
Note: This bypasses Play Store protections. Use only for internal / enterprise distribution.
Add this workflow to build a signed AAB on every push to main:
# .github/workflows/android-build.yml
name: Android Build
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Build frontend
run: |
cd frontend
npm ci
VITE_API_URL=$ npm run build
- name: Install Capacitor
run: npm ci
- name: Sync Android
run: npx cap sync android
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Decode keystore
run: |
echo "$" | base64 --decode \
> android/pdftools-release.keystore
- name: Build AAB
env:
KEYSTORE_PASSWORD: $
KEY_PASSWORD: $
run: |
cd android
./gradlew bundleRelease
- name: Upload AAB artifact
uses: actions/upload-artifact@v4
with:
name: app-release
path: android/app/build/outputs/bundle/release/app-release.aab
Required GitHub Secrets:
| Secret | Value |
|—|—|
| KEYSTORE_BASE64 | base64 pdftools-release.keystore |
| KEYSTORE_PASSWORD | Your keystore password |
| KEY_PASSWORD | Your key password |
| VITE_API_URL | https://api.yourdomain.com |
| Problem | Fix |
|---|---|
| Gradle sync failed | File → Invalidate Caches / Restart, then sync again |
| “INSTALL_FAILED_UPDATE_INCOMPATIBLE” | Uninstall the old version from the device first |
| White screen | Check Logcat — usually a CORS error or wrong VITE_API_URL |
| Mixed content error | API must be HTTPS; check android:usesCleartextTraffic in manifest |
| App crashes on launch | Enable WebView debugging: WebView.setWebContentsDebuggingEnabled(true), inspect via chrome://inspect |
| Keystore lost | Cannot recover — you’d need a new app listing. Store it in 3+ places! |
| Play Store rejects update | Ensure versionCode is incremented in build.gradle |