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:
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user