From 12792f77f3c6307f933fa9c4e79a27987d104f25 Mon Sep 17 00:00:00 2001 From: instant992 Date: Tue, 9 Jun 2026 03:00:30 +0400 Subject: [PATCH] Switch update checker to GitHub Releases - UpdateHelper now fetches latest release from GitHub API instead of helper bot - Compares dotted version names, prefers arm64 APK asset, falls back to release page - Keeps TL_help_appUpdate output contract so existing update UI works unchanged - Decouples ConfigHelper from update check --- .../nekogram/helpers/remote/UpdateHelper.java | 459 ++++++++++++------ 1 file changed, 300 insertions(+), 159 deletions(-) diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/remote/UpdateHelper.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/remote/UpdateHelper.java index c0c35793..094c8b87 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/remote/UpdateHelper.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/remote/UpdateHelper.java @@ -1,159 +1,300 @@ -package tw.nekomimi.nekogram.helpers.remote; - -import android.content.pm.PackageInfo; -import android.os.Build; -import android.text.TextUtils; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -import org.telegram.messenger.ApplicationLoader; -import org.telegram.messenger.BuildConfig; -import org.telegram.messenger.FileLog; -import org.telegram.messenger.LocaleController; -import org.telegram.messenger.R; -import org.telegram.tgnet.TLRPC; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.stream.Collectors; - -public class UpdateHelper extends BaseRemoteHelper { - public static final String UPDATE_METHOD = "check_for_updates"; - - /** - * @param date {long} - date in milliseconds - */ - public static String formatDateUpdate(long date) { - long epoch; - try { - PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0); - epoch = pInfo.lastUpdateTime; - } catch (Exception e) { - epoch = 0; - } - if (date <= epoch) { - return LocaleController.formatString(R.string.LastUpdateNever); - } - try { - Calendar rightNow = Calendar.getInstance(); - int day = rightNow.get(Calendar.DAY_OF_YEAR); - int year = rightNow.get(Calendar.YEAR); - rightNow.setTimeInMillis(date); - int dateDay = rightNow.get(Calendar.DAY_OF_YEAR); - int dateYear = rightNow.get(Calendar.YEAR); - - if (dateDay == day && year == dateYear) { - if (Math.abs(System.currentTimeMillis() - date) < 60000L) { - return LocaleController.formatString(R.string.LastUpdateRecently); - } - return LocaleController.formatString(R.string.LastUpdateFormatted, LocaleController.formatString(R.string.TodayAtFormatted, - LocaleController.getInstance().getFormatterDay().format(new Date(date)))); - } else if (dateDay + 1 == day && year == dateYear) { - return LocaleController.formatString(R.string.LastUpdateFormatted, LocaleController.formatString(R.string.YesterdayAtFormatted, - LocaleController.getInstance().getFormatterDay().format(new Date(date)))); - } else if (Math.abs(System.currentTimeMillis() - date) < 31536000000L) { - String format = LocaleController.formatString(R.string.formatDateAtTime, - LocaleController.getInstance().getFormatterDayMonth().format(new Date(date)), - LocaleController.getInstance().getFormatterDay().format(new Date(date))); - return LocaleController.formatString(R.string.LastUpdateDateFormatted, format); - } else { - String format = LocaleController.formatString(R.string.formatDateAtTime, - LocaleController.getInstance().getFormatterYear().format(new Date(date)), - LocaleController.getInstance().getFormatterDay().format(new Date(date))); - return LocaleController.formatString(R.string.LastUpdateDateFormatted, format); - } - } catch (Exception e) { - FileLog.e(e); - } - return "LOC_ERR"; - } - - private static final class InstanceHolder { - private static final UpdateHelper instance = new UpdateHelper(); - } - - public static UpdateHelper getInstance() { - return InstanceHolder.instance; - } - - @Override - protected void onError(String text, Delegate delegate) { - delegate.onTLResponse(null, text); - } - - @Override - protected String getRequestMethod() { - return UPDATE_METHOD; - } - - @Override - protected String getRequestParams() { - return " " + TextUtils.join(",", Build.SUPPORTED_ABIS); - } - - @Override - protected void onLoadSuccess(ArrayList results, Delegate delegate) { - var map = results.stream() - .collect(Collectors.toMap(result -> result.id, result -> result)); - var update_info = map.get("update_info"); - if (update_info == null) { - delegate.onTLResponse(null, null); - return; - } - var update = new TLRPC.TL_help_appUpdate(); - var json = GSON.fromJson(getTextFromInlineResult(update_info), Update.class); - if (json == null || json.versionCode <= BuildConfig.VERSION_CODE) { - delegate.onTLResponse(null, null); - return; - } - update.version = json.version; - update.can_not_skip = json.canNotSkip; - if (json.url != null) { - update.url = json.url; - update.flags |= 4; - } - var document = map.get("document"); - if (document != null && document.document != null) { - update.document = document.document; - update.flags |= 2; - } - var message = map.get("message"); - if (message != null && message.send_message != null) { - update.text = message.send_message.message; - update.entities = message.send_message.entities; - var entities = map.get("entities"); - if (entities != null) { - var entities_json = GSON.fromJson(getTextFromInlineResult(entities), MessageEntity[].class); - update.entities.addAll(parseBotAPIEntities(entities_json, true)); - } - } - var sticker = map.get("sticker"); - if (sticker != null && sticker.document != null) { - update.sticker = sticker.document; - update.flags |= 8; - } - delegate.onTLResponse(update, null); - } - - public void checkNewVersionAvailable(Delegate delegate) { - load(delegate); - ConfigHelper.getInstance().load(); - } - - public static class Update { - @SerializedName("can_not_skip") - @Expose - public Boolean canNotSkip; - @SerializedName("version") - @Expose - public String version; - @SerializedName("version_code") - @Expose - public Integer versionCode; - @SerializedName("url") - @Expose - public String url; - } -} +package tw.nekomimi.nekogram.helpers.remote; + +import android.content.pm.PackageInfo; +import android.text.TextUtils; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.BuildConfig; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.messenger.Utilities; +import org.telegram.tgnet.TLRPC; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.regex.Pattern; + +public class UpdateHelper extends BaseRemoteHelper { + public static final String UPDATE_METHOD = "check_for_updates"; + + // GitHub repository that hosts FoxiGram releases. + private static final String GITHUB_REPO = "instant992/FoxiGram"; + private static final String RELEASES_API = "https://api.github.com/repos/" + GITHUB_REPO + "/releases/latest"; + + private static final Pattern VERSION_PART = Pattern.compile("\\d+"); + + private volatile boolean checking; + + /** + * @param date {long} - date in milliseconds + */ + public static String formatDateUpdate(long date) { + long epoch; + try { + PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0); + epoch = pInfo.lastUpdateTime; + } catch (Exception e) { + epoch = 0; + } + if (date <= epoch) { + return LocaleController.formatString(R.string.LastUpdateNever); + } + try { + Calendar rightNow = Calendar.getInstance(); + int day = rightNow.get(Calendar.DAY_OF_YEAR); + int year = rightNow.get(Calendar.YEAR); + rightNow.setTimeInMillis(date); + int dateDay = rightNow.get(Calendar.DAY_OF_YEAR); + int dateYear = rightNow.get(Calendar.YEAR); + + if (dateDay == day && year == dateYear) { + if (Math.abs(System.currentTimeMillis() - date) < 60000L) { + return LocaleController.formatString(R.string.LastUpdateRecently); + } + return LocaleController.formatString(R.string.LastUpdateFormatted, LocaleController.formatString(R.string.TodayAtFormatted, + LocaleController.getInstance().getFormatterDay().format(new Date(date)))); + } else if (dateDay + 1 == day && year == dateYear) { + return LocaleController.formatString(R.string.LastUpdateFormatted, LocaleController.formatString(R.string.YesterdayAtFormatted, + LocaleController.getInstance().getFormatterDay().format(new Date(date)))); + } else if (Math.abs(System.currentTimeMillis() - date) < 31536000000L) { + String format = LocaleController.formatString(R.string.formatDateAtTime, + LocaleController.getInstance().getFormatterDayMonth().format(new Date(date)), + LocaleController.getInstance().getFormatterDay().format(new Date(date))); + return LocaleController.formatString(R.string.LastUpdateDateFormatted, format); + } else { + String format = LocaleController.formatString(R.string.formatDateAtTime, + LocaleController.getInstance().getFormatterYear().format(new Date(date)), + LocaleController.getInstance().getFormatterDay().format(new Date(date))); + return LocaleController.formatString(R.string.LastUpdateDateFormatted, format); + } + } catch (Exception e) { + FileLog.e(e); + } + return "LOC_ERR"; + } + + private static final class InstanceHolder { + private static final UpdateHelper instance = new UpdateHelper(); + } + + public static UpdateHelper getInstance() { + return InstanceHolder.instance; + } + + @Override + protected void onError(String text, Delegate delegate) { + if (delegate != null) { + delegate.onTLResponse(null, text); + } + } + + @Override + protected String getRequestMethod() { + return UPDATE_METHOD; + } + + @Override + protected String getRequestParams() { + return ""; + } + + public void checkNewVersionAvailable(Delegate delegate) { + if (checking) { + return; + } + checking = true; + Utilities.globalQueue.postRunnable(() -> fetchLatestRelease(delegate)); + } + + private void fetchLatestRelease(Delegate delegate) { + HttpURLConnection connection = null; + try { + URL url = new URL(RELEASES_API); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Accept", "application/vnd.github+json"); + connection.setRequestProperty("User-Agent", ApplicationLoader.getApplicationId()); + connection.setConnectTimeout(15000); + connection.setReadTimeout(15000); + + int code = connection.getResponseCode(); + if (code != HttpURLConnection.HTTP_OK) { + deliverError(delegate, "HTTP " + code); + return; + } + + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } + + GithubRelease release = new Gson().fromJson(sb.toString(), GithubRelease.class); + TLRPC.TL_help_appUpdate update = buildUpdate(release); + deliverSuccess(delegate, update); + } catch (Exception e) { + FileLog.e(e); + deliverError(delegate, e.getLocalizedMessage()); + } finally { + if (connection != null) { + connection.disconnect(); + } + checking = false; + } + } + + private TLRPC.TL_help_appUpdate buildUpdate(GithubRelease release) { + if (release == null || release.draft || release.prerelease || TextUtils.isEmpty(release.tagName)) { + return null; + } + if (!isNewerVersion(release.tagName, BuildConfig.VERSION_NAME)) { + return null; + } + String apkUrl = selectApkAsset(release.assets); + if (apkUrl == null) { + // No installable asset attached; fall back to the release page. + apkUrl = release.htmlUrl; + } + if (TextUtils.isEmpty(apkUrl)) { + return null; + } + TLRPC.TL_help_appUpdate update = new TLRPC.TL_help_appUpdate(); + update.version = stripVersionPrefix(release.tagName); + update.can_not_skip = false; + update.url = apkUrl; + update.flags |= 4; + String body = TextUtils.isEmpty(release.body) ? "" : release.body.trim(); + String title = TextUtils.isEmpty(release.name) ? update.version : release.name.trim(); + update.text = TextUtils.isEmpty(body) ? title : title + "\n\n" + body; + update.entities = new ArrayList<>(); + return update; + } + + private static String selectApkAsset(List assets) { + if (assets == null || assets.isEmpty()) { + return null; + } + String anyApk = null; + for (GithubAsset asset : assets) { + if (asset == null || TextUtils.isEmpty(asset.name) || TextUtils.isEmpty(asset.downloadUrl)) { + continue; + } + String name = asset.name.toLowerCase(); + if (!name.endsWith(".apk")) { + continue; + } + // The build only targets arm64-v8a, prefer a matching asset. + if (name.contains("arm64")) { + return asset.downloadUrl; + } + if (anyApk == null) { + anyApk = asset.downloadUrl; + } + } + return anyApk; + } + + private static String stripVersionPrefix(String tag) { + if (tag == null) { + return ""; + } + String t = tag.trim(); + if (t.startsWith("v") || t.startsWith("V")) { + t = t.substring(1); + } + return t; + } + + // Compares dotted numeric versions (e.g. "12.7.4" > "12.7.3"). Non-numeric + // suffixes are ignored. Returns true when remote is strictly newer. + private static boolean isNewerVersion(String remoteTag, String currentName) { + int[] remote = parseVersion(stripVersionPrefix(remoteTag)); + int[] current = parseVersion(currentName); + int len = Math.max(remote.length, current.length); + for (int i = 0; i < len; i++) { + int r = i < remote.length ? remote[i] : 0; + int c = i < current.length ? current[i] : 0; + if (r != c) { + return r > c; + } + } + return false; + } + + private static int[] parseVersion(String version) { + if (TextUtils.isEmpty(version)) { + return new int[0]; + } + String[] rawParts = version.split("\\."); + ArrayList parts = new ArrayList<>(); + for (String part : rawParts) { + var matcher = VERSION_PART.matcher(part); + if (matcher.find()) { + try { + parts.add(Integer.parseInt(matcher.group())); + } catch (NumberFormatException ignored) { + parts.add(0); + } + } + } + int[] result = new int[parts.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = parts.get(i); + } + return result; + } + + private void deliverSuccess(Delegate delegate, TLRPC.TL_help_appUpdate update) { + if (delegate == null) { + return; + } + AndroidUtilities.runOnUIThread(() -> delegate.onTLResponse(update, null)); + } + + private void deliverError(Delegate delegate, String error) { + if (delegate == null) { + return; + } + AndroidUtilities.runOnUIThread(() -> delegate.onTLResponse(null, error)); + } + + private static class GithubRelease { + @SerializedName("tag_name") + String tagName; + @SerializedName("name") + String name; + @SerializedName("body") + String body; + @SerializedName("html_url") + String htmlUrl; + @SerializedName("draft") + boolean draft; + @SerializedName("prerelease") + boolean prerelease; + @SerializedName("assets") + List assets; + } + + private static class GithubAsset { + @SerializedName("name") + String name; + @SerializedName("browser_download_url") + String downloadUrl; + @SerializedName("size") + long size; + } +}