- scanner.py: replace heuristic binary manifest parser with aapt/aapt2 (fixes #2, #7) - main.py: add ZIP magic bytes validation on APK upload (fixes #8) - main.py: sanitize screenshot filename with os.path.basename + resolve() check (fixes #6) - main.py: add threading.Lock + TTL to in-memory cache (fixes #4, #9) - SettingsManager.kt: default port 9800 -> 8080 to match config.yaml (fixes #3) - main.py: parse_manifest_bytes for in-memory APK validation on upload
Local App Store
A private Android app store for your local network. Serve APKs from your own repository and install them on your devices via the Android app or browse/download from any browser.
Architecture
- Server: FastAPI application that serves APKs and metadata over HTTP
- Android Client: Kotlin app with Material Design 3 UI, Room database, and PackageInstaller integration
- Web Frontend: Single-page HTML/JS app served by the server, accessible from any browser
- Signing: Self-signed keystore for both the store app and distributed apps
Quick Start
1. Server Setup
cd server
# Activate virtual environment
source venv/bin/activate
# Install dependencies (if not already done)
pip install -r requirements.txt
# Drop your APKs into the repos/ directory
cp /path/to/your/app.apk repos/
# Start the server (runs on all interfaces, port 8080)
python main.py
The server auto-scans repos/ on startup. To rescan after adding new APKs:
curl -X POST http://localhost:8080/api/scan
You can also add app metadata (name, description, icon, screenshots) by creating a JSON file alongside each APK:
repos/
myapp.apk
myapp.json # Optional metadata override
myapp_screenshots/ # Optional screenshots (*.png)
Example myapp.json:
{
"name": "My App",
"description": "A great app for doing things",
"icon": "res/mipmap-hdpi/ic_launcher.png",
"screenshots": ["screenshot1.png", "screenshot2.png"]
}
2. Web Frontend
Once the server is running, open any browser and navigate to:
http://<server-ip>:8080
The web frontend provides:
- Browse all apps in a responsive grid
- Search apps by name or description
- View app details (description, permissions, screenshots)
- Download APK files directly
- Scan repository for new apps
3. Signing APKs
cd signing
# Generate keystore (already done - localstore.keystore exists)
# To regenerate: ./generate-key.sh
# Sign an APK before adding it to repos/
./sign-apk.sh /path/to/your/app.apk
All APKs should be signed with the same keystore for consistency.
4. Building the Android Client
Prerequisites: Java 17, Android SDK (platform-tools, build-tools 34.0.0, platforms android-34).
cd android
# Configure SDK path (adjust to your SDK location)
echo "sdk.dir=/path/to/android-sdk" > local.properties
# Build debug APK
./gradlew :app:assembleDebug
# Build release APK (signed with localstore.keystore)
./gradlew :app:assembleRelease
APKs are output to app/build/outputs/apk/.
5. Installing on Android Device
# Connect device via USB or use wireless ADB
# Install debug build
/home/jarian/android-sdk/platform-tools/adb install app/build/outputs/apk/debug/app-debug.apk
# Install release build
/home/jarian/android-sdk/platform-tools/adb install app/build/outputs/apk/release/app-release.apk
First time setup in the app:
- Open the app -> go to Settings
- Enter your server IP address (e.g.,
192.168.8.128) - Click "Test Connection" to verify
- Click "Save"
- Grant "Install unknown apps" permission when prompted
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | / |
Web frontend |
| GET | /api/apps |
List all apps (supports ?search=, ?offset=, ?limit=) |
| GET | /api/apps/{id} |
Get app details |
| GET | /api/apps/{id}/download |
Download APK file |
| GET | /api/apps/{id}/icon |
Get app icon (128x128 PNG) |
| GET | /api/apps/{id}/screenshots |
List screenshots |
| GET | /api/apps/{id}/screenshots/{file} |
Get screenshot image |
| POST | /api/apps/update-check |
Check for updates |
| POST | /api/scan |
Trigger repository rescan |
| GET | /api/status |
Server status info |
Configuration
Server config is in server/config.yaml:
server:
host: "0.0.0.0" # Bind to all interfaces
port: 8080 # HTTP port
repository:
path: "./repos" # APK storage directory
Project Structure
app-store/
server/
main.py # FastAPI application + web frontend
app.py # Pydantic models
scanner.py # APK metadata scanner
config.yaml # Server configuration
requirements.txt # Python dependencies
venv/ # Python virtual environment
repos/ # Drop APKs here
web/
index.html # Web frontend (HTML/CSS/JS)
android/ # Gradle project (CLI build)
build.gradle # Root build config
settings.gradle # Project settings
local.properties # SDK path
app/
build.gradle # App build config (KSP, Room, etc.)
src/main/
AndroidManifest.xml
kotlin/com/localstore/
LocalStoreApp.kt
data/ # API, models, database, repository, settings
ui/ # Fragments, ViewModels, adapters
installer/ # PackageInstaller wrapper
res/ # Layouts, strings, themes, navigation, colors
gradle/ # Gradle wrapper
signing/
generate-key.sh # Create keystore
sign-apk.sh # Sign APKs
localstore.keystore # Self-signed keystore
app-debug.apk # Debug build
app-release.apk # Release build (signed)
README.md
Android Permissions
The app requires:
INTERNET- connect to the serverREQUEST_INSTALL_PACKAGES- install APKs (user grants in Settings)ACCESS_NETWORK_STATE- check connectivity
Security Notes
- The keystore is self-signed. For production use, consider a proper CA-signed certificate.
- The server serves over HTTP. For HTTPS, configure a reverse proxy (nginx/Caddy) with Let's Encrypt.
- All APKs should be signed with the same keystore to ensure trust chain.
Troubleshooting
Apps not appearing after adding to repos/
- Run
POST /api/scanor restart the server
Icon not loading
- The server extracts icons from APK resources. If extraction fails, specify icon path in the
.jsonmetadata file.
Installation fails
- Ensure "Install unknown apps" permission is granted for the app store app in Android Settings
Connection fails
- Check server IP in Settings matches your server's actual IP
- Ensure both devices are on the same network
- Check firewall allows port 8080
Gradle build fails
- Ensure Java 17 is installed and JAVA_HOME is set
- Ensure Android SDK is installed and
local.propertiespoints to it - Run
./gradlew :app:cleanbefore rebuilding