Add in-app download and silent install for GitHub updates

- ApkInstaller.downloadAndInstall downloads APK by URL with a progress dialog
- Reuses existing PackageInstaller silent-install pipeline via shared File path
- Update dialogs now download in-app instead of opening a browser
- Add UpdateDownloading string

Verified: TMessagesProj compileReleaseJavaWithJavac succeeds.
This commit is contained in:
instant992 2026-06-09 03:24:22 +04:00
parent 12792f77f3
commit d79c149e3e
4 changed files with 126 additions and 3 deletions

View file

@ -149,8 +149,12 @@ public class BlockingUpdateView extends FrameLayout implements NotificationCente
showProgress(true);
}
} else if (appUpdate.url != null) {
if (getContext() instanceof Activity activity) {
ApkInstaller.downloadAndInstall(activity, appUpdate.url, appUpdate.version);
} else {
Browser.openUrl(getContext(), appUpdate.url);
}
}
});
acceptTextView = new TextView(context);

View file

@ -4,6 +4,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
@ -38,6 +39,8 @@ import org.telegram.ui.ActionBar.Theme;
import org.telegram.ui.Components.spoilers.SpoilersTextView;
import org.telegram.ui.Stories.recorder.ButtonWithCounterView;
import tw.nekomimi.nekogram.helpers.ApkInstaller;
public class UpdateAppAlertDialog extends BottomSheet {
private TLRPC.TL_help_appUpdate appUpdate;
@ -297,9 +300,13 @@ public class UpdateAppAlertDialog extends BottomSheet {
doneButton.setOnClickListener(v -> {
if (update.document instanceof TLRPC.TL_document) {
FileLoader.getInstance(accountNum).loadFile(appUpdate.document, "update", FileLoader.PRIORITY_NORMAL, 1);
} else if (appUpdate.url != null) {
if (getContext() instanceof Activity activity) {
ApkInstaller.downloadAndInstall(activity, appUpdate.url, appUpdate.version);
} else {
Browser.openUrl(context, appUpdate.url);
}
}
dismiss();
});
container.addView(doneButton, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.FILL_HORIZONTAL | Gravity.BOTTOM, 20, 0, 20, 48 + 4 + 8));

View file

@ -34,6 +34,7 @@ import org.telegram.messenger.R;
import org.telegram.messenger.UserConfig;
import org.telegram.messenger.Utilities;
import org.telegram.messenger.XiaomiUtilities;
import org.telegram.messenger.browser.Browser;
import org.telegram.tgnet.TLRPC;
import org.telegram.ui.ActionBar.AlertDialog;
import org.telegram.ui.ActionBar.Theme;
@ -44,9 +45,12 @@ import org.telegram.ui.LaunchActivity;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -107,6 +111,113 @@ public final class ApkInstaller {
if (apk == null) {
return;
}
showInstallDialog(context, apk);
}
// Downloads an APK from a direct URL (e.g. a GitHub release asset) with a
// progress dialog, then performs the same silent install as the Telegram
// document update flow.
public static void downloadAndInstall(Activity context, String url, String version) {
if (context == null || TextUtils.isEmpty(url)) {
return;
}
// Not a direct apk link (e.g. a release page) just open it.
if (!url.toLowerCase().endsWith(".apk")) {
Browser.openUrl(context, url);
return;
}
if (dialog != null && dialog.isShowing()) {
return;
}
var progressDialog = new AlertDialog(context, AlertDialog.ALERT_TYPE_LOADING);
progressDialog.setMessage(LocaleController.getString(R.string.UpdateDownloading));
progressDialog.setCanCancel(false);
progressDialog.show();
dialog = progressDialog;
Utilities.globalQueue.postRunnable(() -> {
File apk = downloadApk(context, url, version, progress -> AndroidUtilities.runOnUIThread(() -> {
if (dialog != null && dialog.isShowing()) {
dialog.setProgress(progress);
}
}));
AndroidUtilities.runOnUIThread(() -> {
if (dialog != null) {
dialog.dismiss();
dialog = null;
}
if (apk == null) {
AlertsCreator.createSimpleAlert(context, LocaleController.getString(R.string.ErrorOccurred)).show();
return;
}
showInstallDialog(context, apk);
});
});
}
// @WorkerThread @Nullable
private static File downloadApk(Activity context, String url, String version, Utilities.Callback<Integer> onProgress) {
HttpURLConnection connection = null;
try {
File dir = FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE);
String name = "update-" + (TextUtils.isEmpty(version) ? "latest" : version.replaceAll("[^A-Za-z0-9._-]", "_")) + ".apk";
File apk = new File(dir, name);
if (apk.exists()) {
//noinspection ResultOfMethodCallIgnored
apk.delete();
}
URL downloadUrl = new URL(url);
connection = (HttpURLConnection) downloadUrl.openConnection();
connection.setInstanceFollowRedirects(true);
connection.setConnectTimeout(15000);
connection.setReadTimeout(30000);
connection.setRequestProperty("User-Agent", ApplicationLoader.getApplicationId());
int code = connection.getResponseCode();
if (code != HttpURLConnection.HTTP_OK) {
FileLog.e("ApkInstaller: download failed, HTTP " + code);
return null;
}
long total = connection.getContentLength();
long downloaded = 0;
int lastReported = -1;
try (InputStream in = connection.getInputStream();
OutputStream out = new FileOutputStream(apk)) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) >= 0) {
out.write(buffer, 0, read);
downloaded += read;
if (total > 0 && onProgress != null) {
int progress = (int) (downloaded * 100 / total);
if (progress != lastReported) {
lastReported = progress;
onProgress.run(progress);
}
}
}
}
return apk;
} catch (Exception e) {
FileLog.e(e);
return null;
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private static void showInstallDialog(Activity context, File apk) {
if (context == null || apk == null || !apk.exists()) {
return;
}
if (hasBrokenPackageInstaller(context)) {
AndroidUtilities.openForView(apk, "install.apk", "application/vnd.android.package-archive", context, null, false);
return;
}
if (dialog != null && dialog.isShowing()) {
return;
}

View file

@ -149,6 +149,7 @@
<string name="ShareNekogram">Share FoxiGram...</string>
<string name="NekogramVersion">FoxiGram %1$s\nBased on Telegram %2$s\nDesigned by %3$s</string>
<string name="UpdateInstalling">Installing update...</string>
<string name="UpdateDownloading">Downloading update...</string>
<string name="UpdateInstallingNotification">A notification will be shown when the update completes.</string>
<string name="UpdateInstallingRelaunch">The app will relaunch when the update completes.</string>
<string name="UpdateInstalledNotification">Update installation finished, tap to launch the app.</string>