In-app update (#1244)

This commit is contained in:
南宫雪珊 2021-10-12 15:49:17 +08:00 committed by GitHub
parent 7d1a317120
commit 4c4427ca52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 314 additions and 77 deletions

View File

@ -161,7 +161,6 @@ tasks.whenTaskAdded {
dependencies {
val glideVersion = "4.12.0"
val okhttpVersion = "4.9.1"
val navVersion: String by rootProject.extra
annotationProcessor("com.github.bumptech.glide:compiler:$glideVersion")
implementation("androidx.activity:activity:1.3.1")
@ -178,9 +177,10 @@ dependencies {
implementation("com.google.android.material:material:1.5.0-alpha04")
implementation("com.google.code.gson:gson:2.8.8")
implementation("com.takisoft.preferencex:preferencex:1.1.0")
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.2"))
implementation("com.squareup.okhttp3:okhttp")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps")
implementation("com.squareup.okhttp3:logging-interceptor")
implementation("dev.rikka.rikkax.appcompat:appcompat:1.2.0-rc01")
implementation("dev.rikka.rikkax.core:core:1.3.2")
implementation("dev.rikka.rikkax.insets:insets:1.1.0")

View File

@ -35,14 +35,13 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import com.google.gson.JsonParser;
import org.lsposed.hiddenapibypass.HiddenApiBypass;
import org.lsposed.manager.repo.RepoLoader;
import org.lsposed.manager.ui.activity.CrashReportActivity;
import org.lsposed.manager.util.DoHDNS;
import org.lsposed.manager.util.ModuleUtil;
import org.lsposed.manager.util.ThemeUtil;
import org.lsposed.manager.util.UpdateUtil;
import java.io.ByteArrayOutputStream;
import java.io.File;
@ -50,19 +49,13 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import okhttp3.Cache;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import rikka.material.app.DayNightDelegate;
import rikka.material.app.LocaleDelegate;
@ -183,7 +176,7 @@ public class App extends Application {
}
}, new IntentFilter(Intent.ACTION_PACKAGE_CHANGED));
loadRemoteVersion();
UpdateUtil.loadRemoteVersion();
RepoLoader.getInstance().loadRemoteData();
executorService.submit(HTML_TEMPLATE);
@ -215,58 +208,6 @@ public class App extends Application {
return okHttpCache;
}
private void loadRemoteVersion() {
var request = new Request.Builder()
.url("https://api.github.com/repos/LSPosed/LSPosed/releases/latest")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();
var callback = new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (!response.isSuccessful()) return;
var body = response.body();
if (body == null) return;
try {
var info = JsonParser.parseReader(body.charStream()).getAsJsonObject();
var name = info.getAsJsonArray("assets").get(0).getAsJsonObject().get("name").getAsString();
var code = Integer.parseInt(name.split("-", 4)[2]);
var now = Instant.now().getEpochSecond();
pref.edit()
.putInt("latest_version", code)
.putLong("latest_check", now)
.putBoolean("checked", true)
.apply();
} catch (Throwable t) {
Log.e(App.TAG, t.getMessage(), t);
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e(App.TAG, "loadRemoteVersion: " + e.getMessage());
if (pref.getBoolean("checked", false)) return;
pref.edit().putBoolean("checked", true).apply();
}
};
getOkHttpClient().newCall(request).enqueue(callback);
}
public static boolean needUpdate() {
var pref = getPreferences();
if (!pref.getBoolean("checked", false)) return false;
var now = Instant.now();
var buildTime = Instant.ofEpochSecond(BuildConfig.BUILD_TIME);
var check = pref.getLong("latest_check", 0);
if (check > 0) {
var checkTime = Instant.ofEpochSecond(check);
if (checkTime.atOffset(ZoneOffset.UTC).plusDays(30).toInstant().isBefore(now))
return true;
var code = pref.getInt("latest_version", 0);
return code > BuildConfig.VERSION_CODE;
}
return buildTime.atOffset(ZoneOffset.UTC).plusDays(30).toInstant().isBefore(now);
}
public static Locale getLocale() {
String tag = getPreferences().getString("language", null);
if (TextUtils.isEmpty(tag) || "SYSTEM".equals(tag)) {

View File

@ -341,4 +341,12 @@ public class ConfigManager {
return new HashMap<>();
}
}
public static void flashZip(String zipPath, ParcelFileDescriptor outputStream) {
try {
LSPManagerServiceHolder.getService().flashZip(zipPath, outputStream);
} catch (RemoteException e) {
Log.e(App.TAG, Log.getStackTraceString(e));
}
}
}

View File

@ -0,0 +1,108 @@
package org.lsposed.manager.ui.dialog;
import static org.lsposed.manager.App.TAG;
import android.content.Context;
import android.graphics.Typeface;
import android.os.ParcelFileDescriptor;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.textview.MaterialTextView;
import org.lsposed.manager.App;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R;
import org.lsposed.manager.databinding.DialogWarningBinding;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import rikka.widget.borderview.BorderNestedScrollView;
public class FlashDialogBuilder extends BlurBehindDialogBuilder {
private final String zipPath;
private final TextView textView;
private final BorderNestedScrollView rootView;
public FlashDialogBuilder(@NonNull Context context, @NonNull String zipPath) {
super(context);
this.zipPath = zipPath;
setTitle(R.string.update_lsposed);
textView = new MaterialTextView(context);
textView.setText(R.string.update_lsposed_msg);
textView.setMovementMethod(LinkMovementMethod.getInstance());
LayoutInflater inflater = LayoutInflater.from(context);
DialogWarningBinding binding = DialogWarningBinding.inflate(inflater, null, false);
binding.container.addView(textView);
rootView = binding.getRoot();
setView(rootView);
setNegativeButton(android.R.string.cancel, null);
setPositiveButton(R.string.install, null);
setCancelable(false);
}
@Override
public AlertDialog show() {
var dialog = super.show();
var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener((v) -> setFlashView(v, dialog));
return dialog;
}
private void setFlashView(View view, AlertDialog dialog) {
var positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
var negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
positiveButton.setEnabled(false);
positiveButton.setText(android.R.string.ok);
positiveButton.setOnClickListener((v) -> dialog.dismiss());
negativeButton.setVisibility(View.GONE);
textView.setText("");
textView.setTypeface(Typeface.MONOSPACE);
App.getExecutorService().submit(() -> flash(view, positiveButton));
}
private void flash(View view, Button button) {
try {
var pipe = ParcelFileDescriptor.createReliablePipe();
var readSide = pipe[0];
var writeSide = pipe[1];
ConfigManager.flashZip(zipPath, writeSide);
writeSide.close();
var inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readSide);
var reader = new BufferedReader(new InputStreamReader(inputStream));
for (var line = ""; line != null; line = reader.readLine()) {
if (line.length() > 0) {
var showLine = line + "\n";
view.post(() -> {
textView.append(showLine);
rootView.fullScroll(View.FOCUS_DOWN);
});
}
}
reader.close();
} catch (IOException e) {
Log.e(TAG, "flash", e);
view.post(() -> textView.append("\n\n" + e.getMessage()));
rootView.fullScroll(View.FOCUS_DOWN);
}
view.post(() -> button.setEnabled(true));
}
}

