package com.fox2code.mmm; import com.fox2code.mmm.utils.io.Files; import com.fox2code.mmm.utils.io.Http; import org.json.JSONObject; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.HashMap; import timber.log.Timber; // See https://docs.github.com/en/rest/reference/repos#releases public class AppUpdateManager { public static final int FLAG_COMPAT_LOW_QUALITY = 0x0001; public static final int FLAG_COMPAT_NO_EXT = 0x0002; public static final int FLAG_COMPAT_MAGISK_CMD = 0x0004; public static final int FLAG_COMPAT_NEED_32BIT = 0x0008; public static final int FLAG_COMPAT_MALWARE = 0x0010; public static final int FLAG_COMPAT_NO_ANSI = 0x0020; public static final int FLAG_COMPAT_FORCE_ANSI = 0x0040; public static final int FLAG_COMPAT_FORCE_HIDE = 0x0080; public static final int FLAG_COMPAT_MMT_REBORN = 0x0100; public static final int FLAG_COMPAT_ZIP_WRAPPER = 0x0200; private static final AppUpdateManager INSTANCE = new AppUpdateManager(); public static final String RELEASES_API_URL = "https://api.github.com/repos/Fox2Code/FoxMagiskModuleManager/releases/latest"; private final HashMap compatDataId = new HashMap<>(); private final Object updateLock = new Object(); private final File compatFile; private String latestRelease; private long lastChecked; private AppUpdateManager() { this.compatFile = new File(MainApplication.getINSTANCE().getFilesDir(), "compat.txt"); this.latestRelease = MainApplication.getBootSharedPreferences().getString("updater_latest_release", BuildConfig.VERSION_NAME); this.lastChecked = 0; if (this.compatFile.isFile()) { try { this.parseCompatibilityFlags(new FileInputStream(this.compatFile)); } catch ( IOException e) { e.printStackTrace(); } } } public static AppUpdateManager getAppUpdateManager() { return INSTANCE; } public static int getFlagsForModule(String moduleId) { return INSTANCE.getCompatibilityFlags(moduleId); } public static boolean shouldForceHide(String repoId) { if (BuildConfig.DEBUG || repoId.startsWith("repo_") || repoId.equals("magisk_alt_repo")) return false; return !repoId.startsWith("repo_") && (INSTANCE.getCompatibilityFlags(repoId) & FLAG_COMPAT_FORCE_HIDE) != 0; } // Return true if should show a notification public boolean checkUpdate(boolean force) { if (!BuildConfig.ENABLE_AUTO_UPDATER) return false; if (!force && this.peekShouldUpdate()) return true; long lastChecked = this.lastChecked; if (lastChecked != 0 && // Avoid spam calls by putting a 60 seconds timer lastChecked < System.currentTimeMillis() - 60000L) return force && this.peekShouldUpdate(); synchronized (this.updateLock) { if (lastChecked != this.lastChecked) return this.peekShouldUpdate(); boolean preReleaseNewer = true; try { JSONObject releases = new JSONObject(new String(Http.doHttpGet(RELEASES_API_URL, false), StandardCharsets.UTF_8)); String latestRelease = null, latestPreRelease = null; for (int i = 0; i < releases.length(); i++) { JSONObject release; try { release = releases.getJSONObject(String.valueOf(i)); } catch ( Exception e) { continue; } // Skip invalid entries if (release.getBoolean("draft")) continue; boolean preRelease = release.getBoolean("prerelease"); String version = release.getString("tag_name"); if (version.startsWith("v")) version = version.substring(1); if (preRelease) { if (latestPreRelease == null) latestPreRelease = version; } else if (latestRelease == null) { latestRelease = version; if (latestPreRelease == null) preReleaseNewer = false; } if (latestRelease != null && latestPreRelease != null) { break; // We read everything we needed to read. } } if (latestRelease != null) this.latestRelease = latestRelease; if (BuildConfig.DEBUG) Timber.d("Latest release: %s", latestRelease); if (BuildConfig.DEBUG) Timber.d("Latest pre-release: %s", latestPreRelease); if (BuildConfig.DEBUG) Timber.d("Latest pre-release newer: %s", preReleaseNewer); this.lastChecked = System.currentTimeMillis(); } catch ( Exception ioe) { Timber.e(ioe); } } return this.peekShouldUpdate(); } public void checkUpdateCompat() { compatDataId.clear(); try { Files.write(compatFile, new byte[0]); } catch ( IOException e) { e.printStackTrace(); } // There once lived an implementation that used a GitHub API to get the compatibility flags. It was removed because it was too slow and the API was rate limited. Timber.w("Remote compatibility data flags are not implemented."); } public boolean peekShouldUpdate() { if (!BuildConfig.ENABLE_AUTO_UPDATER || BuildConfig.DEBUG) return false; // Convert both BuildConfig.VERSION_NAME and latestRelease to int int currentVersion = 0, latestVersion = 0; try { currentVersion = Integer.parseInt(BuildConfig.VERSION_NAME.replaceAll("\\D", "")); latestVersion = Integer.parseInt(this.latestRelease.replace("v", "").replaceAll("\\D", "")); } catch ( NumberFormatException ignored) { } return currentVersion < latestVersion; } public boolean peekHasUpdate() { if (!BuildConfig.ENABLE_AUTO_UPDATER || BuildConfig.DEBUG) return false; return this.peekShouldUpdate(); } private void parseCompatibilityFlags(InputStream inputStream) throws IOException { compatDataId.clear(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); String line; while ((line = bufferedReader.readLine()) != null) { line = line.trim(); if (line.isEmpty() || line.startsWith("#")) continue; int i = line.indexOf('/'); if (i == -1) continue; int value = 0; for (String arg : line.substring(i + 1).split(",")) { switch (arg) { default: break; case "lowQuality": value |= FLAG_COMPAT_LOW_QUALITY; break; case "noExt": value |= FLAG_COMPAT_NO_EXT; break; case "magiskCmd": value |= FLAG_COMPAT_MAGISK_CMD; break; case "need32bit": value |= FLAG_COMPAT_NEED_32BIT; break; case "malware": value |= FLAG_COMPAT_MALWARE; break; case "noANSI": value |= FLAG_COMPAT_NO_ANSI; break; case "forceANSI": value |= FLAG_COMPAT_FORCE_ANSI; break; case "forceHide": value |= FLAG_COMPAT_FORCE_HIDE; break; case "mmtReborn": value |= FLAG_COMPAT_MMT_REBORN; break; case "wrapper": value |= FLAG_COMPAT_ZIP_WRAPPER; break; } } compatDataId.put(line.substring(0, i), value); } } public int getCompatibilityFlags(String moduleId) { Integer compatFlags = compatDataId.get(moduleId); return compatFlags == null ? 0 : compatFlags; } }