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