Initial commit: RR3 OTA Update System
Complete standalone package for adding OTA (Over-The-Air) auto-updates to Real Racing 3 APK mods. Features: - Manifest-based version control (versions.json) - Prevents unwanted major version jumps - Material Design WebView UI - WiFi/mobile data download options - Preserves user data during updates - Multi-version channel support Package Contents: - UpdateManager.java - Source code implementation - UpdateManager.smali - Compiled smali for APK integration - community_update_checker.html - Material Design UI - README.md - Complete documentation - INTEGRATION-GUIDE.md - Step-by-step integration instructions Ready for integration into any RR3 APK version (v15, v14, v13, etc.) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
505
INTEGRATION-GUIDE.md
Normal file
505
INTEGRATION-GUIDE.md
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
# 🔧 RR3 OTA Integration Guide
|
||||||
|
|
||||||
|
**Step-by-step guide to integrate the OTA update system into any RR3 APK**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Prerequisites
|
||||||
|
|
||||||
|
### Tools Needed
|
||||||
|
|
||||||
|
- **apktool** - For decompiling/recompiling APKs
|
||||||
|
- **Java JDK 8+** - For compiling Java to class files
|
||||||
|
- **dx tool** (Android SDK) - For converting class to dex
|
||||||
|
- **baksmali/smali** - For dex to smali conversion
|
||||||
|
- **zipalign** - For optimizing APK
|
||||||
|
- **apksigner** - For signing APK
|
||||||
|
- **Text editor** - For editing files
|
||||||
|
|
||||||
|
### Install Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# apktool
|
||||||
|
wget https://github.com/iBotPeaches/Apktool/releases/latest/download/apktool.jar
|
||||||
|
|
||||||
|
# Android SDK (includes dx, zipalign, apksigner)
|
||||||
|
# Download from: https://developer.android.com/studio
|
||||||
|
|
||||||
|
# baksmali/smali
|
||||||
|
wget https://github.com/JesusFreke/smali/releases/latest/download/baksmali.jar
|
||||||
|
wget https://github.com/JesusFreke/smali/releases/latest/download/smali.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Integration Steps
|
||||||
|
|
||||||
|
### Step 1: Decompile Your APK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Decompile RR3 APK
|
||||||
|
apktool d your-rr3.apk -o rr3-decompiled
|
||||||
|
|
||||||
|
cd rr3-decompiled
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output structure:**
|
||||||
|
```
|
||||||
|
rr3-decompiled/
|
||||||
|
├── AndroidManifest.xml
|
||||||
|
├── smali/
|
||||||
|
├── assets/
|
||||||
|
├── res/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Add UpdateManager
|
||||||
|
|
||||||
|
#### Option A: Use Precompiled Smali (Easier)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create directory
|
||||||
|
mkdir -p smali/com/community
|
||||||
|
|
||||||
|
# Copy UpdateManager.smali
|
||||||
|
cp /path/to/rr3-ota/UpdateManager.smali smali/com/community/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option B: Compile from Java (More Control)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compile Java to class
|
||||||
|
javac -source 1.8 -target 1.8 \
|
||||||
|
-cp android.jar:org.json.jar \
|
||||||
|
UpdateManager.java
|
||||||
|
|
||||||
|
# Convert class to dex
|
||||||
|
dx --dex --output=UpdateManager.dex UpdateManager.class
|
||||||
|
|
||||||
|
# Convert dex to smali
|
||||||
|
baksmali d UpdateManager.dex -o smali-output
|
||||||
|
|
||||||
|
# Copy to APK
|
||||||
|
mkdir -p smali/com/community
|
||||||
|
cp smali-output/com/community/UpdateManager.smali smali/com/community/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Important: Update Version Constants
|
||||||
|
|
||||||
|
Edit `smali/com/community/UpdateManager.smali`:
|
||||||
|
|
||||||
|
**Find these lines:**
|
||||||
|
```smali
|
||||||
|
.field private static final CURRENT_VERSION:Ljava/lang/String; = "15.0.0-community-alpha"
|
||||||
|
|
||||||
|
.field private static final CURRENT_VERSION_CODE:I = 0x249f0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Change to your version:**
|
||||||
|
```smali
|
||||||
|
.field private static final CURRENT_VERSION:Ljava/lang/String; = "14.5.0" # Your version
|
||||||
|
|
||||||
|
.field private static final CURRENT_VERSION_CODE:I = 0x23AB8 # 145000 in hex
|
||||||
|
```
|
||||||
|
|
||||||
|
**Calculate version code:**
|
||||||
|
```
|
||||||
|
versionCode = (major * 100000) + (minor * 1000) + patch
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
14.5.0 → 145000 → 0x23AB8 (hex)
|
||||||
|
15.0.0 → 150000 → 0x249F0 (hex)
|
||||||
|
|
||||||
|
# Convert to hex: printf '%x\n' 145000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update Manifest URL
|
||||||
|
|
||||||
|
**Find this line:**
|
||||||
|
```smali
|
||||||
|
.field private static final UPDATE_API_URL:Ljava/lang/String; = "https://raw.githubusercontent.com/project-real-resurrection-3/rr3-releases/main/versions.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Change to your manifest URL:**
|
||||||
|
```smali
|
||||||
|
.field private static final UPDATE_API_URL:Ljava/lang/String; = "https://raw.githubusercontent.com/YOUR-ORG/YOUR-REPO/main/versions.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: Add HTML UI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy update checker HTML
|
||||||
|
cp /path/to/rr3-ota/community_update_checker.html assets/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify placement:**
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
└── community_update_checker.html
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4: Add Permissions
|
||||||
|
|
||||||
|
Edit `AndroidManifest.xml`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<manifest>
|
||||||
|
<!-- Add these permissions -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
|
<!-- Rest of manifest -->
|
||||||
|
</manifest>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5: Integrate into Your UI
|
||||||
|
|
||||||
|
You need to add a button/menu item that triggers the update checker.
|
||||||
|
|
||||||
|
#### Example: Add to Main Menu WebView
|
||||||
|
|
||||||
|
**Find your main menu HTML file** (usually in `assets/`):
|
||||||
|
```bash
|
||||||
|
# Search for HTML files
|
||||||
|
find assets/ -name "*.html" | grep -i menu
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add update button:**
|
||||||
|
```html
|
||||||
|
<!-- Add to your menu -->
|
||||||
|
<button onclick="checkForUpdates()" class="menu-button">
|
||||||
|
🔄 Check for Updates
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function checkForUpdates() {
|
||||||
|
window.location.href = 'file:///android_asset/community_update_checker.html';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add WebView Bridge
|
||||||
|
|
||||||
|
**Find the Activity that creates your WebView** (in `smali/`):
|
||||||
|
|
||||||
|
Common locations:
|
||||||
|
- `smali/com/ea/games/*/MainActivity.smali`
|
||||||
|
- `smali/com/ea/games/*/WebViewActivity.smali`
|
||||||
|
|
||||||
|
**Find where WebView is created:**
|
||||||
|
```smali
|
||||||
|
# Look for this pattern
|
||||||
|
new-instance v0, Landroid/webkit/WebView;
|
||||||
|
invoke-direct {v0, p0}, Landroid/webkit/WebView;-><init>(Landroid/content/Context;)V
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add JavaScript interface after WebView creation:**
|
||||||
|
```smali
|
||||||
|
# Create UpdateManager instance
|
||||||
|
new-instance v1, Lcom/community/UpdateManager;
|
||||||
|
move-object v2, p0 # context
|
||||||
|
invoke-direct {v1, v2}, Lcom/community/UpdateManager;-><init>(Landroid/content/Context;)V
|
||||||
|
|
||||||
|
# Add as JavaScript interface
|
||||||
|
const-string v2, "UpdateManager"
|
||||||
|
invoke-virtual {v0, v1, v2}, Landroid/webkit/WebView;->addJavascriptInterface(Ljava/lang/Object;Ljava/lang/String;)V
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 6: Recompile APK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build APK
|
||||||
|
apktool b rr3-decompiled -o rr3-modified-unsigned.apk
|
||||||
|
|
||||||
|
# Zipalign
|
||||||
|
zipalign -p -f -v 4 rr3-modified-unsigned.apk rr3-modified-aligned.apk
|
||||||
|
|
||||||
|
# Sign APK (create keystore if needed)
|
||||||
|
# Create keystore (first time only):
|
||||||
|
keytool -genkey -v -keystore rr3.keystore -alias rr3-key -keyalg RSA -keysize 2048 -validity 10000
|
||||||
|
|
||||||
|
# Sign
|
||||||
|
apksigner sign --ks rr3.keystore --ks-key-alias rr3-key \
|
||||||
|
--out rr3-modified-signed.apk \
|
||||||
|
rr3-modified-aligned.apk
|
||||||
|
|
||||||
|
# Verify signature
|
||||||
|
apksigner verify rr3-modified-signed.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Final APK:** `rr3-modified-signed.apk`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 7: Test Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install on device
|
||||||
|
adb install rr3-modified-signed.apk
|
||||||
|
|
||||||
|
# Or manually:
|
||||||
|
# 1. Transfer APK to device
|
||||||
|
# 2. Enable "Install from Unknown Sources"
|
||||||
|
# 3. Tap APK to install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing the Integration
|
||||||
|
|
||||||
|
### Test 1: WebView Bridge
|
||||||
|
|
||||||
|
Open your app and run in Android Studio logcat:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb logcat | grep UpdateManager
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** Should see `UpdateManager` logs when app starts.
|
||||||
|
|
||||||
|
### Test 2: UI Button
|
||||||
|
|
||||||
|
1. Open your modified menu
|
||||||
|
2. Click "Check for Updates" button
|
||||||
|
3. Should navigate to update checker page
|
||||||
|
|
||||||
|
**If it doesn't work:**
|
||||||
|
- Check `assets/community_update_checker.html` exists
|
||||||
|
- Verify path: `file:///android_asset/community_update_checker.html`
|
||||||
|
|
||||||
|
### Test 3: Update Check
|
||||||
|
|
||||||
|
1. Click "Check for Updates" in the UI
|
||||||
|
2. Watch logcat: `adb logcat | grep UpdateManager`
|
||||||
|
|
||||||
|
**Expected logs:**
|
||||||
|
```
|
||||||
|
I/UpdateManager: Checking for updates from manifest...
|
||||||
|
I/UpdateManager: No update available
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if update exists:
|
||||||
|
```
|
||||||
|
I/UpdateManager: Update available: 15.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 4: Download
|
||||||
|
|
||||||
|
1. Create a test release on GitHub
|
||||||
|
2. Update your `versions.json` manifest
|
||||||
|
3. Check for updates in app
|
||||||
|
4. Click "Download Update"
|
||||||
|
5. Verify download starts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Issue: "Class not found: UpdateManager"
|
||||||
|
|
||||||
|
**Cause:** UpdateManager.smali not in correct location
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
# Verify file exists
|
||||||
|
ls -la smali/com/community/UpdateManager.smali
|
||||||
|
|
||||||
|
# Should be:
|
||||||
|
# smali/com/community/UpdateManager.smali
|
||||||
|
# NOT: smali/UpdateManager.smali
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "JavaScript interface not found"
|
||||||
|
|
||||||
|
**Cause:** WebView bridge not added or incorrect
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
- Verify you added the JavaScript interface code
|
||||||
|
- Check the interface name matches: `"UpdateManager"`
|
||||||
|
- Make sure it's added BEFORE WebView loads content
|
||||||
|
|
||||||
|
### Issue: "Update check returns error"
|
||||||
|
|
||||||
|
**Cause:** Manifest URL incorrect or not accessible
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
# Test manifest URL manually
|
||||||
|
curl https://raw.githubusercontent.com/YOUR-ORG/YOUR-REPO/main/versions.json
|
||||||
|
|
||||||
|
# Should return JSON, not 404
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "Version code mismatch"
|
||||||
|
|
||||||
|
**Cause:** Incorrect version code calculation
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
# Calculate version code
|
||||||
|
echo "scale=0; (14*100000) + (5*1000) + 0" | bc
|
||||||
|
# Result: 145000
|
||||||
|
|
||||||
|
# Convert to hex
|
||||||
|
printf '%x\n' 145000
|
||||||
|
# Result: 23ab8
|
||||||
|
|
||||||
|
# Update in smali:
|
||||||
|
.field private static final CURRENT_VERSION_CODE:I = 0x23ab8
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "APK won't install"
|
||||||
|
|
||||||
|
**Cause:** Signature mismatch or corrupted APK
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
# Verify APK
|
||||||
|
apksigner verify rr3-modified-signed.apk
|
||||||
|
|
||||||
|
# If invalid, rebuild and re-sign
|
||||||
|
apktool b rr3-decompiled -o new.apk
|
||||||
|
zipalign -p 4 new.apk aligned.apk
|
||||||
|
apksigner sign --ks rr3.keystore aligned.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Platform-Specific Notes
|
||||||
|
|
||||||
|
### Android 11+ (API 30+)
|
||||||
|
|
||||||
|
**File access changes:** Need to use scoped storage
|
||||||
|
|
||||||
|
**In UpdateManager.smali, update download destination:**
|
||||||
|
```java
|
||||||
|
// Old (doesn't work on Android 11+):
|
||||||
|
Environment.DIRECTORY_DOWNLOADS
|
||||||
|
|
||||||
|
// New (works on all versions):
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android 8+ (API 26+)
|
||||||
|
|
||||||
|
**Install unknown apps permission required**
|
||||||
|
|
||||||
|
Add to `AndroidManifest.xml`:
|
||||||
|
```xml
|
||||||
|
<application>
|
||||||
|
<activity android:name=".MainActivity">
|
||||||
|
<!-- Add this -->
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.install_package"
|
||||||
|
android:value="true" />
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security Checklist
|
||||||
|
|
||||||
|
Before releasing:
|
||||||
|
|
||||||
|
- [ ] Version constants updated
|
||||||
|
- [ ] Manifest URL points to your repository
|
||||||
|
- [ ] APK signed with your keystore (not debug key!)
|
||||||
|
- [ ] Permissions added to AndroidManifest.xml
|
||||||
|
- [ ] SHA-256 checksums in manifest (optional but recommended)
|
||||||
|
- [ ] HTTPS used for all URLs
|
||||||
|
- [ ] Tested on multiple Android versions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Version Manifest Setup
|
||||||
|
|
||||||
|
After integration, you need a manifest:
|
||||||
|
|
||||||
|
### 1. Create GitHub Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create releases repository
|
||||||
|
gh repo create YOUR-ORG/rr3-releases --public
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create versions.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"last_updated": "2026-02-24T00:00:00Z",
|
||||||
|
"channels": {
|
||||||
|
"stable": {
|
||||||
|
"description": "Stable releases",
|
||||||
|
"latest": "14.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "14.5.0",
|
||||||
|
"version_code": 145000,
|
||||||
|
"channel": "stable",
|
||||||
|
"release_date": "2026-02-24",
|
||||||
|
"min_android": 21,
|
||||||
|
"target_android": 33,
|
||||||
|
"download_url": "https://github.com/YOUR-ORG/rr3-releases/releases/download/v14.5.0/RR3-v14.5.0.apk",
|
||||||
|
"file_size": 230000000,
|
||||||
|
"sha256": "your-sha256-here",
|
||||||
|
"changelog": "## What's New\n- Your changelog here",
|
||||||
|
"upgrade_from": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Upload to GitHub
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add versions.json
|
||||||
|
git commit -m "Add version manifest"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Verify Accessibility
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://raw.githubusercontent.com/YOUR-ORG/rr3-releases/main/versions.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 You're Done!
|
||||||
|
|
||||||
|
Your RR3 APK now has a fully functional OTA update system!
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
1. Release your first version on GitHub
|
||||||
|
2. Update versions.json when new versions are ready
|
||||||
|
3. Users will automatically see updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Need Help?
|
||||||
|
|
||||||
|
- 📖 **Full documentation:** [README.md](./README.md)
|
||||||
|
- 🐛 **Report issues:** GitHub Issues
|
||||||
|
- 💬 **Community:** Discord (link TBA)
|
||||||
|
- 📧 **Contact:** [@ssfdre38](https://github.com/ssfdre38)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy integrating! 🔄🏁**
|
||||||
474
README.md
Normal file
474
README.md
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
# 🔄 RR3 OTA Update System
|
||||||
|
|
||||||
|
**Standalone OTA (Over-The-Air) update system for Real Racing 3 Community Edition**
|
||||||
|
|
||||||
|
This package provides a complete, drop-in OTA update system that can be integrated into any RR3 APK version (EA's official or community editions).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 What's Included
|
||||||
|
|
||||||
|
```
|
||||||
|
rr3-ota/
|
||||||
|
├── UpdateManager.java (Main update logic - manifest-based)
|
||||||
|
├── UpdateManager.smali (Compiled smali version)
|
||||||
|
├── community_update_checker.html (WebView UI for updates)
|
||||||
|
├── README.md (This file)
|
||||||
|
└── INTEGRATION-GUIDE.md (Step-by-step integration)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🔄 **Manifest-based updates** - Full control over upgrade paths
|
||||||
|
- 📡 **GitHub integration** - Uses GitHub Releases for hosting
|
||||||
|
- 🌐 **Network flexibility** - WiFi or mobile data support
|
||||||
|
- 📊 **Progress tracking** - Real-time download progress
|
||||||
|
- 🎨 **Material Design UI** - Beautiful WebView interface
|
||||||
|
- ⚙️ **User preferences** - WiFi-only mode, auto-check settings
|
||||||
|
- 🔒 **Version control** - Prevents unwanted major version jumps
|
||||||
|
- 📱 **Android 6.0+** - Compatible with API 23+
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 How It Works
|
||||||
|
|
||||||
|
### Manifest System
|
||||||
|
|
||||||
|
Instead of relying on GitHub's `/releases/latest` API, this system uses a **version manifest** (`versions.json`) that you control:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "15.1.0",
|
||||||
|
"version_code": 151000,
|
||||||
|
"download_url": "https://github.com/.../RR3-v15.1.0.apk",
|
||||||
|
"upgrade_from": ["15.0.0", "15.0.1"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ You control which versions get updates
|
||||||
|
- ✅ Users on v14.x won't be forced to v15.x
|
||||||
|
- ✅ Can create multiple update channels (stable, beta, legacy)
|
||||||
|
- ✅ Easy to manage (edit single JSON file)
|
||||||
|
|
||||||
|
### Update Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User opens app
|
||||||
|
↓
|
||||||
|
2. UpdateManager fetches versions.json
|
||||||
|
↓
|
||||||
|
3. Finds applicable update (checks upgrade_from)
|
||||||
|
↓
|
||||||
|
4. Shows update dialog with changelog
|
||||||
|
↓
|
||||||
|
5. User clicks Download
|
||||||
|
↓
|
||||||
|
6. DownloadManager downloads APK
|
||||||
|
↓
|
||||||
|
7. Installation prompt (preserves user data)
|
||||||
|
↓
|
||||||
|
8. Updated!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Integration
|
||||||
|
|
||||||
|
### For RR3 Community Edition (Already Integrated)
|
||||||
|
|
||||||
|
If you're building RR3 Community Edition from our repository:
|
||||||
|
- ✅ UpdateManager is already patched in
|
||||||
|
- ✅ HTML UI already in assets
|
||||||
|
- ✅ Just build the APK
|
||||||
|
|
||||||
|
### For Other RR3 Versions (Integration Required)
|
||||||
|
|
||||||
|
Follow the detailed guide: **[INTEGRATION-GUIDE.md](./INTEGRATION-GUIDE.md)**
|
||||||
|
|
||||||
|
**Quick steps:**
|
||||||
|
1. Decompile your RR3 APK with apktool
|
||||||
|
2. Add `UpdateManager.smali` to `smali/com/community/`
|
||||||
|
3. Add `community_update_checker.html` to `assets/`
|
||||||
|
4. Add "Check for Updates" button to your UI
|
||||||
|
5. Recompile and sign APK
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Configuration
|
||||||
|
|
||||||
|
### Update Version Constants
|
||||||
|
|
||||||
|
Edit `UpdateManager.java` (or `.smali`) before building:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// IMPORTANT: Update these for each build
|
||||||
|
private static final String CURRENT_VERSION = "15.0.0-community-alpha";
|
||||||
|
private static final int CURRENT_VERSION_CODE = 150000;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Version Code Formula:**
|
||||||
|
```
|
||||||
|
versionCode = (major * 100000) + (minor * 1000) + patch
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
15.0.0 → 150000
|
||||||
|
15.1.0 → 151000
|
||||||
|
14.5.2 → 145200
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manifest URL
|
||||||
|
|
||||||
|
The manifest URL is hardcoded in UpdateManager:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private static final String UPDATE_API_URL =
|
||||||
|
"https://raw.githubusercontent.com/YOUR-ORG/YOUR-REPO/main/versions.json";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Change this to your own manifest location!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Version Manifest Setup
|
||||||
|
|
||||||
|
### 1. Create versions.json
|
||||||
|
|
||||||
|
Create a `versions.json` file in your releases repository:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"last_updated": "2026-02-24T04:00:00Z",
|
||||||
|
"channels": {
|
||||||
|
"stable": {
|
||||||
|
"description": "Stable releases",
|
||||||
|
"latest": "15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "15.0.0",
|
||||||
|
"version_code": 150000,
|
||||||
|
"channel": "stable",
|
||||||
|
"release_date": "2026-02-24",
|
||||||
|
"min_android": 23,
|
||||||
|
"target_android": 34,
|
||||||
|
"download_url": "https://github.com/YOUR-ORG/releases/download/v15.0.0/RR3-v15.0.0.apk",
|
||||||
|
"file_size": 240000000,
|
||||||
|
"sha256": "abc123...",
|
||||||
|
"changelog": "## Initial Release\n- Feature 1\n- Feature 2",
|
||||||
|
"upgrade_from": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Host on GitHub
|
||||||
|
|
||||||
|
Upload `versions.json` to your GitHub repository (main branch, root or any folder).
|
||||||
|
|
||||||
|
**Access URL:**
|
||||||
|
```
|
||||||
|
https://raw.githubusercontent.com/YOUR-ORG/YOUR-REPO/main/versions.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add New Versions
|
||||||
|
|
||||||
|
When releasing a new version:
|
||||||
|
|
||||||
|
1. **Create GitHub Release** (hosts APK file)
|
||||||
|
2. **Update versions.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "15.1.0",
|
||||||
|
"version_code": 151000,
|
||||||
|
"download_url": "https://github.com/.../v15.1.0/RR3-v15.1.0.apk",
|
||||||
|
"upgrade_from": ["15.0.0"],
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. **Update channel latest:**
|
||||||
|
```json
|
||||||
|
"channels": {
|
||||||
|
"stable": { "latest": "15.1.0" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. **Commit and push** versions.json
|
||||||
|
|
||||||
|
Users will see the update within seconds!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 UI Integration
|
||||||
|
|
||||||
|
### Add Update Button
|
||||||
|
|
||||||
|
The easiest way to integrate the UI is to add a button that opens the update checker:
|
||||||
|
|
||||||
|
**Example (in your HTML UI):**
|
||||||
|
```html
|
||||||
|
<button onclick="showUpdateChecker()">🔄 Check for Updates</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showUpdateChecker() {
|
||||||
|
window.location.href = 'file:///android_asset/community_update_checker.html';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebView Bridge Setup
|
||||||
|
|
||||||
|
The UpdateManager must be added as a JavaScript interface to your WebView:
|
||||||
|
|
||||||
|
```java
|
||||||
|
webView.addJavascriptInterface(new UpdateManager(context), "UpdateManager");
|
||||||
|
```
|
||||||
|
|
||||||
|
**In smali:**
|
||||||
|
```smali
|
||||||
|
new-instance v0, Lcom/community/UpdateManager;
|
||||||
|
move-object v1, p0 # context
|
||||||
|
invoke-direct {v0, v1}, Lcom/community/UpdateManager;-><init>(Landroid/content/Context;)V
|
||||||
|
|
||||||
|
const-string v1, "UpdateManager"
|
||||||
|
invoke-virtual {webView, v0, v1}, Landroid/webkit/WebView;->addJavascriptInterface(Ljava/lang/Object;Ljava/lang/String;)V
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Test Manifest Parsing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fetch your manifest
|
||||||
|
curl https://raw.githubusercontent.com/YOUR-ORG/YOUR-REPO/main/versions.json
|
||||||
|
|
||||||
|
# Check structure
|
||||||
|
jq . versions.json
|
||||||
|
|
||||||
|
# Verify accessible
|
||||||
|
curl -I https://raw.githubusercontent.com/YOUR-ORG/YOUR-REPO/main/versions.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Update Flow
|
||||||
|
|
||||||
|
1. **Set current version lower** (e.g., 15.0.0)
|
||||||
|
2. **Add test version to manifest** (e.g., 15.0.1)
|
||||||
|
3. **Set upgrade_from** to current version
|
||||||
|
4. **Open app and check for updates**
|
||||||
|
5. **Verify update is offered**
|
||||||
|
|
||||||
|
### Test Download
|
||||||
|
|
||||||
|
1. **Create actual GitHub release with APK**
|
||||||
|
2. **Update manifest download_url**
|
||||||
|
3. **Test download (WiFi only, then mobile)**
|
||||||
|
4. **Verify progress tracking works**
|
||||||
|
5. **Test installation**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Customization
|
||||||
|
|
||||||
|
### Change Manifest URL
|
||||||
|
|
||||||
|
Edit `UpdateManager.java` line ~23:
|
||||||
|
```java
|
||||||
|
private static final String UPDATE_API_URL = "YOUR_MANIFEST_URL";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modify UI
|
||||||
|
|
||||||
|
Edit `community_update_checker.html`:
|
||||||
|
- Change colors (CSS variables at top)
|
||||||
|
- Modify layout
|
||||||
|
- Add custom branding
|
||||||
|
- Change button text
|
||||||
|
|
||||||
|
### Add Update Channels
|
||||||
|
|
||||||
|
In your manifest:
|
||||||
|
```json
|
||||||
|
"channels": {
|
||||||
|
"stable": { "latest": "15.0.0" },
|
||||||
|
"beta": { "latest": "15.1.0-beta" },
|
||||||
|
"legacy-v14": { "latest": "14.5.2" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add channel selection in your UI.
|
||||||
|
|
||||||
|
### Network Preferences
|
||||||
|
|
||||||
|
Users can toggle WiFi-only mode:
|
||||||
|
```javascript
|
||||||
|
UpdateManager.setWifiOnlyPreference(true); // WiFi only
|
||||||
|
UpdateManager.setWifiOnlyPreference(false); // WiFi + Mobile
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Android Permissions
|
||||||
|
|
||||||
|
Add to `AndroidManifest.xml`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Network access for checking updates -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<!-- Download updates -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
|
||||||
|
<!-- Install updates -->
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security
|
||||||
|
|
||||||
|
### APK Verification
|
||||||
|
|
||||||
|
**Recommended:** Verify SHA-256 checksum after download
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Add to UpdateManager
|
||||||
|
public boolean verifyDownload(String expectedSha256) {
|
||||||
|
// Calculate SHA-256 of downloaded APK
|
||||||
|
// Compare with manifest value
|
||||||
|
// Return true if match
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTPS Only
|
||||||
|
|
||||||
|
- ✅ Manifest URL uses HTTPS (raw.githubusercontent.com)
|
||||||
|
- ✅ Download URLs use HTTPS (github.com)
|
||||||
|
- ✅ No insecure connections
|
||||||
|
|
||||||
|
### Code Signing
|
||||||
|
|
||||||
|
Always sign your APK with the same keystore for update continuity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 File Structure
|
||||||
|
|
||||||
|
### After Integration
|
||||||
|
|
||||||
|
```
|
||||||
|
your-rr3-apk/
|
||||||
|
├── smali/
|
||||||
|
│ └── com/
|
||||||
|
│ └── community/
|
||||||
|
│ └── UpdateManager.smali ← Add this
|
||||||
|
├── assets/
|
||||||
|
│ └── community_update_checker.html ← Add this
|
||||||
|
└── AndroidManifest.xml ← Add permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Your GitHub Release Repo
|
||||||
|
|
||||||
|
```
|
||||||
|
your-releases-repo/
|
||||||
|
├── versions.json ← Version manifest
|
||||||
|
└── README.md ← Usage instructions
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ FAQ
|
||||||
|
|
||||||
|
### Q: Does this work with EA's official RR3?
|
||||||
|
|
||||||
|
**A:** Technically yes, but EA may ban your account for modified APKs. This is designed for community servers where EA's servers are shut down.
|
||||||
|
|
||||||
|
### Q: Can users downgrade versions?
|
||||||
|
|
||||||
|
**A:** No. The system only offers updates to versions with higher version codes.
|
||||||
|
|
||||||
|
### Q: What if GitHub goes down?
|
||||||
|
|
||||||
|
**A:** Update checks will fail gracefully. Users can still play, just won't get updates until GitHub is back.
|
||||||
|
|
||||||
|
### Q: Can I host the manifest elsewhere?
|
||||||
|
|
||||||
|
**A:** Yes! Just change the `UPDATE_API_URL`. Any HTTPS endpoint that returns the JSON works.
|
||||||
|
|
||||||
|
### Q: How do I prevent v14 users from updating to v15?
|
||||||
|
|
||||||
|
**A:** In your v15 manifest entry, don't include v14.x in `upgrade_from`:
|
||||||
|
```json
|
||||||
|
"upgrade_from": ["15.0.0", "15.0.1"] // No 14.x!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Can I have multiple APK variants (arm64, x86)?
|
||||||
|
|
||||||
|
**A:** Not currently, but you can extend the manifest:
|
||||||
|
```json
|
||||||
|
"variants": [
|
||||||
|
{"arch": "arm64-v8a", "url": "...-arm64.apk"},
|
||||||
|
{"arch": "armeabi-v7a", "url": "...-arm.apk"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update UpdateManager to select based on device architecture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Found a bug? Have an improvement?
|
||||||
|
|
||||||
|
1. Test your changes thoroughly
|
||||||
|
2. Update documentation
|
||||||
|
3. Submit with clear description
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📜 License
|
||||||
|
|
||||||
|
This OTA system is part of the RR3 Community Edition project.
|
||||||
|
|
||||||
|
**License:** MIT License
|
||||||
|
|
||||||
|
**Credits:**
|
||||||
|
- Project Lead: [@ssfdre38](https://github.com/ssfdre38) (Daniel Elliott)
|
||||||
|
- AI Assistant: GitHub Copilot CLI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
- **Main Project:** [RR3 Community Edition](https://github.com/project-real-resurrection-3)
|
||||||
|
- **Releases:** [rr3-releases](https://github.com/project-real-resurrection-3/rr3-releases)
|
||||||
|
- **Legal:** [LEGAL.md](https://github.com/project-real-resurrection-3/.github/blob/main/LEGAL.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
**Need help?**
|
||||||
|
- 🐛 Report issues on GitHub
|
||||||
|
- 💬 Join community Discord (link TBA)
|
||||||
|
- 📧 Contact: [@ssfdre38](https://github.com/ssfdre38)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**🔄 Keep your RR3 Community Edition up to date! 🏁**
|
||||||
|
|
||||||
|
Made with ❤️ by the RR3 Community
|
||||||
|
|
||||||
|
</div>
|
||||||
362
UpdateManager.java
Normal file
362
UpdateManager.java
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
package com.community;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.app.DownloadManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.JavascriptInterface;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateManager - Manifest-based OTA Update System
|
||||||
|
*
|
||||||
|
* Fetches version manifest from GitHub and determines applicable updates
|
||||||
|
* based on upgrade paths defined in versions.json
|
||||||
|
*/
|
||||||
|
public class UpdateManager {
|
||||||
|
private static final String TAG = "UpdateManager";
|
||||||
|
private static final String UPDATE_API_URL = "https://raw.githubusercontent.com/project-real-resurrection-3/rr3-releases/main/versions.json";
|
||||||
|
private static final String PREFS_NAME = "com.ea.games.r3_row_preferences";
|
||||||
|
private static final String KEY_WIFI_ONLY = "wifi_only_updates";
|
||||||
|
private static final String KEY_AUTO_CHECK = "auto_check_updates";
|
||||||
|
private static final String KEY_LAST_CHECK = "last_update_check";
|
||||||
|
private static final String KEY_SKIPPED_VERSIONS = "skipped_versions";
|
||||||
|
private static final String KEY_DOWNLOAD_ID = "download_id";
|
||||||
|
|
||||||
|
// IMPORTANT: Update these values for each build
|
||||||
|
private static final String CURRENT_VERSION = "15.0.0-community-alpha";
|
||||||
|
private static final int CURRENT_VERSION_CODE = 150000;
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private DownloadManager downloadManager;
|
||||||
|
private long downloadId = -1;
|
||||||
|
|
||||||
|
public UpdateManager(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
this.downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for updates using version manifest
|
||||||
|
* Returns JSON with update info or "no update" status
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public String checkForUpdates() {
|
||||||
|
Log.i(TAG, "Checking for updates from manifest...");
|
||||||
|
JSONObject result = new JSONObject();
|
||||||
|
HttpURLConnection connection = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch versions.json manifest
|
||||||
|
URL url = new URL(UPDATE_API_URL);
|
||||||
|
connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setRequestProperty("User-Agent", "RR3-Community-Updater");
|
||||||
|
connection.setConnectTimeout(10000);
|
||||||
|
connection.setReadTimeout(10000);
|
||||||
|
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(connection.getInputStream())
|
||||||
|
);
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
response.append(line);
|
||||||
|
}
|
||||||
|
reader.close();
|
||||||
|
|
||||||
|
// Parse manifest
|
||||||
|
JSONObject manifest = new JSONObject(response.toString());
|
||||||
|
JSONArray versions = manifest.getJSONArray("versions");
|
||||||
|
|
||||||
|
// Get current version (strip suffix like -alpha, -beta)
|
||||||
|
String currentVersionClean = CURRENT_VERSION;
|
||||||
|
if (currentVersionClean.contains("-")) {
|
||||||
|
currentVersionClean = currentVersionClean.split("-")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for applicable update
|
||||||
|
JSONObject updateVersion = findApplicableUpdate(versions, currentVersionClean, CURRENT_VERSION_CODE);
|
||||||
|
|
||||||
|
if (updateVersion != null) {
|
||||||
|
// Found an update!
|
||||||
|
result.put("hasUpdate", true);
|
||||||
|
result.put("version", updateVersion.getString("version"));
|
||||||
|
result.put("downloadUrl", updateVersion.getString("download_url"));
|
||||||
|
result.put("fileSize", updateVersion.getLong("file_size"));
|
||||||
|
result.put("changelog", updateVersion.optString("changelog", ""));
|
||||||
|
result.put("releaseDate", updateVersion.optString("release_date", ""));
|
||||||
|
|
||||||
|
Log.i(TAG, "Update available: " + updateVersion.getString("version"));
|
||||||
|
} else {
|
||||||
|
// No update found
|
||||||
|
result.put("hasUpdate", false);
|
||||||
|
Log.i(TAG, "No update available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last check time
|
||||||
|
SharedPreferences prefs = getSharedPreferences();
|
||||||
|
prefs.edit()
|
||||||
|
.putLong(KEY_LAST_CHECK, System.currentTimeMillis())
|
||||||
|
.apply();
|
||||||
|
} else {
|
||||||
|
result.put("hasUpdate", false);
|
||||||
|
result.put("error", "HTTP " + responseCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error checking for updates", e);
|
||||||
|
try {
|
||||||
|
result.put("hasUpdate", false);
|
||||||
|
result.put("error", "Failed to check for updates");
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
} finally {
|
||||||
|
if (connection != null) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find applicable update from versions array
|
||||||
|
* Checks if version_code is newer AND current version is in upgrade_from
|
||||||
|
*/
|
||||||
|
private JSONObject findApplicableUpdate(JSONArray versions, String currentVersion, int currentVersionCode) {
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < versions.length(); i++) {
|
||||||
|
JSONObject version = versions.getJSONObject(i);
|
||||||
|
|
||||||
|
// Get version code
|
||||||
|
int versionCode = version.getInt("version_code");
|
||||||
|
|
||||||
|
// Only consider versions newer than current
|
||||||
|
if (versionCode <= currentVersionCode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current version is in upgrade_from array
|
||||||
|
JSONArray upgradeFrom = version.optJSONArray("upgrade_from");
|
||||||
|
if (upgradeFrom == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each entry in upgrade_from
|
||||||
|
for (int j = 0; j < upgradeFrom.length(); j++) {
|
||||||
|
String allowedVersion = upgradeFrom.getString(j);
|
||||||
|
|
||||||
|
// Check for exact match
|
||||||
|
if (allowedVersion.equals(currentVersion)) {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for wildcard match (e.g., "15.0.x" matches "15.0.0")
|
||||||
|
if (allowedVersion.endsWith(".x")) {
|
||||||
|
String wildcardPrefix = allowedVersion.substring(0, allowedVersion.length() - 2);
|
||||||
|
if (currentVersion.startsWith(wildcardPrefix)) {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for major version wildcard (e.g., "14.x" matches any 14.x.x)
|
||||||
|
if (allowedVersion.matches("\\d+\\.x")) {
|
||||||
|
String majorVersion = allowedVersion.split("\\.")[0];
|
||||||
|
if (currentVersion.startsWith(majorVersion + ".")) {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error finding applicable update", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download update APK
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public boolean downloadUpdate(String downloadUrl, String version) {
|
||||||
|
try {
|
||||||
|
Log.i(TAG, "Starting download: " + version);
|
||||||
|
|
||||||
|
// Check network preference
|
||||||
|
boolean wifiOnly = getSharedPreferences().getBoolean(KEY_WIFI_ONLY, false);
|
||||||
|
|
||||||
|
Uri uri = Uri.parse(downloadUrl);
|
||||||
|
DownloadManager.Request request = new DownloadManager.Request(uri);
|
||||||
|
request.setTitle("RR3 Community Update");
|
||||||
|
request.setDescription("Downloading v" + version);
|
||||||
|
request.setDestinationInExternalPublicDir(
|
||||||
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
|
"RR3-Community-v" + version + ".apk"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set network type based on preference
|
||||||
|
if (wifiOnly) {
|
||||||
|
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
|
||||||
|
} else {
|
||||||
|
request.setAllowedNetworkTypes(
|
||||||
|
DownloadManager.Request.NETWORK_WIFI |
|
||||||
|
DownloadManager.Request.NETWORK_MOBILE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.setNotificationVisibility(
|
||||||
|
DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
||||||
|
);
|
||||||
|
request.setMimeType("application/vnd.android.package-archive");
|
||||||
|
|
||||||
|
downloadId = downloadManager.enqueue(request);
|
||||||
|
|
||||||
|
// Save download ID
|
||||||
|
getSharedPreferences().edit()
|
||||||
|
.putLong(KEY_DOWNLOAD_ID, downloadId)
|
||||||
|
.apply();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error starting download", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get download progress (0-100, or -1 if not downloading)
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public int getDownloadProgress() {
|
||||||
|
if (downloadId == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DownloadManager.Query query = new DownloadManager.Query();
|
||||||
|
query.setFilterById(downloadId);
|
||||||
|
android.database.Cursor cursor = downloadManager.query(query);
|
||||||
|
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
int bytesDownloaded = cursor.getInt(
|
||||||
|
cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
|
||||||
|
);
|
||||||
|
int bytesTotal = cursor.getInt(
|
||||||
|
cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (bytesTotal > 0) {
|
||||||
|
return (int) ((bytesDownloaded * 100L) / bytesTotal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error getting download progress", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if download is complete
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public boolean isDownloadComplete() {
|
||||||
|
if (downloadId == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DownloadManager.Query query = new DownloadManager.Query();
|
||||||
|
query.setFilterById(downloadId);
|
||||||
|
android.database.Cursor cursor = downloadManager.query(query);
|
||||||
|
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
int status = cursor.getInt(
|
||||||
|
cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||||
|
);
|
||||||
|
cursor.close();
|
||||||
|
return status == DownloadManager.STATUS_SUCCESSFUL;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error checking download status", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get WiFi-only preference
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public boolean getWifiOnlyPreference() {
|
||||||
|
return getSharedPreferences().getBoolean(KEY_WIFI_ONLY, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set WiFi-only preference
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public void setWifiOnlyPreference(boolean wifiOnly) {
|
||||||
|
getSharedPreferences().edit()
|
||||||
|
.putBoolean(KEY_WIFI_ONLY, wifiOnly)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get auto-check preference
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public boolean getAutoCheckPreference() {
|
||||||
|
return getSharedPreferences().getBoolean(KEY_AUTO_CHECK, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set auto-check preference
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public void setAutoCheckPreference(boolean autoCheck) {
|
||||||
|
getSharedPreferences().edit()
|
||||||
|
.putBoolean(KEY_AUTO_CHECK, autoCheck)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get SharedPreferences instance
|
||||||
|
*/
|
||||||
|
private SharedPreferences getSharedPreferences() {
|
||||||
|
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse version string to version code
|
||||||
|
* Format: "15.0.0" -> 150000
|
||||||
|
*/
|
||||||
|
private int parseVersionCode(String version) {
|
||||||
|
try {
|
||||||
|
String[] parts = version.split("\\.");
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
int major = Integer.parseInt(parts[0]);
|
||||||
|
int minor = Integer.parseInt(parts[1]);
|
||||||
|
String patchPart = parts[2].split("-")[0]; // Remove suffix
|
||||||
|
int patch = Integer.parseInt(patchPart);
|
||||||
|
|
||||||
|
return (major * 100000) + (minor * 1000) + patch;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
Log.e(TAG, "Error parsing version code", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
1170
UpdateManager.smali
Normal file
1170
UpdateManager.smali
Normal file
File diff suppressed because it is too large
Load Diff
752
community_update_checker.html
Normal file
752
community_update_checker.html
Normal file
@@ -0,0 +1,752 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>RR3 Update Available</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
animation: fadeIn 0.5s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-preference {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-toggle-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-info h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-info p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 56px;
|
||||||
|
height: 30px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle-slider {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle-slider:before {
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-status {
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-status-label {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-status-value {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-status-value.wifi {
|
||||||
|
color: #2ed573;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-status-value.mobile {
|
||||||
|
color: #ffa502;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-warning {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(255, 165, 0, 0.2);
|
||||||
|
border: 2px solid #ffa502;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ffa502;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-warning.show {
|
||||||
|
display: block;
|
||||||
|
animation: slideDown 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-content {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-content::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-content::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-content ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-content li {
|
||||||
|
padding: 8px 0;
|
||||||
|
padding-left: 25px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-content li:before {
|
||||||
|
content: "•";
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
color: #4CAF50;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
margin: 20px 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container.show {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #4CAF50 0%, #8BC34A 100%);
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(135deg, #2ed573 0%, #24a353 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<h1>🎉 Update Available!</h1>
|
||||||
|
<span class="version-badge" id="versionBadge">v15.0.1-community-alpha</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Network Preference Card -->
|
||||||
|
<div class="card network-preference">
|
||||||
|
<div class="network-toggle-container">
|
||||||
|
<div class="network-label">
|
||||||
|
<span class="network-icon">📶</span>
|
||||||
|
<div class="network-info">
|
||||||
|
<h4>WiFi Only</h4>
|
||||||
|
<p>Download updates only on WiFi (saves mobile data)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="wifiOnlyToggle" onchange="toggleWifiOnly()">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="network-status">
|
||||||
|
<span class="network-status-label">Network:</span>
|
||||||
|
<span class="network-status-value" id="networkStatus">Loading...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-warning" id="dataWarning">
|
||||||
|
⚠️ <strong>You're on mobile data.</strong> This download is approximately <span id="warningSize">120 MB</span> and may incur data charges from your carrier.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Changelog Card -->
|
||||||
|
<div class="card changelog">
|
||||||
|
<h3>📝 What's New</h3>
|
||||||
|
<div class="changelog-content" id="changelogContent">
|
||||||
|
<ul>
|
||||||
|
<li>Loading changelog...</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Info Card -->
|
||||||
|
<div class="card file-info">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">📦 File Size</span>
|
||||||
|
<span class="info-value" id="fileSize">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">📅 Release Date</span>
|
||||||
|
<span class="info-value" id="releaseDate">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">🚀 Current Version</span>
|
||||||
|
<span class="info-value" id="currentVersion">15.0.0-community-alpha</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Section -->
|
||||||
|
<div class="progress-container" id="progressContainer">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="progressFill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="status-text" id="statusText">Preparing download...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-primary" id="downloadBtn" onclick="downloadUpdate()">
|
||||||
|
<span class="btn-icon">📥</span>
|
||||||
|
<span id="btnText">Download Now</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="skipUpdate()">
|
||||||
|
Skip This Version
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let updateInfo = null;
|
||||||
|
let progressInterval = null;
|
||||||
|
let downloadUrl = '';
|
||||||
|
let version = '';
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
function init() {
|
||||||
|
try {
|
||||||
|
loadNetworkPreference();
|
||||||
|
updateNetworkStatus();
|
||||||
|
loadUpdateInfo();
|
||||||
|
getCurrentVersion();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Initialization error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load network preference
|
||||||
|
function loadNetworkPreference() {
|
||||||
|
try {
|
||||||
|
const wifiOnly = UpdateManager.isWifiOnlyEnabled();
|
||||||
|
document.getElementById('wifiOnlyToggle').checked = wifiOnly;
|
||||||
|
updateNetworkUI(wifiOnly);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load network preference:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle WiFi-only mode
|
||||||
|
function toggleWifiOnly() {
|
||||||
|
const toggle = document.getElementById('wifiOnlyToggle');
|
||||||
|
const enabled = toggle.checked;
|
||||||
|
|
||||||
|
try {
|
||||||
|
UpdateManager.setWifiOnlyEnabled(enabled);
|
||||||
|
updateNetworkUI(enabled);
|
||||||
|
updateNetworkStatus();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to set network preference:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update network UI based on preference
|
||||||
|
function updateNetworkUI(wifiOnly) {
|
||||||
|
const networkStatus = document.getElementById('networkStatus');
|
||||||
|
|
||||||
|
if (wifiOnly) {
|
||||||
|
networkStatus.textContent = 'WiFi Only ✅';
|
||||||
|
networkStatus.className = 'network-status-value wifi';
|
||||||
|
} else {
|
||||||
|
networkStatus.textContent = 'WiFi or Mobile ⚡';
|
||||||
|
networkStatus.className = 'network-status-value mobile';
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDataWarning();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update network status
|
||||||
|
function updateNetworkStatus() {
|
||||||
|
// Network status is updated by updateNetworkUI
|
||||||
|
checkDataWarning();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if should show data warning
|
||||||
|
function checkDataWarning() {
|
||||||
|
const dataWarning = document.getElementById('dataWarning');
|
||||||
|
const wifiOnly = document.getElementById('wifiOnlyToggle').checked;
|
||||||
|
|
||||||
|
// Show warning if NOT WiFi-only AND update info loaded
|
||||||
|
if (!wifiOnly && updateInfo && updateInfo.fileSize) {
|
||||||
|
const sizeMB = (updateInfo.fileSize / 1024 / 1024).toFixed(1);
|
||||||
|
document.getElementById('warningSize').textContent = sizeMB + ' MB';
|
||||||
|
dataWarning.classList.add('show');
|
||||||
|
} else {
|
||||||
|
dataWarning.classList.remove('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current version
|
||||||
|
function getCurrentVersion() {
|
||||||
|
try {
|
||||||
|
const current = UpdateManager.getCurrentVersion();
|
||||||
|
document.getElementById('currentVersion').textContent = current;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to get current version:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load update info
|
||||||
|
function loadUpdateInfo() {
|
||||||
|
try {
|
||||||
|
const jsonStr = UpdateManager.checkForUpdates();
|
||||||
|
updateInfo = JSON.parse(jsonStr);
|
||||||
|
|
||||||
|
if (updateInfo.error) {
|
||||||
|
showError('Failed to check for updates: ' + updateInfo.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updateInfo.hasUpdate) {
|
||||||
|
showError('No update available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI with info
|
||||||
|
version = updateInfo.version;
|
||||||
|
downloadUrl = updateInfo.downloadUrl;
|
||||||
|
|
||||||
|
document.getElementById('versionBadge').textContent = 'v' + version;
|
||||||
|
document.getElementById('fileSize').textContent = formatBytes(updateInfo.fileSize);
|
||||||
|
document.getElementById('releaseDate').textContent = formatDate(updateInfo.releaseDate);
|
||||||
|
|
||||||
|
// Update changelog
|
||||||
|
if (updateInfo.changelog) {
|
||||||
|
displayChangelog(updateInfo.changelog);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDataWarning();
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load update info:', e);
|
||||||
|
showError('Error loading update information');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display changelog
|
||||||
|
function displayChangelog(changelog) {
|
||||||
|
const container = document.getElementById('changelogContent');
|
||||||
|
|
||||||
|
// Parse markdown-style changelog
|
||||||
|
const lines = changelog.split('\n');
|
||||||
|
let html = '<ul>';
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.startsWith('- ') || line.startsWith('* ')) {
|
||||||
|
html += '<li>' + line.substring(2) + '</li>';
|
||||||
|
} else if (line.length > 0 && !line.startsWith('#')) {
|
||||||
|
html += '<li>' + line + '</li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</ul>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download update
|
||||||
|
function downloadUpdate() {
|
||||||
|
const downloadBtn = document.getElementById('downloadBtn');
|
||||||
|
|
||||||
|
// Check if network is suitable
|
||||||
|
try {
|
||||||
|
const suitable = UpdateManager.isNetworkSuitableForDownload();
|
||||||
|
if (!suitable) {
|
||||||
|
alert('⚠️ WiFi connection required\n\nConnect to WiFi or disable "WiFi Only" mode to download on mobile data.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to check network:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable button and show progress
|
||||||
|
downloadBtn.disabled = true;
|
||||||
|
downloadBtn.innerHTML = '<span class="loading"></span><span>Downloading...</span>';
|
||||||
|
|
||||||
|
document.getElementById('progressContainer').classList.add('show');
|
||||||
|
document.getElementById('statusText').textContent = 'Starting download...';
|
||||||
|
|
||||||
|
// Start download
|
||||||
|
try {
|
||||||
|
UpdateManager.downloadUpdate(downloadUrl, version);
|
||||||
|
|
||||||
|
// Monitor progress
|
||||||
|
progressInterval = setInterval(updateProgress, 500);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to start download:', e);
|
||||||
|
showError('Failed to start download: ' + e.message);
|
||||||
|
resetDownloadButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update download progress
|
||||||
|
function updateProgress() {
|
||||||
|
try {
|
||||||
|
const progress = UpdateManager.getDownloadProgress();
|
||||||
|
const progressFill = document.getElementById('progressFill');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
const downloadBtn = document.getElementById('downloadBtn');
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
// Download complete
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
progressFill.style.width = '100%';
|
||||||
|
statusText.textContent = '✅ Download complete! Tap button to install.';
|
||||||
|
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
downloadBtn.className = 'btn btn-success';
|
||||||
|
downloadBtn.innerHTML = '<span class="btn-icon">✅</span><span>Tap to Install</span>';
|
||||||
|
downloadBtn.onclick = installUpdate;
|
||||||
|
|
||||||
|
} else if (progress < 0) {
|
||||||
|
// Download paused (no suitable network)
|
||||||
|
progressFill.style.width = '0%';
|
||||||
|
statusText.textContent = '⏸️ Download paused. Waiting for WiFi connection...';
|
||||||
|
downloadBtn.innerHTML = '<span class="btn-icon">⏸️</span><span>Paused (No WiFi)</span>';
|
||||||
|
|
||||||
|
} else if (progress === 0) {
|
||||||
|
// Still preparing
|
||||||
|
statusText.textContent = 'Preparing download...';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Downloading
|
||||||
|
progressFill.style.width = progress + '%';
|
||||||
|
statusText.textContent = `Downloading... ${progress.toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Progress check error:', e);
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install update
|
||||||
|
function installUpdate() {
|
||||||
|
try {
|
||||||
|
document.getElementById('statusText').textContent = 'Opening installer...';
|
||||||
|
UpdateManager.installUpdate();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to install:', e);
|
||||||
|
showError('Failed to open installer: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip this version
|
||||||
|
function skipUpdate() {
|
||||||
|
if (confirm('Skip this version?\n\nYou can always check for updates manually later.')) {
|
||||||
|
try {
|
||||||
|
UpdateManager.skipThisVersion(version);
|
||||||
|
window.history.back();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to skip version:', e);
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset download button
|
||||||
|
function resetDownloadButton() {
|
||||||
|
const downloadBtn = document.getElementById('downloadBtn');
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
downloadBtn.innerHTML = '<span class="btn-icon">📥</span><span>Download Now</span>';
|
||||||
|
document.getElementById('progressContainer').classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
function showError(message) {
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
statusText.textContent = '❌ ' + message;
|
||||||
|
statusText.style.color = '#ff6b6b';
|
||||||
|
document.getElementById('progressContainer').classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format bytes to human readable
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return 'Unknown';
|
||||||
|
try {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on load
|
||||||
|
window.addEventListener('load', init);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user