View File

@ -43,17 +43,17 @@ public class InfoDialogBuilder extends BlurBehindDialogBuilder {
if (ConfigManager.isBinderAlive()) {
binding.apiVersion.setText(String.valueOf(ConfigManager.getXposedApiVersion()));
binding.frameworkVersion.setText(String.format(Locale.US, "%s (%s)", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode()));
binding.frameworkVersion.setText(String.format(Locale.ROOT, "%s (%s)", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode()));
} else {
binding.apiVersion.setText(R.string.not_installed);
binding.frameworkVersion.setText(R.string.not_installed);
}
binding.managerVersion.setText(String.format(Locale.US, "%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
binding.managerVersion.setText(String.format(Locale.ROOT, "%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
if (Build.VERSION.PREVIEW_SDK_INT != 0) {
binding.systemVersion.setText(String.format(Locale.US, "%1$s Preview (API %2$d)", Build.VERSION.CODENAME, Build.VERSION.SDK_INT));
binding.systemVersion.setText(String.format(Locale.ROOT, "%1$s Preview (API %2$d)", Build.VERSION.CODENAME, Build.VERSION.SDK_INT));
} else {
binding.systemVersion.setText(String.format(Locale.US, "%1$s (API %2$d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
binding.systemVersion.setText(String.format(Locale.ROOT, "%1$s (API %2$d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
}
binding.device.setText(getDevice());

View File

@ -70,7 +70,7 @@ public class AppListFragment extends BaseFragment {
binding.appBar.setLifted(true);
String title;
if (module.userId != 0) {
title = String.format(Locale.US, "%s (%d)", module.getAppName(), module.userId);
title = String.format(Locale.ROOT, "%s (%d)", module.getAppName(), module.userId);
} else {
title = module.getAppName();
}

View File

@ -43,10 +43,12 @@ import org.lsposed.manager.databinding.DialogAboutBinding;
import org.lsposed.manager.databinding.FragmentHomeBinding;
import org.lsposed.manager.receivers.LSPManagerServiceHolder;
import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
import org.lsposed.manager.ui.dialog.FlashDialogBuilder;
import org.lsposed.manager.ui.dialog.InfoDialogBuilder;
import org.lsposed.manager.ui.dialog.WarningDialogBuilder;
import org.lsposed.manager.util.ModuleUtil;
import org.lsposed.manager.util.NavUtil;
import org.lsposed.manager.util.UpdateUtil;
import org.lsposed.manager.util.chrome.LinkTransformationMethod;
import java.util.Locale;
@ -92,13 +94,20 @@ public class HomeFragment extends BaseFragment {
Activity activity = requireActivity();
binding.status.setOnClickListener(v -> {
if (ConfigManager.isBinderAlive() && !App.needUpdate()) {
if (ConfigManager.isBinderAlive() && !UpdateUtil.needUpdate()) {
if (!ConfigManager.isSepolicyLoaded() || !ConfigManager.systemServerRequested() || !ConfigManager.dex2oatFlagsLoaded()) {
new WarningDialogBuilder(activity).show();
} else {
new InfoDialogBuilder(activity).setTitle(R.string.info).show();
new InfoDialogBuilder(activity).show();
}
} else {
if (UpdateUtil.canUpdate()) {
var zip = App.getPreferences().getString("zip_file", null);
if (zip != null) {
new FlashDialogBuilder(activity, zip).show();
return;
}
}
NavUtil.startURL(activity, getString(R.string.about_source));
}
});
@ -108,7 +117,7 @@ public class HomeFragment extends BaseFragment {
binding.settings.setOnClickListener(new StartFragmentListener(R.id.action_settings_fragment, false));
binding.issue.setOnClickListener(view -> NavUtil.startURL(activity, "https://github.com/LSPosed/LSPosed/issues"));
updateStates(requireActivity(), ConfigManager.isBinderAlive(), App.needUpdate());
updateStates(requireActivity(), ConfigManager.isBinderAlive(), UpdateUtil.needUpdate());
return binding.getRoot();
}
@ -116,7 +125,7 @@ public class HomeFragment extends BaseFragment {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.menu_refresh) {
updateStates(requireActivity(), ConfigManager.isBinderAlive(), App.needUpdate());
updateStates(requireActivity(), ConfigManager.isBinderAlive(), UpdateUtil.needUpdate());
} else if (itemId == R.id.menu_info) {
new InfoDialogBuilder(requireActivity()).setTitle(R.string.info).show();
} else if (itemId == R.id.menu_about) {
@ -129,7 +138,7 @@ public class HomeFragment extends BaseFragment {
R.string.about_view_source_code,
"<b><a href=\"https://github.com/LSPosed/LSPosed\">GitHub</a></b>",
"<b><a href=\"https://t.me/LSPosed\">Telegram</a></b>"), HtmlCompat.FROM_HTML_MODE_LEGACY));
binding.designAboutVersion.setText(String.format(Locale.US, "%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
binding.designAboutVersion.setText(String.format(Locale.ROOT, "%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
new BlurBehindDialogBuilder(activity)
.setView(binding.getRoot())
.show();
@ -140,7 +149,7 @@ public class HomeFragment extends BaseFragment {
private void updateStates(Activity activity, boolean binderAlive, boolean needUpdate) {
int cardBackgroundColor;
if (binderAlive) {
StringBuilder sb = new StringBuilder(String.format(Locale.US, "%s (%d)",
StringBuilder sb = new StringBuilder(String.format(Locale.ROOT, "%s (%d)",
ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode()));
if (needUpdate) {
cardBackgroundColor = ResourceUtils.resolveColor(activity.getTheme(), R.attr.colorInstall);

View File

@ -0,0 +1,115 @@
package org.lsposed.manager.util;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonParser;
import org.lsposed.manager.App;
import org.lsposed.manager.BuildConfig;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneOffset;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Request;
import okhttp3.Response;
import okio.Okio;
public class UpdateUtil {
public static void loadRemoteVersion() {
var pref = App.getPreferences();
var request = new Request.Builder()
.url("https://api.github.com/repos/LSPosed/LSPosed/releases/latest")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();
var callback = new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (!response.isSuccessful()) return;
var body = response.body();
if (body == null) return;
try {
var info = JsonParser.parseReader(body.charStream()).getAsJsonObject();
var assets = info.getAsJsonArray("assets").get(0).getAsJsonObject();
var name = assets.get("name").getAsString();
var code = Integer.parseInt(name.split("-", 4)[2]);
var now = Instant.now().getEpochSecond();
pref.edit()
.putInt("latest_version", code)
.putLong("latest_check", now)
.putBoolean("checked", true)
.apply();
var updatedAt = Instant.parse(assets.get("updated_at").getAsString());
var downloadUrl = assets.get("browser_download_url").getAsString();
var nowZipTime = pref.getLong("zip_time", BuildConfig.BUILD_TIME);
if (updatedAt.isAfter(Instant.ofEpochSecond(nowZipTime))) {
var zip = downloadNewZipSync(downloadUrl, name);
var size = assets.get("size").getAsLong();
if (zip != null && zip.length() == size) {
pref.edit()
.putLong("zip_time", updatedAt.getEpochSecond())
.putString("zip_file", zip.getAbsolutePath())
.apply();
}
}
} catch (Throwable t) {
Log.e(App.TAG, t.getMessage(), t);
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e(App.TAG, "loadRemoteVersion: " + e.getMessage());
if (pref.getBoolean("checked", false)) return;
pref.edit().putBoolean("checked", true).apply();
}
};
App.getOkHttpClient().newCall(request).enqueue(callback);
}
public static boolean needUpdate() {
var pref = App.getPreferences();
if (!pref.getBoolean("checked", false)) return false;
var now = Instant.now();
var buildTime = Instant.ofEpochSecond(BuildConfig.BUILD_TIME);
var check = pref.getLong("latest_check", 0);
if (check > 0) {
var checkTime = Instant.ofEpochSecond(check);
if (checkTime.atOffset(ZoneOffset.UTC).plusDays(30).toInstant().isBefore(now))
return true;
var code = pref.getInt("latest_version", 0);
return code > BuildConfig.VERSION_CODE;
}
return buildTime.atOffset(ZoneOffset.UTC).plusDays(30).toInstant().isBefore(now);
}
@Nullable
private static File downloadNewZipSync(String url, String name) {
var request = new Request.Builder().url(url).build();
var zip = new File(App.getInstance().getCacheDir(), name);
try (Response response = App.getOkHttpClient().newCall(request).execute()) {
var body = response.body();
if (!response.isSuccessful() || body == null) return null;
try (var source = body.source();
var sink = Okio.buffer(Okio.sink(zip))) {
sink.writeAll(source);
}
} catch (IOException e) {
Log.e(App.TAG, "downloadNewZipSync: " + e.getMessage());
return null;
}
return zip;
}
public static boolean canUpdate() {
var pref = App.getPreferences();
var zipTime = pref.getLong("zip_time", BuildConfig.BUILD_TIME);
return zipTime > BuildConfig.BUILD_TIME;
}
}

View File

@ -59,6 +59,8 @@
<string name="create_shortcut">创建快捷方式</string>
<string name="never_show">不再显示</string>
<string name="failed_to_create_shortcut">创建快捷方式失败:%1$s</string>
<string name="update_lsposed">更新 LSPosed</string>
<string name="update_lsposed_msg">是否安装新版 LSPosed完成后设备将自动重启</string>
<!-- LogsActivity -->
<string name="menuSaveToSd">保存</string>
<string name="nav_item_logs_lsp">LSPosed 日志</string>

View File

@ -61,6 +61,8 @@
<string name="create_shortcut">Create shortcut</string>
<string name="never_show">Never show</string>
<string name="failed_to_create_shortcut">Failed to create shortcut: %1$s</string>
<string name="update_lsposed">Update LSPosed</string>
<string name="update_lsposed_msg">Want to install a new version of LSPosed? The device will reboot after completion</string>
<!-- LogsActivity -->
<string name="menuSaveToSd">Save</string>

View File

@ -309,13 +309,32 @@ val pushLspdNative = task("pushLspdNative", Exec::class) {
}
commandLine(adb, "push", "libdaemon.so", "/data/local/tmp/libdaemon.so")
}
task("reRunLspd", Exec::class) {
val reRunLspd = task("reRunLspd", Exec::class) {
dependsOn(pushLspd)
dependsOn(pushLspdNative)
dependsOn(killLspd)
commandLine(adb, "shell", "su", "-c", "sh /data/adb/modules/riru_lsposed/service.sh&")
isIgnoreExitValue = true
}
val tmpApk = "/data/local/tmp/lsp.apk"
val pushApk = task("pushApk", Exec::class) {
dependsOn(":app:assembleDebug")
workingDir("${project(":app").buildDir}/outputs/apk/debug")
commandLine(adb, "push", "LSPosedManager-v$verName-$verCode-debug.apk", tmpApk)
}
val openApp = task("openApp", Exec::class) {
commandLine(
adb, "shell", "am start -a android.intent.action.MAIN " +
"-c org.lsposed.manager.LAUNCH_MANAGER " +
"com.android.shell/.BugreportWarningActivity"
)
}
task("reRunApp", Exec::class) {
dependsOn(pushApk)
commandLine(adb, "shell", "su", "-c", "mv -f $tmpApk /data/adb/lspd/manager.apk")
isIgnoreExitValue = true
finalizedBy(reRunLspd)
}
val generateVersion = task("generateVersion", Copy::class) {
inputs.property("VERSION_CODE", verCode)

View File

@ -22,6 +22,7 @@ package org.lsposed.lspd.service;
import static android.content.Context.BIND_AUTO_CREATE;
import static org.lsposed.lspd.service.ServiceManager.TAG;
import android.annotation.SuppressLint;
import android.app.INotificationManager;
import android.app.IServiceConnection;
import android.app.Notification;
@ -65,12 +66,15 @@ import org.lsposed.lspd.util.FakeContext;
import org.lsposed.lspd.util.Utils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import de.robv.android.xposed.XposedBridge;
import hidden.HiddenApiBridge;
@ -254,6 +258,7 @@ public class LSPManagerService extends ILSPManagerService.Stub {
}
}
@SuppressLint("WrongConstant")
public static void broadcastIntent(String modulePackageName, int moduleUserId, boolean packageFullyRemoved) {
Intent intent = new Intent(Intent.ACTION_PACKAGE_CHANGED);
intent.addFlags(0x01000000); //Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
@ -713,4 +718,30 @@ public class LSPManagerService extends ILSPManagerService.Stub {
createOrUpdateShortcut(true);
setAddShortcut(true);
}
@Override
public void flashZip(String zipPath, ParcelFileDescriptor outputStream) {
var processBuilder = new ProcessBuilder("magisk", "--install-module", zipPath);
var fd = new File("/proc/self/fd/" + outputStream.getFd());
processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(fd));
try (outputStream; var fdw = new FileOutputStream(fd, true)) {
var proc = processBuilder.start();
if (proc.waitFor(10, TimeUnit.SECONDS)) {
var exit = proc.exitValue();
if (exit == 0) {
fdw.write("- Reboot after 5s\n".getBytes());
Thread.sleep(5000);
reboot(false);
} else {
var s = "! Flash failed, exit with " + exit + "\n";
fdw.write(s.getBytes());
}
} else {
proc.destroy();
fdw.write("! Timeout, abort\n".getBytes());
}
} catch (IOException | InterruptedException e) {
Log.e(TAG, "flashZip: ", e);
}
}
}

View File

@ -71,4 +71,6 @@ interface ILSPManagerService {
boolean isAddShortcut() = 37;
void setAddShortcut(boolean enabled) = 38;
oneway void flashZip(String zipPath, in ParcelFileDescriptor outputStream) = 39;
}