Carved out from the app-store monorepo. Kotlin + Material 3 client that browses the private distribution server and installs APKs via PackageInstaller. Keystore password redacted from docs.
3.6 KiB
3.6 KiB
Upload Android App to Local App Store
Follow these steps to add a new Android app to the Local App Store.
Prerequisites
- Java 17:
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 - Android SDK:
export ANDROID_HOME=/home/jarian/android-sdk - Gradle wrapper in your app project (run
gradle wrapperif missing) - Signing keystore:
signing/localstore.keystore(aliaslocalstore; store the keystore password out of band, e.g. in your password manager) - Server running on port 9800
Step 1: Build the APK
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export ANDROID_HOME=/home/jarian/android-sdk
cd /path/to/your/app/project
./gradlew :app:assembleRelease
The signed release APK will be at app/build/outputs/apk/release/app-release.apk.
Step 2: Copy APK to repos
cp app/build/outputs/apk/release/app-release.apk /home/jarian/playground/app-store/server/repos/<app_id>.apk
Use a lowercase, hyphen-separated ID (e.g., myapp, hello-world).
Step 3: Create metadata JSON
Create <app_id>.json in the repos directory alongside the APK:
{
"name": "App Display Name",
"description": "One or two sentences describing the app.",
"icon": null,
"screenshots": [],
"package_name": "com.your.package",
"version_name": "1.0",
"version_code": "1",
"min_sdk": "26",
"target_sdk": "34",
"permissions": ["android.permission.INTERNET"]
}
Field notes:
name: Human-readable display namedescription: Short description shown in the storeicon:nullif no PNG icon in the APK, otherwise the resource path (e.g.,res/drawable-mdpi/ic_launcher.png)screenshots: Array of screenshot filenames, or[]if nonepackage_name: FromAndroidManifest.xmlorbuild.gradleversion_name/version_code: Frombuild.gradledefaultConfigmin_sdk/target_sdk: Frombuild.gradledefaultConfigpermissions: Array of<uses-permission>entries fromAndroidManifest.xml
Step 4: Trigger server scan
curl -X POST http://localhost:9800/api/scan
Or restart the server if preferred:
screen -S appstore -X quit 2>/dev/null
sleep 1
screen -dmS appstore bash -c "cd /home/jarian/playground/app-store/server && source venv/bin/activate && python -c 'import uvicorn, sys; sys.path.insert(0, \".\"); from main import app; uvicorn.run(app, host=\"0.0.0.0\", port=9800)'"
Step 5: Verify
curl -s http://localhost:9800/api/apps | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f'Total: {data[\"total\"]} apps')
for app in data['apps']:
print(f' - {app[\"name\"]} (v{app[\"version_name\"]}, pkg={app[\"package_name\"]})')
"
Optional: Add to downloads directory
For easy browser download via /dl/ endpoint:
cp app/build/outputs/apk/release/app-release.apk /home/jarian/playground/app-store/server/downloads/<app_id>.apk
Common Issues
- Binary XML manifest parsing: The scanner reads APKs directly without
aapt. Fields likeversion_namemay showunknownif the string parser misses them — always set them in the JSON override. - No PNG icon: If the APK only has XML vector drawables, set
"icon": nullin the JSON. - KSP plugin: Required for Room. Keep only in
app/build.gradle, not rootbuild.gradle. - Navigation: Use
<fragment>tag (notFrameLayout) withandroid:name="androidx.navigation.fragment.NavHostFragment", or add NavHostFragment programmatically in MainActivity. - Data binding: Cannot create typed properties for
<fragment>tags. Use programmatic NavHostFragment instead. - Lint: Set
checkReleaseBuilds falseto avoid blocking builds.