Files
rr3-ota/UpdateManager.java
ssfdre38 b9191a0e86 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>
2026-02-23 21:10:22 -08:00

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;
}
}