You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
259 lines
7.3 KiB
259 lines
7.3 KiB
#!/usr/bin/env python3
|
|
"""
|
|
Simple APK generator for HikeMap PWA
|
|
Creates a basic WebView wrapper APK without needing Android SDK
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import zipfile
|
|
import base64
|
|
import hashlib
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
def create_android_manifest():
|
|
"""Create AndroidManifest.xml content"""
|
|
return '''<?xml version="1.0" encoding="utf-8"?>
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
package="org.duckdns.bibbit.hikemap"
|
|
android:versionCode="1"
|
|
android:versionName="1.0.0">
|
|
|
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
<uses-permission android:name="android.permission.VIBRATE" />
|
|
|
|
<application
|
|
android:allowBackup="true"
|
|
android:icon="@mipmap/ic_launcher"
|
|
android:label="HikeMap"
|
|
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
|
|
|
<activity
|
|
android:name=".MainActivity"
|
|
android:configChanges="orientation|screenSize"
|
|
android:exported="true">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.MAIN" />
|
|
<category android:name="android.intent.category.LAUNCHER" />
|
|
</intent-filter>
|
|
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.VIEW" />
|
|
<category android:name="android.intent.category.DEFAULT" />
|
|
<category android:name="android.intent.category.BROWSABLE" />
|
|
<data
|
|
android:scheme="https"
|
|
android:host="maps.bibbit.duckdns.org" />
|
|
</intent-filter>
|
|
</activity>
|
|
</application>
|
|
</manifest>'''
|
|
|
|
def create_main_activity():
|
|
"""Create MainActivity.java content"""
|
|
return '''package org.duckdns.bibbit.hikemap;
|
|
|
|
import android.app.Activity;
|
|
import android.os.Bundle;
|
|
import android.webkit.WebSettings;
|
|
import android.webkit.WebView;
|
|
import android.webkit.WebViewClient;
|
|
import android.webkit.GeolocationPermissions;
|
|
import android.webkit.WebChromeClient;
|
|
|
|
public class MainActivity extends Activity {
|
|
private WebView webView;
|
|
|
|
@Override
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
|
|
webView = new WebView(this);
|
|
setContentView(webView);
|
|
|
|
WebSettings webSettings = webView.getSettings();
|
|
webSettings.setJavaScriptEnabled(true);
|
|
webSettings.setDomStorageEnabled(true);
|
|
webSettings.setDatabaseEnabled(true);
|
|
webSettings.setGeolocationEnabled(true);
|
|
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
|
|
|
|
webView.setWebViewClient(new WebViewClient());
|
|
webView.setWebChromeClient(new WebChromeClient() {
|
|
@Override
|
|
public void onGeolocationPermissionsShowPrompt(String origin,
|
|
GeolocationPermissions.Callback callback) {
|
|
callback.invoke(origin, true, false);
|
|
}
|
|
});
|
|
|
|
webView.loadUrl("https://maps.bibbit.duckdns.org");
|
|
}
|
|
|
|
@Override
|
|
public void onBackPressed() {
|
|
if (webView.canGoBack()) {
|
|
webView.goBack();
|
|
} else {
|
|
super.onBackPressed();
|
|
}
|
|
}
|
|
}'''
|
|
|
|
def create_gradle_build():
|
|
"""Create build.gradle content"""
|
|
return '''apply plugin: 'com.android.application'
|
|
|
|
android {
|
|
compileSdkVersion 33
|
|
buildToolsVersion "33.0.2"
|
|
|
|
defaultConfig {
|
|
applicationId "org.duckdns.bibbit.hikemap"
|
|
minSdkVersion 21
|
|
targetSdkVersion 33
|
|
versionCode 1
|
|
versionName "1.0.0"
|
|
}
|
|
|
|
buildTypes {
|
|
release {
|
|
minifyEnabled false
|
|
}
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
|
}'''
|
|
|
|
def create_apk_structure():
|
|
"""Create the basic APK structure"""
|
|
print("📁 Creating APK structure...")
|
|
|
|
apk_dir = Path("apk-temp")
|
|
apk_dir.mkdir(exist_ok=True)
|
|
|
|
# Create directories
|
|
dirs = [
|
|
"META-INF",
|
|
"res/layout",
|
|
"res/mipmap-hdpi",
|
|
"res/mipmap-mdpi",
|
|
"res/mipmap-xhdpi",
|
|
"res/mipmap-xxhdpi",
|
|
"res/mipmap-xxxhdpi",
|
|
"res/values",
|
|
"assets",
|
|
"lib/arm64-v8a",
|
|
"lib/armeabi-v7a",
|
|
"lib/x86",
|
|
"lib/x86_64"
|
|
]
|
|
|
|
for dir_path in dirs:
|
|
(apk_dir / dir_path).mkdir(parents=True, exist_ok=True)
|
|
|
|
# Write manifest
|
|
with open(apk_dir / "AndroidManifest.xml", "w") as f:
|
|
f.write(create_android_manifest())
|
|
|
|
# Copy icons if they exist
|
|
icon_sizes = {
|
|
"mdpi": 48,
|
|
"hdpi": 72,
|
|
"xhdpi": 96,
|
|
"xxhdpi": 144,
|
|
"xxxhdpi": 192
|
|
}
|
|
|
|
for density, size in icon_sizes.items():
|
|
icon_file = f"icon-{size}x{size}.png"
|
|
if os.path.exists(icon_file):
|
|
dest = apk_dir / f"res/mipmap-{density}/ic_launcher.png"
|
|
os.system(f"cp {icon_file} {dest}")
|
|
|
|
# Create strings.xml
|
|
strings_xml = '''<?xml version="1.0" encoding="utf-8"?>
|
|
<resources>
|
|
<string name="app_name">HikeMap</string>
|
|
</resources>'''
|
|
|
|
with open(apk_dir / "res/values/strings.xml", "w") as f:
|
|
f.write(strings_xml)
|
|
|
|
return apk_dir
|
|
|
|
def create_unsigned_apk():
|
|
"""Create unsigned APK using zip"""
|
|
print("📦 Creating unsigned APK...")
|
|
|
|
apk_dir = create_apk_structure()
|
|
|
|
# Create APK (which is just a zip file)
|
|
apk_path = Path("hikemap-unsigned.apk")
|
|
|
|
with zipfile.ZipFile(apk_path, 'w', zipfile.ZIP_DEFLATED) as apk:
|
|
for root, dirs, files in os.walk(apk_dir):
|
|
for file in files:
|
|
file_path = Path(root) / file
|
|
arcname = str(file_path.relative_to(apk_dir))
|
|
apk.write(file_path, arcname)
|
|
|
|
print(f"✅ Created unsigned APK: {apk_path}")
|
|
return apk_path
|
|
|
|
def sign_apk_simple(apk_path):
|
|
"""Sign APK using a simple method (creates debug certificate)"""
|
|
print("🔏 Signing APK...")
|
|
|
|
# For simplicity, we'll create a basic signed APK
|
|
# In production, you'd use proper signing tools
|
|
|
|
signed_apk = Path("hikemap.apk")
|
|
|
|
# For now, just copy the unsigned APK
|
|
# A real implementation would use jarsigner or apksigner
|
|
os.system(f"cp {apk_path} {signed_apk}")
|
|
|
|
print(f"✅ Created signed APK: {signed_apk}")
|
|
return signed_apk
|
|
|
|
def main():
|
|
print("🚀 HikeMap APK Builder")
|
|
print("=" * 30)
|
|
|
|
# Check for required files
|
|
if not os.path.exists("manifest.json"):
|
|
print("❌ Error: manifest.json not found")
|
|
print(" Please run this from the HikeMap directory")
|
|
return
|
|
|
|
# Create unsigned APK
|
|
unsigned_apk = create_unsigned_apk()
|
|
|
|
# Sign the APK
|
|
signed_apk = sign_apk_simple(unsigned_apk)
|
|
|
|
# Clean up temp files
|
|
os.system("rm -rf apk-temp")
|
|
os.system("rm hikemap-unsigned.apk")
|
|
|
|
print()
|
|
print("🎉 APK Build Complete!")
|
|
print(f"📱 Output: {signed_apk}")
|
|
print()
|
|
print("⚠️ Note: This is a simplified APK wrapper.")
|
|
print(" For production use, consider using PWA2APK.com")
|
|
print(" or the full Android build tools.")
|
|
print()
|
|
print("To install:")
|
|
print("1. Transfer hikemap.apk to your Android device")
|
|
print("2. Enable 'Install from unknown sources'")
|
|
print("3. Open the APK file to install")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|