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>
363 lines
13 KiB
Java
363 lines
13 KiB
Java
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;
|
|
}
|
|
}
|