/* * This file is part of LSPosed. * * LSPosed is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LSPosed is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LSPosed. If not, see . * * Copyright (C) 2020 EdXposed Contributors * Copyright (C) 2021 LSPosed Contributors */ package org.lsposed.manager; import android.app.Application; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Build; import android.os.Process; import android.system.Os; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import com.google.android.material.color.DynamicColors; 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.theme.ThemeUtil; import java.io.ByteArrayOutputStream; import java.io.File; 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; public class App extends Application { public static final FutureTask HTML_TEMPLATE = new FutureTask<>(() -> readWebviewHTML("template.html")); public static final FutureTask HTML_TEMPLATE_DARK = new FutureTask<>(() -> readWebviewHTML("template_dark.html")); private static String readWebviewHTML(String name) { try { var input = App.getInstance().getAssets().open("webview/" + name); var result = new ByteArrayOutputStream(1024); var buffer = new byte[1024]; for (int length; (length = input.read(buffer)) != -1; ) { result.write(buffer, 0, length); } return result.toString(StandardCharsets.UTF_8.name()); } catch (IOException e) { Log.e(App.TAG, "read webview HTML", e); return "@body@"; } } static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // TODO: set specific class name HiddenApiBypass.addHiddenApiExemptions(""); } } public static final String TAG = "LSPosedManager"; private static App instance = null; private static OkHttpClient okHttpClient; private static Cache okHttpCache; private static boolean parasiticShown = false; private SharedPreferences pref; private ExecutorService executorService; public static App getInstance() { return instance; } public static SharedPreferences getPreferences() { return instance.pref; } public static ExecutorService getExecutorService() { return instance.executorService; } public static boolean isParasitic() { return !Process.isApplicationUid(Process.myUid()); } public static boolean isParasiticShown() { return parasiticShown; } public static void setParasiticShown(boolean parasiticShown) { App.parasiticShown = parasiticShown; } private void setCrashReport() { Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); throwable.printStackTrace(pw); String stackTraceString = sw.toString(); //Reduce data to 128KB so we don't get a TransactionTooLargeException when sending the intent. //The limit is 1MB on Android but some devices seem to have it lower. //See: http://developer.android.com/reference/android/os/TransactionTooLargeException.html //And: http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception#comment46697371_12809171 if (stackTraceString.length() > 131071) { String disclaimer = " [stack trace too large]"; stackTraceString = stackTraceString.substring(0, 131071 - disclaimer.length()) + disclaimer; } Intent intent = new Intent(App.this, CrashReportActivity.class); intent.putExtra(BuildConfig.APPLICATION_ID + ".EXTRA_STACK_TRACE", stackTraceString); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); App.this.startActivity(intent); System.exit(10); Process.killProcess(Os.getpid()); }); } @Override public void onCreate() { super.onCreate(); if (!BuildConfig.DEBUG && !isParasitic()) { setCrashReport(); } instance = this; executorService = Executors.newCachedThreadPool(); pref = PreferenceManager.getDefaultSharedPreferences(this); if ("CN".equals(Locale.getDefault().getCountry())) { if (!pref.contains("doh")) { pref.edit().putBoolean("doh", true).apply(); } } DayNightDelegate.setApplicationContext(this); DayNightDelegate.setDefaultNightMode(ThemeUtil.getDarkTheme()); LocaleDelegate.setDefaultLocale(getLocale()); if (ThemeUtil.isSystemAccent()) { DynamicColors.applyToActivitiesIfAvailable(this); } registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int userId = intent.getIntExtra(Intent.EXTRA_USER, 0); String packageName = intent.getStringExtra("android.intent.extra.PACKAGES"); boolean packageFullyRemoved = intent.getBooleanExtra(Intent.ACTION_PACKAGE_FULLY_REMOVED, false); if (packageName != null) { ModuleUtil.getInstance().reloadSingleModule(packageName, userId, packageFullyRemoved); } } }, new IntentFilter(Intent.ACTION_PACKAGE_CHANGED)); loadRemoteVersion(); RepoLoader.getInstance().loadRemoteData(); executorService.submit(HTML_TEMPLATE); executorService.submit(HTML_TEMPLATE_DARK); } @NonNull public static OkHttpClient getOkHttpClient() { if (okHttpClient == null) { OkHttpClient.Builder builder = new OkHttpClient.Builder().cache(getOkHttpCache()); builder.addInterceptor(chain -> { var request = chain.request().newBuilder(); request.header("User-Agent", TAG); return chain.proceed(request.build()); }); HttpLoggingInterceptor log = new HttpLoggingInterceptor(); log.setLevel(HttpLoggingInterceptor.Level.HEADERS); if (BuildConfig.DEBUG) builder.addInterceptor(log); okHttpClient = builder.dns(new DoHDNS(builder.build())).build(); } return okHttpClient; } @NonNull private static Cache getOkHttpCache() { if (okHttpCache == null) { okHttpCache = new Cache(new File(App.getInstance().getCacheDir(), "http_cache"), 50L * 1024L * 1024L); } 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)) { return Locale.getDefault(); } return Locale.forLanguageTag(tag); } }