207 lines
7.8 KiB
Java
207 lines
7.8 KiB
Java
package com.neatstudio.tmuxandroid;
|
|
|
|
import android.app.Activity;
|
|
import android.app.AlertDialog;
|
|
import android.content.ActivityNotFoundException;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.provider.Settings;
|
|
import android.widget.Toast;
|
|
|
|
import org.json.JSONObject;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.InputStream;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.URL;
|
|
import java.security.MessageDigest;
|
|
import java.util.Locale;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Executors;
|
|
|
|
final class UpdateManager {
|
|
interface Callback {
|
|
void onChecking(boolean checking);
|
|
void onMessage(String message);
|
|
}
|
|
|
|
private final Activity activity;
|
|
private final SharedPreferences prefs;
|
|
private final Callback callback;
|
|
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
|
|
UpdateManager(Activity activity, SharedPreferences prefs, Callback callback) {
|
|
this.activity = activity;
|
|
this.prefs = prefs;
|
|
this.callback = callback;
|
|
}
|
|
|
|
void check(boolean userInitiated) {
|
|
String manifestUrl = prefs.getString("update_url", BuildConfig.DEFAULT_UPDATE_URL);
|
|
callback.onChecking(true);
|
|
postMessage("Checking update...");
|
|
executor.execute(() -> {
|
|
try {
|
|
ReleaseInfo info = fetchReleaseInfo(manifestUrl);
|
|
if (info.versionCode <= BuildConfig.VERSION_CODE) {
|
|
postMessage("Already up to date: " + BuildConfig.VERSION_NAME);
|
|
return;
|
|
}
|
|
postMessage("Update found: " + info.versionName);
|
|
activity.runOnUiThread(() -> showUpdateDialog(info));
|
|
} catch (Exception error) {
|
|
postMessage(userInitiated ? "Update check failed: " + error.getMessage() : null);
|
|
} finally {
|
|
activity.runOnUiThread(() -> callback.onChecking(false));
|
|
}
|
|
});
|
|
}
|
|
|
|
private ReleaseInfo fetchReleaseInfo(String manifestUrl) throws Exception {
|
|
String json = readText(manifestUrl);
|
|
JSONObject root = new JSONObject(json);
|
|
return new ReleaseInfo(
|
|
root.getInt("versionCode"),
|
|
root.optString("versionName", ""),
|
|
root.getString("apkUrl"),
|
|
root.optString("sha256", ""),
|
|
root.optString("releasePageUrl", "")
|
|
);
|
|
}
|
|
|
|
private String readText(String url) throws Exception {
|
|
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
|
connection.setConnectTimeout(12000);
|
|
connection.setReadTimeout(12000);
|
|
connection.setRequestProperty("Accept", "application/json");
|
|
try (InputStream input = new BufferedInputStream(connection.getInputStream())) {
|
|
byte[] bytes = readAllBytes(input);
|
|
return new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
|
|
} finally {
|
|
connection.disconnect();
|
|
}
|
|
}
|
|
|
|
private void showUpdateDialog(ReleaseInfo info) {
|
|
new AlertDialog.Builder(activity)
|
|
.setTitle("Update available")
|
|
.setMessage("Install " + info.versionName + " now?")
|
|
.setNegativeButton("Later", null)
|
|
.setPositiveButton("Install", (dialog, which) -> downloadAndInstall(info))
|
|
.show();
|
|
}
|
|
|
|
private void downloadAndInstall(ReleaseInfo info) {
|
|
callback.onChecking(true);
|
|
postMessage("Downloading " + info.versionName + "...");
|
|
executor.execute(() -> {
|
|
try {
|
|
File apk = downloadApk(info);
|
|
if (!info.sha256.isEmpty()) {
|
|
postMessage("Verifying APK...");
|
|
String actual = sha256(apk);
|
|
if (!actual.equalsIgnoreCase(info.sha256)) {
|
|
throw new IllegalStateException("APK SHA-256 mismatch");
|
|
}
|
|
}
|
|
activity.runOnUiThread(() -> installApk(apk));
|
|
} catch (Exception error) {
|
|
postMessage("Update download failed: " + error.getMessage());
|
|
} finally {
|
|
activity.runOnUiThread(() -> callback.onChecking(false));
|
|
}
|
|
});
|
|
}
|
|
|
|
private File downloadApk(ReleaseInfo info) throws Exception {
|
|
File dir = new File(activity.getCacheDir(), "updates");
|
|
if (!dir.exists() && !dir.mkdirs()) {
|
|
throw new IllegalStateException("Cannot create update cache");
|
|
}
|
|
File apk = new File(dir, "tmux-android-" + info.versionCode + ".apk");
|
|
|
|
HttpURLConnection connection = (HttpURLConnection) new URL(info.apkUrl).openConnection();
|
|
connection.setConnectTimeout(12000);
|
|
connection.setReadTimeout(60000);
|
|
try (InputStream input = new BufferedInputStream(connection.getInputStream());
|
|
FileOutputStream output = new FileOutputStream(apk)) {
|
|
byte[] buffer = new byte[64 * 1024];
|
|
int read;
|
|
while ((read = input.read(buffer)) != -1) {
|
|
output.write(buffer, 0, read);
|
|
}
|
|
} finally {
|
|
connection.disconnect();
|
|
}
|
|
return apk;
|
|
}
|
|
|
|
private void installApk(File apk) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
|
&& !activity.getPackageManager().canRequestPackageInstalls()) {
|
|
Intent settingsIntent = new Intent(
|
|
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
|
Uri.parse("package:" + activity.getPackageName())
|
|
);
|
|
activity.startActivity(settingsIntent);
|
|
Toast.makeText(activity, "Allow installs, then run update again", Toast.LENGTH_LONG).show();
|
|
postMessage("Install permission required");
|
|
return;
|
|
}
|
|
|
|
Uri apkUri = UpdateFileProvider.getUriForFile(
|
|
activity,
|
|
activity.getPackageName() + ".fileprovider",
|
|
apk
|
|
);
|
|
Intent intent = new Intent(Intent.ACTION_VIEW)
|
|
.setDataAndType(apkUri, "application/vnd.android.package-archive")
|
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
try {
|
|
activity.startActivity(intent);
|
|
postMessage("Opened Android package installer");
|
|
} catch (ActivityNotFoundException error) {
|
|
postMessage("No package installer found");
|
|
}
|
|
}
|
|
|
|
private static byte[] readAllBytes(InputStream input) throws Exception {
|
|
java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream();
|
|
byte[] buffer = new byte[16 * 1024];
|
|
int read;
|
|
while ((read = input.read(buffer)) != -1) {
|
|
output.write(buffer, 0, read);
|
|
}
|
|
return output.toByteArray();
|
|
}
|
|
|
|
private static String sha256(File file) throws Exception {
|
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
try (InputStream input = new BufferedInputStream(new java.io.FileInputStream(file))) {
|
|
byte[] buffer = new byte[64 * 1024];
|
|
int read;
|
|
while ((read = input.read(buffer)) != -1) {
|
|
digest.update(buffer, 0, read);
|
|
}
|
|
}
|
|
byte[] bytes = digest.digest();
|
|
StringBuilder builder = new StringBuilder(bytes.length * 2);
|
|
for (byte item : bytes) {
|
|
builder.append(String.format(Locale.US, "%02x", item));
|
|
}
|
|
return builder.toString();
|
|
}
|
|
|
|
private void postMessage(String message) {
|
|
if (message == null) {
|
|
return;
|
|
}
|
|
activity.runOnUiThread(() -> callback.onMessage(message));
|
|
}
|
|
}
|