commit 5dddd6290bc59bd986c54cc49690d07fe1aad468
Author: NekoInverter <42698724+NekoInverter@users.noreply.github.com>
Date: Mon Feb 3 18:57:01 2020 +0800
first commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..603b1407
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 00000000..681f41ae
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 00000000..d291b3d7
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..bed10fbd
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 00000000..7f68460d
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 00000000..0e1ab904
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,41 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 28
+ buildToolsVersion "29.0.2"
+ defaultConfig {
+ applicationId "org.meowcat.edxposed.manager"
+ minSdkVersion 21
+ targetSdkVersion 27
+ versionCode 45401
+ versionName "4.5.4"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'com.google.android.material:material:1.2.0-alpha04'
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
+ implementation 'com.github.bumptech.glide:glide:4.11.0'
+ implementation "com.github.topjohnwu.libsu:core:2.5.0"
+ implementation 'androidx.browser:browser:1.2.0'
+ implementation 'com.timehop.stickyheadersrecyclerview:library:0.4.3@aar'
+ implementation 'com.takisoft.preferencex:preferencex:1.1.0'
+ implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.1.0'
+ implementation "androidx.recyclerview:recyclerview:1.2.0-alpha01"
+ implementation 'com.annimon:stream:1.2.0'
+ implementation 'com.google.code.gson:gson:2.8.6'
+ implementation 'de.psdev.licensesdialog:licensesdialog:1.8.3'
+}
diff --git a/app/libs/AndroidHiddenAPI.jar b/app/libs/AndroidHiddenAPI.jar
new file mode 100644
index 00000000..113acc1e
Binary files /dev/null and b/app/libs/AndroidHiddenAPI.jar differ
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..723e37be
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java b/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java
new file mode 100644
index 00000000..0a98c2de
--- /dev/null
+++ b/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java
@@ -0,0 +1,56 @@
+package de.robv.android.xposed.installer;
+
+import android.annotation.SuppressLint;
+import android.app.Application;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import de.robv.android.xposed.installer.util.InstallZipUtil;
+
+import static de.robv.android.xposed.installer.util.InstallZipUtil.parseXposedProp;
+
+@SuppressLint("Registered")
+public class XposedApp extends Application {
+ public static final String TAG = "XposedApp";
+ private static final File EDXPOSED_PROP_FILE = new File("/system/framework/edconfig.jar");
+ private static XposedApp mInstance = null;
+ public InstallZipUtil.XposedProp mXposedProp;
+
+ public static XposedApp getInstance() {
+ return mInstance;
+ }
+
+ // This method is hooked by XposedBridge to return the current version
+ public static Integer getActiveXposedVersion() {
+ Log.d(TAG, "EdXposed is not active");
+ return -1;
+ }
+
+ public void onCreate() {
+ super.onCreate();
+ mInstance = this;
+ }
+
+ public void reloadXposedProp() {
+ InstallZipUtil.XposedProp prop = null;
+ File file = null;
+
+ if (EDXPOSED_PROP_FILE.canRead()) {
+ file = EDXPOSED_PROP_FILE;
+ }
+
+ if (file != null) {
+ try (FileInputStream is = new FileInputStream(file)) {
+ prop = parseXposedProp(is);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not read " + file.getPath(), e);
+ }
+ }
+ synchronized (this) {
+ mXposedProp = prop;
+ }
+ }
+}
diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java
new file mode 100644
index 00000000..b2797198
--- /dev/null
+++ b/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java
@@ -0,0 +1,57 @@
+package de.robv.android.xposed.installer.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.meowcat.edxposed.manager.util.ModuleUtil;
+
+public final class InstallZipUtil {
+
+ public static XposedProp parseXposedProp(InputStream is) throws IOException {
+ XposedProp prop = new XposedProp();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String[] parts = line.split("=", 2);
+ if (parts.length != 2) {
+ continue;
+ }
+
+ String key = parts[0].trim();
+ if (key.charAt(0) == '#') {
+ continue;
+ }
+
+ String value = parts[1].trim();
+
+ if ("version".equals(key)) {
+ prop.mVersion = value;
+ prop.mVersionInt = ModuleUtil.extractIntPart(value);
+ }
+ }
+ reader.close();
+ return prop.isComplete() ? prop : null;
+ }
+
+ public static class XposedProp {
+ private String mVersion = null;
+ private int mVersionInt = 0;
+ //private Set mRequires = new HashSet<>();
+
+ private boolean isComplete() {
+ return mVersion != null
+ && mVersionInt > 0;
+ }
+
+ public String getVersion() {
+ return mVersion;
+ }
+
+// public int getVersionInt() {
+// return mVersionInt;
+// }
+
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/AboutActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/AboutActivity.java
new file mode 100644
index 00000000..03171453
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/AboutActivity.java
@@ -0,0 +1,108 @@
+package org.meowcat.edxposed.manager;
+
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.text.Html;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.appcompat.app.ActionBar;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import org.meowcat.edxposed.manager.util.NavUtil;
+
+import de.psdev.licensesdialog.LicensesDialog;
+import de.psdev.licensesdialog.licenses.ApacheSoftwareLicense20;
+import de.psdev.licensesdialog.licenses.MITLicense;
+import de.psdev.licensesdialog.model.Notice;
+import de.psdev.licensesdialog.model.Notices;
+
+public class AboutActivity extends BaseActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_about);
+ setSupportActionBar(findViewById(R.id.toolbar));
+ ActionBar bar = getSupportActionBar();
+ if (bar != null) {
+ bar.setDisplayHomeAsUpEnabled(true);
+ }
+ View changelogView = findViewById(R.id.changelogView);
+ View licensesView = findViewById(R.id.licensesView);
+ View translatorsView = findViewById(R.id.translatorsView);
+ View sourceCodeView = findViewById(R.id.sourceCodeView);
+ View tgChannelView = findViewById(R.id.tgChannelView);
+ View installerSupportView = findViewById(R.id.installerSupportView);
+ View faqView = findViewById(R.id.faqView);
+ View donateView = findViewById(R.id.donateView);
+ TextView txtModuleSupport = findViewById(R.id.tab_support_module_description);
+ View qqGroupView = findViewById(R.id.qqGroupView);
+ View tgGroupView = findViewById(R.id.tgGroupView);
+
+ String packageName = getPackageName();
+ String translator = getResources().getString(R.string.translator);
+
+ SharedPreferences prefs = getSharedPreferences(packageName + "_preferences", MODE_PRIVATE);
+
+ final String changes = prefs.getString("changelog", null);
+
+ if (changes == null) {
+ changelogView.setVisibility(View.GONE);
+ } else {
+ changelogView.setOnClickListener(v1 -> new MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.changes)
+ .setMessage(Html.fromHtml(changes))
+ .setPositiveButton(android.R.string.ok, null).show());
+ }
+
+ try {
+ String version = getPackageManager().getPackageInfo(packageName, 0).versionName;
+ ((TextView) findViewById(R.id.app_version)).setText(version);
+ } catch (PackageManager.NameNotFoundException ignored) {
+ }
+
+ licensesView.setOnClickListener(v12 -> createLicenseDialog());
+
+ txtModuleSupport.setText(getString(R.string.support_modules_description,
+ getString(R.string.module_support)));
+
+ setupView(installerSupportView, R.string.support_material_xda);
+ setupView(faqView, R.string.support_faq_url);
+ setupView(tgGroupView, R.string.group_telegram_link);
+ setupView(qqGroupView, R.string.group_qq_link);
+ setupView(donateView, R.string.support_donate_url);
+ setupView(sourceCodeView, R.string.about_source);
+ setupView(tgChannelView, R.string.group_telegram_channel_link);
+
+ if (translator.isEmpty()) {
+ translatorsView.setVisibility(View.GONE);
+ }
+ }
+
+ void setupView(View v, final int url) {
+ v.setOnClickListener(v1 -> NavUtil.startURL(this, getString(url)));
+ }
+
+ private void createLicenseDialog() {
+ Notices notices = new Notices();
+ notices.addNotice(new Notice("material-dialogs", "https://github.com/afollestad/material-dialogs", "Copyright (c) 2014-2016 Aidan Michael Follestad", new MITLicense()));
+ notices.addNotice(new Notice("StickyListHeaders", "https://github.com/emilsjolander/StickyListHeaders", "Emil Sjölander", new ApacheSoftwareLicense20()));
+ notices.addNotice(new Notice("PreferenceFragment-Compat", "https://github.com/Machinarius/PreferenceFragment-Compat", "machinarius", new ApacheSoftwareLicense20()));
+ notices.addNotice(new Notice("libsuperuser", "https://github.com/Chainfire/libsuperuser", "Copyright (C) 2012-2015 Jorrit \"Chainfire\" Jongma", new ApacheSoftwareLicense20()));
+ notices.addNotice(new Notice("picasso", "https://github.com/square/picasso", "Copyright 2013 Square, Inc.", new ApacheSoftwareLicense20()));
+
+ new LicensesDialog.Builder(this)
+ .setNotices(notices)
+ .setIncludeOwnLicense(true)
+ .build()
+ .show();
+ }
+
+ public void openLink(View view) {
+ NavUtil.startURL(this, view.getTag().toString());
+ }
+
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/BaseActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/BaseActivity.java
new file mode 100644
index 00000000..22a41e33
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/BaseActivity.java
@@ -0,0 +1,267 @@
+package org.meowcat.edxposed.manager;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatDelegate;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.topjohnwu.superuser.Shell;
+
+import org.meowcat.edxposed.manager.util.NavUtil;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+
+@SuppressLint("Registered")
+public class BaseActivity extends AppCompatActivity {
+
+ private static final String THEME_DEFAULT = "DEFAULT";
+ private static final String THEME_BLACK = "BLACK";
+ private String mTheme;
+
+ public static boolean isBlackNightTheme() {
+ return XposedApp.getPreferences().getBoolean("black_dark_theme", false);
+ }
+
+ public static String getTheme(Context context) {
+ if (isBlackNightTheme()
+ && isNightMode(context.getResources().getConfiguration()))
+ return THEME_BLACK;
+
+ return THEME_DEFAULT;
+ }
+
+ @StyleRes
+ public static int getThemeStyleRes(Context context) {
+ switch (getTheme(context)) {
+ case THEME_BLACK:
+ return R.style.ThemeOverlay_Black;
+ case THEME_DEFAULT:
+ default:
+ return R.style.ThemeOverlay;
+ }
+ }
+
+ public static boolean isNightMode(Configuration configuration) {
+ return (configuration.uiMode & Configuration.UI_MODE_NIGHT_YES) > 0;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ AppCompatDelegate.setDefaultNightMode(XposedApp.getPreferences().getInt("theme", 0));
+ mTheme = getTheme(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (!Objects.equals(mTheme, getTheme(this))) {
+ recreate();
+ }
+ }
+
+ @Override
+ protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
+ // apply real style and our custom style
+ if (getParent() == null) {
+ theme.applyStyle(resid, true);
+ } else {
+ try {
+ theme.setTo(getParent().getTheme());
+ } catch (Exception e) {
+ // Empty
+ }
+ theme.applyStyle(resid, false);
+ }
+ theme.applyStyle(getThemeStyleRes(this), true);
+ // only pass theme style to super, so styled theme will not be overwritten
+ super.onApplyThemeResource(theme, R.style.ThemeOverlay, first);
+ }
+
+ private void areYouSure(int contentTextId, DialogInterface.OnClickListener listener) {
+ new MaterialAlertDialogBuilder(this).setTitle(R.string.areyousure)
+ .setMessage(contentTextId)
+ .setPositiveButton(android.R.string.yes, listener)
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+ }
+
+ void softReboot() {
+ if (startShell())
+ return;
+
+ List messages = new LinkedList<>();
+ Shell.Result result = Shell.su("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote").exec();
+ if (result.getCode() != 0) {
+ messages.add(result.getOut().toString());
+ messages.add("");
+ messages.add(getString(R.string.reboot_failed));
+ showAlert(TextUtils.join("\n", messages).trim());
+ }
+ }
+
+ private boolean startShell() {
+ if (Shell.rootAccess())
+ return false;
+
+ showAlert(getString(R.string.root_failed));
+ return true;
+ }
+
+ void showAlert(final String result) {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ runOnUiThread(() -> showAlert(result));
+ return;
+ }
+
+ AlertDialog dialog = new MaterialAlertDialogBuilder(this).setMessage(result).setPositiveButton(android.R.string.ok, null).create();
+ dialog.show();
+
+ TextView txtMessage = dialog
+ .findViewById(android.R.id.message);
+ try {
+ txtMessage.setTextSize(14);
+ } catch (NullPointerException ignored) {
+ }
+ }
+
+ void reboot(String mode) {
+ if (startShell())
+ return;
+
+ List messages = new LinkedList<>();
+
+ String command = "/system/bin/svc power reboot";
+ if (mode != null) {
+ command += " " + mode;
+ if (mode.equals("recovery"))
+ // create a flag used by some kernels to boot into recovery
+ Shell.su("touch /cache/recovery/boot").exec();
+ }
+ Shell.Result result = Shell.su(command).exec();
+ if (result.getCode() != 0) {
+ messages.add(result.getOut().toString());
+ messages.add("");
+ messages.add(getString(R.string.reboot_failed));
+ showAlert(TextUtils.join("\n", messages).trim());
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.dexopt_all:
+ areYouSure(R.string.take_while_cannot_resore, (dialog, which) -> {
+ new MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.dexopt_now)
+ .setMessage(R.string.this_may_take_a_while)
+ .setCancelable(false)
+ .show();
+ new Thread("dexopt") {
+ @Override
+ public void run() {
+ if (!Shell.rootAccess()) {
+ dialog.dismiss();
+ NavUtil.showMessage(BaseActivity.this, getString(R.string.root_failed));
+ return;
+ }
+ Shell.su("cmd package bg-dexopt-job").exec();
+
+ dialog.dismiss();
+ XposedApp.runOnUiThread(() -> Toast.makeText(BaseActivity.this, R.string.done, Toast.LENGTH_LONG).show());
+ }
+ }.start();
+ }
+ );
+
+ break;
+ case R.id.speed_all:
+ areYouSure(R.string.take_while_cannot_resore, (dialog, which) -> {
+
+ new MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.dexopt_now)
+ .setMessage(R.string.this_may_take_a_while)
+ .setCancelable(false)
+ .show();
+ new Thread("dex2oat") {
+ @Override
+ public void run() {
+ if (!Shell.rootAccess()) {
+ dialog.dismiss();
+ NavUtil.showMessage(BaseActivity.this, getString(R.string.root_failed));
+ return;
+ }
+
+ Shell.su("cmd package compile -m speed -a").exec();
+
+ dialog.dismiss();
+ XposedApp.runOnUiThread(() -> Toast.makeText(BaseActivity.this, R.string.done, Toast.LENGTH_LONG).show());
+ }
+
+ };
+ });
+ break;
+ case R.id.reboot:
+ if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) {
+ areYouSure(R.string.reboot, (dialog, which) -> reboot(null));
+ } else {
+ reboot(null);
+ }
+ break;
+ case R.id.soft_reboot:
+ if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) {
+ areYouSure(R.string.soft_reboot, (dialog, which) -> softReboot());
+ } else {
+ softReboot();
+ }
+ break;
+ case R.id.reboot_recovery:
+ if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) {
+ areYouSure(R.string.reboot_recovery, (dialog, which) -> reboot("recovery"));
+ } else {
+ reboot("recovery");
+ }
+ break;
+ case R.id.reboot_bootloader:
+ if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) {
+ areYouSure(R.string.reboot_bootloader, (dialog, which) -> reboot("bootloader"));
+ } else {
+ reboot("bootloader");
+ }
+ break;
+ case R.id.reboot_download:
+ if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) {
+ areYouSure(R.string.reboot_download, (dialog, which) -> reboot("download"));
+ } else {
+ reboot("download");
+ }
+ break;
+ case R.id.reboot_edl:
+ if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) {
+ areYouSure(R.string.reboot_download, (dialog, which) -> reboot("edl"));
+ } else {
+ reboot("edl");
+ }
+ break;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/BaseAdvancedInstaller.java b/app/src/main/java/org/meowcat/edxposed/manager/BaseAdvancedInstaller.java
new file mode 100644
index 00000000..54ab3e99
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/BaseAdvancedInstaller.java
@@ -0,0 +1,241 @@
+package org.meowcat.edxposed.manager;
+
+import android.Manifest;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.ActivityCompat;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import org.meowcat.edxposed.manager.util.NavUtil;
+import org.meowcat.edxposed.manager.util.json.XposedTab;
+import org.meowcat.edxposed.manager.util.json.XposedZip;
+
+import java.util.List;
+import java.util.Objects;
+
+import static org.meowcat.edxposed.manager.XposedApp.WRITE_EXTERNAL_PERMISSION;
+
+public class BaseAdvancedInstaller extends Fragment {
+
+ // private static final String JAR_PATH = XposedApp.BASE_DIR + "bin/XposedBridge.jar";
+// private static final int INSTALL_MODE_NORMAL = 0;
+// private static final int INSTALL_MODE_RECOVERY_AUTO = 1;
+// private static final int INSTALL_MODE_RECOVERY_MANUAL = 2;
+// private static String APP_PROCESS_NAME = null;
+ //private List messages = new ArrayList<>();
+ private View mClickedButton;
+
+ static BaseAdvancedInstaller newInstance(XposedTab tab) {
+ BaseAdvancedInstaller myFragment = new BaseAdvancedInstaller();
+
+ Bundle args = new Bundle();
+ args.putParcelable("tab", tab);
+ myFragment.setArguments(args);
+
+ return myFragment;
+ }
+
+ private List installers() {
+ XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
+ return Objects.requireNonNull(tab).installers;
+ }
+
+ private List uninstallers() {
+ XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
+ return Objects.requireNonNull(tab).uninstallers;
+ }
+
+ private String notice() {
+ XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
+ return Objects.requireNonNull(tab).notice;
+ }
+
+// private String compatibility() {
+// XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
+// return Objects.requireNonNull(tab).getCompatibility();
+// }
+
+// private String incompatibility() {
+// XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
+// return Objects.requireNonNull(tab).getIncompatibility();
+// }
+
+ protected String author() {
+ XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
+ return Objects.requireNonNull(tab).author;
+ }
+
+ private String supportUrl() {
+ XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
+ return Objects.requireNonNull(tab).support;
+ }
+
+ protected boolean isStable() {
+ XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
+ return Objects.requireNonNull(tab).stable;
+ }
+
+ private boolean isOfficial() {
+ XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
+ return Objects.requireNonNull(tab).official;
+ }
+
+ private String description() {
+ XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
+ return Objects.requireNonNull(tab).description;
+ }
+
+ private boolean checkPermissions() {
+ if (Build.VERSION.SDK_INT < 23) return false;
+
+ if (ActivityCompat.checkSelfPermission(Objects.requireNonNull(getActivity()), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.single_installer_view, container, false);
+
+ final Spinner chooserInstallers = view.findViewById(R.id.chooserInstallers);
+ final Spinner chooserUninstallers = view.findViewById(R.id.chooserUninstallers);
+ final Button btnInstall = view.findViewById(R.id.btnInstall);
+ final Button btnUninstall = view.findViewById(R.id.btnUninstall);
+ ImageView infoInstaller = view.findViewById(R.id.infoInstaller);
+ ImageView infoUninstaller = view.findViewById(R.id.infoUninstaller);
+ TextView noticeTv = view.findViewById(R.id.noticeTv);
+ TextView author = view.findViewById(R.id.author);
+ View showOnXda = view.findViewById(R.id.show_on_xda);
+ View updateDescription = view.findViewById(R.id.updateDescription);
+
+ try {
+ chooserInstallers.setAdapter(new XposedZip.MyAdapter(getContext(), installers()));
+ chooserUninstallers.setAdapter(new XposedZip.MyAdapter(getContext(), uninstallers()));
+ } catch (Exception ignored) {
+ }
+ infoInstaller.setOnClickListener(v -> {
+ XposedZip selectedInstaller = (XposedZip) chooserInstallers.getSelectedItem();
+ String s = getString(R.string.infoInstaller,
+ selectedInstaller.name,
+ selectedInstaller.version);
+
+ new MaterialAlertDialogBuilder(Objects.requireNonNull(getContext())).setTitle(R.string.info)
+ .setMessage(s).setPositiveButton(android.R.string.ok, null).show();
+ });
+ infoUninstaller.setOnClickListener(v -> {
+ XposedZip selectedUninstaller = (XposedZip) chooserUninstallers.getSelectedItem();
+ String s = getString(R.string.infoUninstaller,
+ selectedUninstaller.name,
+ selectedUninstaller.version);
+
+ new MaterialAlertDialogBuilder(Objects.requireNonNull(getContext())).setTitle(R.string.info)
+ .setMessage(s).setPositiveButton(android.R.string.ok, null).show();
+ });
+
+ btnInstall.setOnClickListener(v -> {
+ mClickedButton = v;
+ if (checkPermissions()) return;
+
+ areYouSure(R.string.warningArchitecture,
+ (dialog, which) -> {
+ XposedZip selectedInstaller = (XposedZip) chooserInstallers.getSelectedItem();
+ Uri uri = Uri.parse(selectedInstaller.link);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ startActivity(intent);
+ });
+ });
+
+ btnUninstall.setOnClickListener(v -> {
+ mClickedButton = v;
+ if (checkPermissions()) return;
+
+ areYouSure(R.string.warningArchitecture,
+ (dialog, which) -> {
+ XposedZip selectedUninstaller = (XposedZip) chooserUninstallers.getSelectedItem();
+ Uri uri = Uri.parse(selectedUninstaller.link);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ startActivity(intent);
+ });
+ });
+
+ noticeTv.setText(Html.fromHtml(notice()));
+ author.setText(getString(R.string.download_author, author()));
+
+ try {
+ if (uninstallers().size() == 0) {
+ infoUninstaller.setVisibility(View.GONE);
+ chooserUninstallers.setVisibility(View.GONE);
+ btnUninstall.setVisibility(View.GONE);
+ }
+ } catch (Exception ignored) {
+ }
+
+ if (!isStable()) {
+ view.findViewById(R.id.warning_unstable).setVisibility(View.VISIBLE);
+ }
+
+ if (!isOfficial()) {
+ view.findViewById(R.id.warning_unofficial).setVisibility(View.VISIBLE);
+ }
+
+ showOnXda.setOnClickListener(v -> NavUtil.startURL(getActivity(), supportUrl()));
+ updateDescription.setOnClickListener(v -> new MaterialAlertDialogBuilder(Objects.requireNonNull(getContext()))
+ .setTitle(R.string.changes)
+ .setMessage(Html.fromHtml(description()))
+ .setPositiveButton(android.R.string.ok, null).show());
+
+ return view;
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == WRITE_EXTERNAL_PERMISSION) {
+ if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (mClickedButton != null) {
+ new Handler().postDelayed(() -> mClickedButton.performClick(), 500);
+ }
+ } else {
+ Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+
+ @SuppressWarnings("SameParameterValue")
+ private void areYouSure(int contentTextId, DialogInterface.OnClickListener listener) {
+ new MaterialAlertDialogBuilder(Objects.requireNonNull(getActivity())).setTitle(R.string.areyousure)
+ .setMessage(contentTextId)
+ .setPositiveButton(android.R.string.yes, listener)
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/BlackListActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/BlackListActivity.java
new file mode 100644
index 00000000..a53e3072
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/BlackListActivity.java
@@ -0,0 +1,122 @@
+package org.meowcat.edxposed.manager;
+
+import android.content.pm.ApplicationInfo;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.widget.SearchView;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import org.meowcat.edxposed.manager.adapters.AppAdapter;
+import org.meowcat.edxposed.manager.adapters.AppHelper;
+import org.meowcat.edxposed.manager.adapters.BlackListAdapter;
+
+public class BlackListActivity extends BaseActivity implements AppAdapter.Callback {
+ private SwipeRefreshLayout mSwipeRefreshLayout;
+ private SearchView mSearchView;
+ private BlackListAdapter mAppAdapter;
+
+ private SearchView.OnQueryTextListener mSearchListener;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_black_list);
+ setSupportActionBar(findViewById(R.id.toolbar));
+ ActionBar bar = getSupportActionBar();
+ if (bar != null) {
+ bar.setDisplayHomeAsUpEnabled(true);
+ }
+ mSwipeRefreshLayout = findViewById(R.id.swipeRefreshLayout);
+ RecyclerView mRecyclerView = findViewById(R.id.recyclerView);
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+ final boolean isWhiteListMode = isWhiteListMode();
+ mAppAdapter = new BlackListAdapter(this, isWhiteListMode);
+ mRecyclerView.setAdapter(mAppAdapter);
+ mAppAdapter.setCallback(this);
+ DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mRecyclerView.getContext(),
+ DividerItemDecoration.VERTICAL);
+ mRecyclerView.addItemDecoration(dividerItemDecoration);
+ mSwipeRefreshLayout.setRefreshing(true);
+ mSwipeRefreshLayout.setOnRefreshListener(mAppAdapter::refresh);
+ mSearchListener = new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ mAppAdapter.filter(query);
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ mAppAdapter.filter(newText);
+ return false;
+ }
+ };
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(@NonNull Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_app_list, menu);
+ mSearchView = (SearchView) menu.findItem(R.id.app_search).getActionView();
+ mSearchView.setOnQueryTextListener(mSearchListener);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ changeTitle(isBlackListMode(), isWhiteListMode());
+ }
+
+
+ private void changeTitle(boolean isBlackListMode, boolean isWhiteListMode) {
+ if (isBlackListMode) {
+ setTitle(isWhiteListMode ? R.string.title_white_list : R.string.title_black_list);
+ } else {
+ setTitle(R.string.nav_title_black_list);
+ }
+ }
+
+ private boolean isWhiteListMode() {
+ return AppHelper.isWhiteListMode();
+ }
+
+ private boolean isBlackListMode() {
+ return AppHelper.isBlackListMode();
+ }
+
+ @Override
+ public void onDataReady() {
+ mSwipeRefreshLayout.setRefreshing(false);
+ String queryStr = mSearchView != null ? mSearchView.getQuery().toString() : "";
+ mAppAdapter.filter(queryStr);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onItemClick(View v, ApplicationInfo info) {
+ getSupportFragmentManager();
+ AppHelper.showMenu(this, getSupportFragmentManager(), v, info);
+ }
+
+ @Override
+ public void onPointerCaptureChanged(boolean hasCapture) {
+
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mSearchView.isIconified()) {
+ super.onBackPressed();
+ } else {
+ mSearchView.setIconified(true);
+ }
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/CompileDialogFragment.java b/app/src/main/java/org/meowcat/edxposed/manager/CompileDialogFragment.java
new file mode 100644
index 00000000..35087a39
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/CompileDialogFragment.java
@@ -0,0 +1,126 @@
+package org.meowcat.edxposed.manager;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatDialogFragment;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.topjohnwu.superuser.Shell;
+
+import org.meowcat.edxposed.manager.util.ToastUtil;
+
+import java.lang.ref.WeakReference;
+
+public class CompileDialogFragment extends AppCompatDialogFragment {
+
+ private static final String KEY_APP_INFO = "app_info";
+ private static final String KEY_MSG = "msg";
+ private static final String KEY_COMMANDS = "commands";
+ private ApplicationInfo appInfo;
+
+
+ public CompileDialogFragment() {
+ }
+
+ public static CompileDialogFragment newInstance(ApplicationInfo appInfo,
+ String msg, String[] commands) {
+ Bundle arguments = new Bundle();
+ arguments.putParcelable(KEY_APP_INFO, appInfo);
+ arguments.putString(KEY_MSG, msg);
+ arguments.putStringArray(KEY_COMMANDS, commands);
+ CompileDialogFragment fragment = new CompileDialogFragment();
+ fragment.setArguments(arguments);
+ fragment.setCancelable(false);
+ return fragment;
+ }
+
+ @Override
+ @NonNull
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Bundle arguments = getArguments();
+ if (arguments == null) {
+ throw new IllegalStateException("arguments should not be null.");
+ }
+ appInfo = arguments.getParcelable(KEY_APP_INFO);
+ if (appInfo == null) {
+ throw new IllegalStateException("appInfo should not be null.");
+ }
+ String msg = arguments.getString(KEY_MSG, getString(R.string.compile_speed_msg));
+ final PackageManager pm = requireContext().getPackageManager();
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
+ .setIcon(appInfo.loadIcon(pm))
+ .setTitle(appInfo.loadLabel(pm))
+ .setCancelable(false);
+ @SuppressLint("InflateParams") View customView = LayoutInflater.from(requireContext()).inflate(R.layout.fragment_compile_dialog, null);
+ builder.setView(customView);
+ TextView msgView = customView.findViewById(R.id.message);
+ //ProgressBar progressView = customView.findViewById(R.id.progress);
+ msgView.setText(msg);
+ AlertDialog alertDialog = builder.create();
+ alertDialog.setCanceledOnTouchOutside(false);
+ return alertDialog;
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ Bundle arguments = getArguments();
+ if (arguments != null) {
+ String[] commandPrefixes = arguments.getStringArray(KEY_COMMANDS);
+ appInfo = arguments.getParcelable(KEY_APP_INFO);
+ if (commandPrefixes == null || commandPrefixes.length == 0 || appInfo == null) {
+ ToastUtil.showShortToast(context, R.string.empty_param);
+ dismissAllowingStateLoss();
+ return;
+ }
+ String[] commands = new String[commandPrefixes.length];
+ for (int i = 0; i < commandPrefixes.length; i++) {
+ commands[i] = commandPrefixes[i] + appInfo.packageName;
+ }
+ new CompileTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, commands);
+ } else {
+ dismissAllowingStateLoss();
+ }
+ }
+
+ private static class CompileTask extends AsyncTask {
+
+ WeakReference outerRef;
+
+ CompileTask(CompileDialogFragment fragment) {
+ outerRef = new WeakReference<>(fragment);
+ }
+
+ @Override
+ protected String doInBackground(String... commands) {
+ if (outerRef.get() == null) {
+ return outerRef.get().requireContext().getString(R.string.compile_failed);
+ }
+ return Shell.su(commands).exec().getOut().toString();
+ }
+
+ @Override
+ protected void onPostExecute(String result) {
+ if (outerRef.get() == null || !outerRef.get().isAdded()) {
+ return;
+ }
+ if ("".equals(result.substring(1, result.length() - 1))) {
+ ToastUtil.showLongToast(outerRef.get().requireContext(), R.string.compile_failed);
+ } else {
+ ToastUtil.showLongToast(outerRef.get().requireContext(), R.string.done);
+ }
+ outerRef.get().dismissAllowingStateLoss();
+ }
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/DownloadActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/DownloadActivity.java
new file mode 100644
index 00000000..170fdb4d
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/DownloadActivity.java
@@ -0,0 +1,439 @@
+package org.meowcat.edxposed.manager;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.widget.SearchView;
+import androidx.core.view.MenuItemCompat;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersAdapter;
+import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersDecoration;
+
+import org.meowcat.edxposed.manager.adapters.CursorRecyclerViewAdapter;
+import org.meowcat.edxposed.manager.repo.RepoDb;
+import org.meowcat.edxposed.manager.repo.RepoDbDefinitions;
+import org.meowcat.edxposed.manager.util.ModuleUtil;
+import org.meowcat.edxposed.manager.util.RepoLoader;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+public class DownloadActivity extends BaseActivity implements RepoLoader.RepoListener, ModuleUtil.ModuleListener, SharedPreferences.OnSharedPreferenceChangeListener {
+ private SharedPreferences mPref;
+ private DownloadsAdapter mAdapter;
+ private String mFilterText;
+ private RepoLoader mRepoLoader;
+ private ModuleUtil mModuleUtil;
+ private int mSortingOrder;
+ private SearchView mSearchView;
+ private SharedPreferences mIgnoredUpdatesPref;
+ private boolean changed = false;
+ private BroadcastReceiver connectionListener = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+
+ if (mRepoLoader != null) {
+ /*if (networkInfo == null) {
+ ((TextView) backgroundList.findViewById(R.id.list_status)).setText(R.string.no_connection_available);
+ backgroundList.findViewById(R.id.progress).setVisibility(View.GONE);
+ } else {
+ ((TextView) backgroundList.findViewById(R.id.list_status)).setText(R.string.update_download_list);
+ backgroundList.findViewById(R.id.progress).setVisibility(View.VISIBLE);
+ }
+*/
+ mRepoLoader.triggerReload(true);
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_download);
+ setSupportActionBar(findViewById(R.id.toolbar));
+ ActionBar bar = getSupportActionBar();
+ if (bar != null) {
+ bar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ mPref = XposedApp.getPreferences();
+ mRepoLoader = RepoLoader.getInstance();
+ mModuleUtil = ModuleUtil.getInstance();
+ mAdapter = new DownloadsAdapter(this, RepoDb.queryModuleOverview(mSortingOrder, mFilterText));
+ /*mAdapter.setFilterQueryProvider(new FilterQueryProvider() {
+ @Override
+ public Cursor runQuery(CharSequence constraint) {
+ return RepoDb.queryModuleOverview(mSortingOrder, constraint);
+ }
+ });*/
+
+ mSortingOrder = mPref.getInt("download_sorting_order",
+ RepoDb.SORT_STATUS);
+
+ mIgnoredUpdatesPref = getSharedPreferences("update_ignored", MODE_PRIVATE);
+ RecyclerView mListView = findViewById(R.id.recyclerView);
+ if (Build.VERSION.SDK_INT >= 26) {
+ mListView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
+ }
+ final SwipeRefreshLayout refreshLayout = findViewById(R.id.swipeRefreshLayout);
+ refreshLayout.setOnRefreshListener(() -> {
+ mRepoLoader.setSwipeRefreshLayout(refreshLayout);
+ mRepoLoader.triggerReload(true);
+ });
+ mRepoLoader.addListener(this, true);
+ mModuleUtil.addListener(this);
+ mListView.setAdapter(mAdapter);
+
+ mListView.setLayoutManager(new LinearLayoutManager(this));
+ mListView.addItemDecoration(new StickyRecyclerHeadersDecoration(mAdapter));
+ DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mListView.getContext(),
+ DividerItemDecoration.VERTICAL);
+ mListView.addItemDecoration(dividerItemDecoration);
+ /*mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (mListView.getChildAt(0) != null) {
+ refreshLayout.setEnabled(mListView.getFirstVisiblePosition() == 0 && mListView.getChildAt(0).getTop() == 0);
+ }
+ }
+ });*/
+
+ /*mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ Cursor cursor = (Cursor) mAdapter.getItem(position);
+ String packageName = cursor.getString(OverviewColumnsIndexes.PKGNAME);
+
+ Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class);
+ detailsIntent.setData(Uri.fromParts("package", packageName, null));
+ startActivity(detailsIntent);
+ }
+ });*/
+ mListView.setOnKeyListener(new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // Expand the search view when the SEARCH key is triggered
+ if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getAction() == KeyEvent.ACTION_UP && (event.getFlags() & KeyEvent.FLAG_CANCELED) == 0) {
+ if (mSearchView != null)
+ mSearchView.setIconified(false);
+ return true;
+ }
+ return false;
+ }
+ });
+
+ }
+
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ mIgnoredUpdatesPref.registerOnSharedPreferenceChangeListener(this);
+ if (changed) {
+ reloadItems();
+ changed = !changed;
+ }
+
+ registerReceiver(connectionListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ unregisterReceiver(connectionListener);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ mRepoLoader.removeListener(this);
+ mModuleUtil.removeListener(this);
+ mIgnoredUpdatesPref.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_download, menu);
+
+ // Setup search button
+ final MenuItem searchItem = menu.findItem(R.id.menu_search);
+ mSearchView = (SearchView) searchItem.getActionView();
+ mSearchView.setIconifiedByDefault(true);
+ mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ setFilter(query);
+ mSearchView.clearFocus();
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ setFilter(newText);
+ return true;
+ }
+ });
+ MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ setFilter(null);
+ return true; // Return true to collapse action view
+ }
+
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ return true; // Return true to expand action view
+ }
+ });
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ private void setFilter(String filterText) {
+ mFilterText = filterText;
+ reloadItems();
+ }
+
+ private void reloadItems() {
+ mAdapter.swapCursor(RepoDb.queryModuleOverview(mSortingOrder, mFilterText));
+ mAdapter.notifyDataSetChanged();
+ //mAdapter.getFilter().filter(mFilterText);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_sort:
+ new MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.download_sorting_title)
+ .setSingleChoiceItems(R.array.download_sort_order, mSortingOrder, (dialog, which) -> {
+ mSortingOrder = which;
+ mPref.edit().putInt("download_sorting_order", mSortingOrder).apply();
+ reloadItems();
+ dialog.dismiss();
+ })
+ .show();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onRepoReloaded(final RepoLoader loader) {
+ reloadItems();
+ }
+
+ @Override
+ public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, ModuleUtil.InstalledModule module) {
+ reloadItems();
+ }
+
+ @Override
+ public void onInstalledModulesReloaded(ModuleUtil moduleUtil) {
+ reloadItems();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ changed = true;
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mSearchView.isIconified()) {
+ super.onBackPressed();
+ } else {
+ mSearchView.setIconified(true);
+ }
+ }
+
+ private class DownloadsAdapter extends CursorRecyclerViewAdapter implements StickyRecyclerHeadersAdapter {
+ private final Context mContext;
+ private final DateFormat mDateFormatter = DateFormat.getDateInstance(DateFormat.SHORT);
+ private final SharedPreferences mPrefs;
+ private String[] sectionHeadersStatus;
+ private String[] sectionHeadersDate;
+
+ DownloadsAdapter(Context context, Cursor cursor) {
+ super(context, cursor);
+ mContext = context;
+ mPrefs = context.getSharedPreferences("update_ignored", MODE_PRIVATE);
+
+ Resources res = context.getResources();
+ sectionHeadersStatus = new String[]{
+ res.getString(R.string.download_section_framework),
+ res.getString(R.string.download_section_update_available),
+ res.getString(R.string.download_section_installed),
+ res.getString(R.string.download_section_not_installed),};
+ sectionHeadersDate = new String[]{
+ res.getString(R.string.download_section_24h),
+ res.getString(R.string.download_section_7d),
+ res.getString(R.string.download_section_30d),
+ res.getString(R.string.download_section_older)};
+ }
+
+ @Override
+ public long getHeaderId(int position) {
+ Cursor cursor = getCursor();
+ cursor.moveToPosition(position);
+ long created = cursor.getLong(RepoDbDefinitions.OverviewColumnsIndexes.CREATED);
+ long updated = cursor.getLong(RepoDbDefinitions.OverviewColumnsIndexes.UPDATED);
+ boolean isFramework = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.IS_FRAMEWORK) > 0;
+ boolean isInstalled = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.IS_INSTALLED) > 0;
+ boolean updateIgnored = mPrefs.getBoolean(cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.PKGNAME), false);
+ boolean updateIgnorePreference = XposedApp.getPreferences().getBoolean("ignore_updates", false);
+ boolean hasUpdate = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.HAS_UPDATE) > 0;
+
+ if (hasUpdate && updateIgnored && updateIgnorePreference) {
+ hasUpdate = false;
+ }
+
+ if (mSortingOrder != RepoDb.SORT_STATUS) {
+ long timestamp = (mSortingOrder == RepoDb.SORT_UPDATED) ? updated : created;
+ long age = System.currentTimeMillis() - timestamp;
+ final long mSecsPerDay = 24 * 60 * 60 * 1000L;
+ if (age < mSecsPerDay)
+ return 0;
+ if (age < 7 * mSecsPerDay)
+ return 1;
+ if (age < 30 * mSecsPerDay)
+ return 2;
+ return 3;
+ } else {
+ if (isFramework)
+ return 0;
+
+ if (hasUpdate)
+ return 1;
+ else if (isInstalled)
+ return 2;
+ else
+ return 3;
+ }
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.sticky_header_download, parent, false);
+ return new RecyclerView.ViewHolder(view) {
+ };
+ }
+
+ @Override
+ public void onBindHeaderViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+ long section = getHeaderId(position);
+ TextView tv = viewHolder.itemView.findViewById(android.R.id.title);
+ tv.setText(mSortingOrder == RepoDb.SORT_STATUS
+ ? sectionHeadersStatus[(int) section]
+ : sectionHeadersDate[(int) section]);
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_download, parent, false);
+ return new ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, Cursor cursor) {
+ String title = cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.TITLE);
+ String summary = cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.SUMMARY);
+ String installedVersion = cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.INSTALLED_VERSION);
+ String latestVersion = cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.LATEST_VERSION);
+ long created = cursor.getLong(RepoDbDefinitions.OverviewColumnsIndexes.CREATED);
+ long updated = cursor.getLong(RepoDbDefinitions.OverviewColumnsIndexes.UPDATED);
+ boolean isInstalled = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.IS_INSTALLED) > 0;
+ boolean updateIgnored = mPrefs.getBoolean(cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.PKGNAME), false);
+ boolean updateIgnorePreference = XposedApp.getPreferences().getBoolean("ignore_updates", false);
+ boolean hasUpdate = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.HAS_UPDATE) > 0;
+
+ if (hasUpdate && updateIgnored && updateIgnorePreference) {
+ hasUpdate = false;
+ }
+
+ TextView txtTitle = holder.appName;
+ txtTitle.setText(title);
+
+ TextView txtSummary = holder.appDescription;
+ txtSummary.setText(summary);
+
+ TextView txtStatus = holder.downloadStatus;
+ if (hasUpdate) {
+ txtStatus.setText(mContext.getString(
+ R.string.download_status_update_available,
+ installedVersion, latestVersion));
+ txtStatus.setTextColor(getResources().getColor(R.color.download_status_update_available));
+ txtStatus.setVisibility(View.VISIBLE);
+ } else if (isInstalled) {
+ txtStatus.setText(mContext.getString(
+ R.string.download_status_installed, installedVersion));
+ //txtStatus.setTextColor(ThemeUtil.getThemeColor(mContext, R.attr.download_status_installed));
+ txtStatus.setVisibility(View.VISIBLE);
+ } else {
+ txtStatus.setVisibility(View.GONE);
+ }
+
+ String creationDate = mDateFormatter.format(new Date(created));
+ String updateDate = mDateFormatter.format(new Date(updated));
+ holder.timestamps.setText(getString(R.string.download_timestamps, creationDate, updateDate));
+ String packageName = cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.PKGNAME);
+ holder.itemView.setOnClickListener(v -> {
+ Intent detailsIntent = new Intent(DownloadActivity.this, DownloadDetailsActivity.class);
+ detailsIntent.setData(Uri.fromParts("package", packageName, null));
+ startActivity(detailsIntent);
+ });
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+ TextView appName;
+ TextView appDescription;
+ TextView downloadStatus;
+ TextView timestamps;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ appName = itemView.findViewById(R.id.title);
+ appDescription = itemView.findViewById(R.id.description);
+ downloadStatus = itemView.findViewById(R.id.downloadStatus);
+ timestamps = itemView.findViewById(R.id.timestamps);
+ }
+ }
+ }
+}
+
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsActivity.java
new file mode 100644
index 00000000..213e8546
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsActivity.java
@@ -0,0 +1,298 @@
+package org.meowcat.edxposed.manager;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
+import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.tabs.TabLayout;
+
+import org.meowcat.edxposed.manager.repo.Module;
+import org.meowcat.edxposed.manager.util.ModuleUtil;
+import org.meowcat.edxposed.manager.util.RepoLoader;
+
+import java.util.List;
+import java.util.Objects;
+
+public class DownloadDetailsActivity extends BaseActivity implements RepoLoader.RepoListener, ModuleUtil.ModuleListener {
+
+ public static final int DOWNLOAD_DESCRIPTION = 0;
+ public static final int DOWNLOAD_VERSIONS = 1;
+ public static final int DOWNLOAD_SETTINGS = 2;
+ static final String XPOSED_REPO_LINK = "http://repo.xposed.info/module/%s";
+ static final String PLAY_STORE_PACKAGE = "com.android.vending";
+ static final String PLAY_STORE_LINK = "https://play.google.com/store/apps/details?id=%s";
+ private static final String TAG = "DownloadDetailsActivity";
+ private static final String NOT_ACTIVE_NOTE_TAG = "NOT_ACTIVE_NOTE";
+ private static RepoLoader sRepoLoader = RepoLoader.getInstance();
+ private static ModuleUtil sModuleUtil = ModuleUtil.getInstance();
+ private ViewPager mPager;
+ private String mPackageName;
+ private Module mModule;
+ private ModuleUtil.InstalledModule mInstalledModule;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+
+ mPackageName = getModulePackageName();
+ try {
+ mModule = sRepoLoader.getModule(mPackageName);
+ } catch (Exception e) {
+ Log.i(TAG, "DownloadDetailsActivity -> " + e.getMessage());
+
+ mModule = null;
+ }
+
+ mInstalledModule = ModuleUtil.getInstance().getModule(mPackageName);
+
+ super.onCreate(savedInstanceState);
+ sRepoLoader.addListener(this, false);
+ sModuleUtil.addListener(this);
+
+ if (mModule != null) {
+ setContentView(R.layout.activity_download_details);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ toolbar.setNavigationOnClickListener(view -> finish());
+
+ ActionBar ab = getSupportActionBar();
+
+ if (ab != null) {
+ ab.setTitle(R.string.nav_item_download);
+ ab.setDisplayHomeAsUpEnabled(true);
+ }
+
+ setupTabs();
+
+ boolean directDownload = getIntent().getBooleanExtra("direct_download", false);
+ // Updates available => start on the versions page
+ if (mInstalledModule != null && mInstalledModule.isUpdate(sRepoLoader.getLatestVersion(mModule)) || directDownload)
+ mPager.setCurrentItem(DOWNLOAD_VERSIONS);
+
+ } else {
+ setContentView(R.layout.activity_download_details_not_found);
+
+ TextView txtMessage = findViewById(android.R.id.message);
+ txtMessage.setText(getResources().getString(R.string.download_details_not_found, mPackageName));
+
+ findViewById(R.id.reload).setOnClickListener(v -> {
+ v.setEnabled(false);
+ sRepoLoader.triggerReload(true);
+ });
+ }
+ }
+
+ private void setupTabs() {
+ mPager = findViewById(R.id.download_pager);
+ mPager.setAdapter(new SwipeFragmentPagerAdapter(getSupportFragmentManager()));
+ TabLayout mTabLayout = findViewById(R.id.sliding_tabs);
+ mTabLayout.setupWithViewPager(mPager);
+ mTabLayout.setBackgroundColor(XposedApp.getColor(this));
+ }
+
+ private String getModulePackageName() {
+ Uri uri = getIntent().getData();
+ if (uri == null)
+ return null;
+
+ String scheme = uri.getScheme();
+ if (TextUtils.isEmpty(scheme)) {
+ return null;
+ } else switch (Objects.requireNonNull(scheme)) {
+ case "xposed":
+ case "package":
+ return uri.getSchemeSpecificPart().replace("//", "");
+ case "http":
+ List segments = uri.getPathSegments();
+ if (segments.size() > 1)
+ return segments.get(1);
+ break;
+ }
+ return null;
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ sRepoLoader.removeListener(this);
+ sModuleUtil.removeListener(this);
+ }
+
+ public Module getModule() {
+ return mModule;
+ }
+
+ public ModuleUtil.InstalledModule getInstalledModule() {
+ return mInstalledModule;
+ }
+
+ public void gotoPage(int page) {
+ mPager.setCurrentItem(page);
+ }
+
+ private void reload() {
+ runOnUiThread(this::recreate);
+ }
+
+ @Override
+ public void onRepoReloaded(RepoLoader loader) {
+ reload();
+ }
+
+ @Override
+ public void onInstalledModulesReloaded(ModuleUtil moduleUtil) {
+ reload();
+ }
+
+ @Override
+ public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, ModuleUtil.InstalledModule module) {
+ if (packageName.equals(mPackageName))
+ reload();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.menu_download_details, menu);
+
+ boolean updateIgnorePreference = XposedApp.getPreferences().getBoolean("ignore_updates", false);
+ if (updateIgnorePreference) {
+ SharedPreferences prefs = getSharedPreferences("update_ignored", MODE_PRIVATE);
+
+ boolean ignored = prefs.getBoolean(mModule.packageName, false);
+ menu.findItem(R.id.ignoreUpdate).setChecked(ignored);
+ } else {
+ menu.removeItem(R.id.ignoreUpdate);
+ }
+ setupBookmark(false);
+ return true;
+ }
+
+ private void setupBookmark(boolean clicked) {
+ SharedPreferences myPref = getSharedPreferences("bookmarks", MODE_PRIVATE);
+
+ boolean saved = myPref.getBoolean(mModule.packageName, false);
+ boolean newValue;
+
+ if (clicked) {
+ newValue = !saved;
+ myPref.edit().putBoolean(mModule.packageName, newValue).apply();
+
+ int msg = newValue ? R.string.bookmark_added : R.string.bookmark_removed;
+
+ Snackbar.make(findViewById(android.R.id.content), msg, Snackbar.LENGTH_SHORT).show();
+ }
+
+ saved = myPref.getBoolean(mModule.packageName, false);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_refresh:
+ RepoLoader.getInstance().triggerReload(true);
+ return true;
+ case R.id.menu_share:
+ String text = mModule.name + " - ";
+
+ if (isPackageInstalled(mPackageName, this)) {
+ String s = getPackageManager().getInstallerPackageName(mPackageName);
+ boolean playStore;
+
+ try {
+ playStore = s.equals(PLAY_STORE_PACKAGE);
+ } catch (NullPointerException e) {
+ playStore = false;
+ }
+
+ if (playStore) {
+ text += String.format(PLAY_STORE_LINK, mPackageName);
+ } else {
+ text += String.format(XPOSED_REPO_LINK, mPackageName);
+ }
+ } else {
+ text += String.format(XPOSED_REPO_LINK,
+ mPackageName);
+ }
+
+ Intent sharingIntent = new Intent(Intent.ACTION_SEND);
+ sharingIntent.setType("text/plain");
+ sharingIntent.putExtra(Intent.EXTRA_TEXT, text);
+ startActivity(Intent.createChooser(sharingIntent, getString(R.string.share)));
+ return true;
+ case R.id.ignoreUpdate:
+ SharedPreferences prefs = getSharedPreferences("update_ignored", MODE_PRIVATE);
+
+ boolean ignored = prefs.getBoolean(mModule.packageName, false);
+ prefs.edit().putBoolean(mModule.packageName, !ignored).apply();
+ item.setChecked(!ignored);
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private boolean isPackageInstalled(String packagename, Context context) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ class SwipeFragmentPagerAdapter extends FragmentPagerAdapter {
+ final int PAGE_COUNT = 3;
+ private String[] tabTitles = new String[]{getString(R.string.download_details_page_description), getString(R.string.download_details_page_versions), getString(R.string.download_details_page_settings),};
+
+ SwipeFragmentPagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public int getCount() {
+ return PAGE_COUNT;
+ }
+
+ @NonNull
+ @Override
+ public Fragment getItem(int position) {
+ switch (position) {
+ case DOWNLOAD_DESCRIPTION:
+ return new DownloadDetailsFragment();
+ case DOWNLOAD_VERSIONS:
+ return new DownloadDetailsVersionsFragment();
+ case DOWNLOAD_SETTINGS:
+ return new DownloadDetailsSettingsFragment();
+ default:
+ //noinspection ConstantConditions
+ return null;
+ }
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ // Generate title based on item position
+ return tabTitles[position];
+ }
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsFragment.java b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsFragment.java
new file mode 100644
index 00000000..a4152b91
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsFragment.java
@@ -0,0 +1,86 @@
+package org.meowcat.edxposed.manager;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.fragment.app.Fragment;
+
+import org.meowcat.edxposed.manager.repo.Module;
+import org.meowcat.edxposed.manager.repo.RepoParser;
+import org.meowcat.edxposed.manager.util.NavUtil;
+import org.meowcat.edxposed.manager.util.chrome.LinkTransformationMethod;
+
+public class DownloadDetailsFragment extends Fragment {
+ private DownloadDetailsActivity mActivity;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mActivity = (DownloadDetailsActivity) activity;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final Module module = mActivity.getModule();
+ if (module == null)
+ return null;
+
+ final View view = inflater.inflate(R.layout.download_details, container, false);
+
+ TextView title = view.findViewById(R.id.download_title);
+ title.setText(module.name);
+ title.setTextIsSelectable(true);
+
+ TextView author = view.findViewById(R.id.download_author);
+ if (module.author != null && !module.author.isEmpty())
+ author.setText(getString(R.string.download_author, module.author));
+ else
+ author.setText(R.string.download_unknown_author);
+
+ TextView description = view.findViewById(R.id.download_description);
+ if (module.description != null) {
+ if (module.descriptionIsHtml) {
+ description.setText(RepoParser.parseSimpleHtml(getActivity(), module.description, description));
+ description.setTransformationMethod(new LinkTransformationMethod(getActivity()));
+ description.setMovementMethod(LinkMovementMethod.getInstance());
+ } else {
+ description.setText(module.description);
+ }
+ description.setTextIsSelectable(true);
+ } else {
+ description.setVisibility(View.GONE);
+ }
+
+ ViewGroup moreInfoContainer = view.findViewById(R.id.download_moreinfo_container);
+ for (Pair moreInfoEntry : module.moreInfo) {
+ View moreInfoView = inflater.inflate(R.layout.download_moreinfo, moreInfoContainer, false);
+ TextView txtTitle = moreInfoView.findViewById(android.R.id.title);
+ TextView txtValue = moreInfoView.findViewById(android.R.id.message);
+
+ txtTitle.setText(moreInfoEntry.first + ":");
+ txtValue.setText(moreInfoEntry.second);
+
+ final Uri link = NavUtil.parseURL(moreInfoEntry.second);
+ if (link != null) {
+ txtValue.setTextColor(txtValue.getLinkTextColors());
+ moreInfoView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ NavUtil.startURL(getActivity(), link);
+ }
+ });
+ }
+
+ moreInfoContainer.addView(moreInfoView);
+ }
+
+ return view;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsSettingsFragment.java b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsSettingsFragment.java
new file mode 100644
index 00000000..acfcfc4f
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsSettingsFragment.java
@@ -0,0 +1,60 @@
+package org.meowcat.edxposed.manager;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+import androidx.preference.PreferenceManager;
+
+import com.takisoft.preferencex.PreferenceFragmentCompat;
+
+import org.meowcat.edxposed.manager.repo.Module;
+import org.meowcat.edxposed.manager.util.PrefixedSharedPreferences;
+import org.meowcat.edxposed.manager.util.RepoLoader;
+
+import java.util.Map;
+
+public class DownloadDetailsSettingsFragment extends PreferenceFragmentCompat {
+ private DownloadDetailsActivity mActivity;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mActivity = (DownloadDetailsActivity) activity;
+ }
+
+ @Override
+ public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
+ final Module module = mActivity.getModule();
+ if (module == null)
+ return;
+
+ final String packageName = module.packageName;
+
+ PreferenceManager prefManager = getPreferenceManager();
+ prefManager.setSharedPreferencesName("module_settings");
+ PrefixedSharedPreferences.injectToPreferenceManager(prefManager, module.packageName);
+ addPreferencesFromResource(R.xml.module_prefs);
+
+ SharedPreferences prefs = getActivity().getSharedPreferences("module_settings", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+
+ if (prefs.getBoolean("no_global", true)) {
+ for (Map.Entry k : prefs.getAll().entrySet()) {
+ if (prefs.getString(k.getKey(), "").equals("global")) {
+ editor.putString(k.getKey(), "").apply();
+ }
+ }
+
+ editor.putBoolean("no_global", false).apply();
+ }
+
+ findPreference("release_type").setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ RepoLoader.getInstance().setReleaseTypeLocal(packageName, (String) newValue);
+ return true;
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsVersionsFragment.java b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsVersionsFragment.java
new file mode 100644
index 00000000..77602cdb
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsVersionsFragment.java
@@ -0,0 +1,261 @@
+package org.meowcat.edxposed.manager;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.ListFragment;
+
+import org.meowcat.edxposed.manager.repo.Module;
+import org.meowcat.edxposed.manager.repo.ModuleVersion;
+import org.meowcat.edxposed.manager.repo.ReleaseType;
+import org.meowcat.edxposed.manager.repo.RepoParser;
+import org.meowcat.edxposed.manager.util.DownloadsUtil;
+import org.meowcat.edxposed.manager.util.HashUtil;
+import org.meowcat.edxposed.manager.util.InstallApkUtil;
+import org.meowcat.edxposed.manager.util.ModuleUtil.InstalledModule;
+import org.meowcat.edxposed.manager.util.RepoLoader;
+import org.meowcat.edxposed.manager.util.chrome.LinkTransformationMethod;
+import org.meowcat.edxposed.manager.widget.DownloadView;
+
+import java.io.File;
+import java.text.DateFormat;
+import java.util.Date;
+
+import static org.meowcat.edxposed.manager.XposedApp.WRITE_EXTERNAL_PERMISSION;
+
+public class DownloadDetailsVersionsFragment extends ListFragment {
+ private static VersionsAdapter sAdapter;
+ private DownloadDetailsActivity mActivity;
+ private Module module;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mActivity = (DownloadDetailsActivity) activity;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ module = mActivity.getModule();
+ if (module == null)
+ return;
+
+ if (module.versions.isEmpty()) {
+ setEmptyText(getString(R.string.download_no_versions));
+ setListShown(true);
+ } else {
+ RepoLoader repoLoader = RepoLoader.getInstance();
+ if (!repoLoader.isVersionShown(module.versions.get(0))) {
+ TextView txtHeader = new TextView(getActivity());
+ txtHeader.setText(R.string.download_test_version_not_shown);
+ txtHeader.setTextColor(getResources().getColor(R.color.warning));
+ txtHeader.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mActivity.gotoPage(DownloadDetailsActivity.DOWNLOAD_SETTINGS);
+ }
+ });
+ getListView().addHeaderView(txtHeader);
+ }
+
+ sAdapter = new VersionsAdapter(mActivity, mActivity.getInstalledModule());
+ for (ModuleVersion version : module.versions) {
+ if (repoLoader.isVersionShown(version))
+ sAdapter.add(version);
+ }
+ setListAdapter(sAdapter);
+ }
+
+ getListView().setClipToPadding(false);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ setListAdapter(null);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == WRITE_EXTERNAL_PERMISSION) {
+ if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ DownloadView.mClickedButton.performClick();
+ } else {
+ Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ static class ViewHolder {
+ TextView txtStatus;
+ TextView txtVersion;
+ TextView txtRelType;
+ TextView txtUploadDate;
+ DownloadView downloadView;
+ TextView txtChangesTitle;
+ TextView txtChanges;
+ }
+
+ public static class DownloadModuleCallback implements DownloadsUtil.DownloadFinishedCallback {
+ private final ModuleVersion moduleVersion;
+
+ DownloadModuleCallback(ModuleVersion moduleVersion) {
+ this.moduleVersion = moduleVersion;
+ }
+
+ @Override
+ public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) {
+ File localFile = new File(info.localFilename);
+
+ if (!localFile.isFile())
+ return;
+
+ if (moduleVersion.md5sum != null && !moduleVersion.md5sum.isEmpty()) {
+ try {
+ String actualMd5Sum = HashUtil.md5(localFile);
+ if (!moduleVersion.md5sum.equals(actualMd5Sum)) {
+ Toast.makeText(context, context.getString(R.string.download_md5sum_incorrect, actualMd5Sum, moduleVersion.md5sum), Toast.LENGTH_LONG).show();
+ DownloadsUtil.removeById(context, info.id);
+ return;
+ }
+ } catch (Exception e) {
+ Toast.makeText(context, context.getString(R.string.download_could_not_read_file, e.getMessage()), Toast.LENGTH_LONG).show();
+ DownloadsUtil.removeById(context, info.id);
+ return;
+ }
+ }
+
+ PackageManager pm = context.getPackageManager();
+ PackageInfo packageInfo = pm.getPackageArchiveInfo(info.localFilename, 0);
+
+ if (packageInfo == null) {
+ Toast.makeText(context, R.string.download_no_valid_apk, Toast.LENGTH_LONG).show();
+ DownloadsUtil.removeById(context, info.id);
+ return;
+ }
+
+ if (!packageInfo.packageName.equals(moduleVersion.module.packageName)) {
+ Toast.makeText(context, context.getString(R.string.download_incorrect_package_name, packageInfo.packageName, moduleVersion.module.packageName), Toast.LENGTH_LONG).show();
+ DownloadsUtil.removeById(context, info.id);
+ return;
+ }
+
+ new InstallApkUtil(context, info).execute();
+ }
+ }
+
+ private class VersionsAdapter extends ArrayAdapter {
+ private final DateFormat mDateFormatter = DateFormat
+ .getDateInstance(DateFormat.SHORT);
+ private final int mColorRelTypeStable;
+ private final int mColorRelTypeOthers;
+ private final int mColorInstalled;
+ private final int mColorUpdateAvailable;
+ private final String mTextInstalled;
+ private final String mTextUpdateAvailable;
+ private final long mInstalledVersionCode;
+
+ public VersionsAdapter(Context context, InstalledModule installed) {
+ super(context, R.layout.item_version);
+ TypedValue typedValue = new TypedValue();
+ Resources.Theme theme = context.getTheme();
+ theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true);
+ int color = ContextCompat.getColor(context, typedValue.resourceId);
+ mColorRelTypeStable = color;
+ mColorRelTypeOthers = getResources().getColor(R.color.warning);
+ mColorInstalled = color;
+ mColorUpdateAvailable = getResources().getColor(R.color.download_status_update_available);
+ mTextInstalled = getString(R.string.download_section_installed) + ":";
+ mTextUpdateAvailable = getString(R.string.download_section_update_available) + ":";
+ mInstalledVersionCode = (installed != null) ? installed.versionCode : -1;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = inflater.inflate(R.layout.item_version, null, true);
+ ViewHolder viewHolder = new ViewHolder();
+ viewHolder.txtStatus = view.findViewById(R.id.txtStatus);
+ viewHolder.txtVersion = view.findViewById(R.id.txtVersion);
+ viewHolder.txtRelType = view.findViewById(R.id.txtRelType);
+ viewHolder.txtUploadDate = view.findViewById(R.id.txtUploadDate);
+ viewHolder.downloadView = view.findViewById(R.id.downloadView);
+ viewHolder.txtChangesTitle = view.findViewById(R.id.txtChangesTitle);
+ viewHolder.txtChanges = view.findViewById(R.id.txtChanges);
+ viewHolder.downloadView.fragment = DownloadDetailsVersionsFragment.this;
+ view.setTag(viewHolder);
+ }
+
+ ViewHolder holder = (ViewHolder) view.getTag();
+ ModuleVersion item = getItem(position);
+
+ holder.txtVersion.setText(item.name);
+ holder.txtRelType.setText(item.relType.getTitleId());
+ holder.txtRelType.setTextColor(item.relType == ReleaseType.STABLE
+ ? mColorRelTypeStable : mColorRelTypeOthers);
+
+ if (item.uploaded > 0) {
+ holder.txtUploadDate.setText(
+ mDateFormatter.format(new Date(item.uploaded)));
+ holder.txtUploadDate.setVisibility(View.VISIBLE);
+ } else {
+ holder.txtUploadDate.setVisibility(View.GONE);
+ }
+
+ if (item.code <= 0 || mInstalledVersionCode <= 0
+ || item.code < mInstalledVersionCode) {
+ holder.txtStatus.setVisibility(View.GONE);
+ } else if (item.code == mInstalledVersionCode) {
+ holder.txtStatus.setText(mTextInstalled);
+ holder.txtStatus.setTextColor(mColorInstalled);
+ holder.txtStatus.setVisibility(View.VISIBLE);
+ } else { // item.code > mInstalledVersionCode
+ holder.txtStatus.setText(mTextUpdateAvailable);
+ holder.txtStatus.setTextColor(mColorUpdateAvailable);
+ holder.txtStatus.setVisibility(View.VISIBLE);
+ }
+
+ holder.downloadView.setUrl(item.downloadLink);
+ holder.downloadView.setTitle(mActivity.getModule().name);
+ holder.downloadView.setDownloadFinishedCallback(new DownloadModuleCallback(item));
+
+ if (item.changelog != null && !item.changelog.isEmpty()) {
+ holder.txtChangesTitle.setVisibility(View.VISIBLE);
+ holder.txtChanges.setVisibility(View.VISIBLE);
+
+ if (item.changelogIsHtml) {
+ holder.txtChanges.setText(RepoParser.parseSimpleHtml(getActivity(), item.changelog, holder.txtChanges));
+ holder.txtChanges.setTransformationMethod(new LinkTransformationMethod(getActivity()));
+ holder.txtChanges.setMovementMethod(LinkMovementMethod.getInstance());
+ } else {
+ holder.txtChanges.setText(item.changelog);
+ holder.txtChanges.setMovementMethod(null);
+ }
+
+ } else {
+ holder.txtChangesTitle.setVisibility(View.GONE);
+ holder.txtChanges.setVisibility(View.GONE);
+ }
+
+ return view;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/EdDownloadActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/EdDownloadActivity.java
new file mode 100644
index 00000000..084b1b09
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/EdDownloadActivity.java
@@ -0,0 +1,175 @@
+package org.meowcat.edxposed.manager;
+
+import android.annotation.SuppressLint;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.View;
+import android.widget.CheckBox;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
+import com.annimon.stream.Stream;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.android.material.tabs.TabLayout;
+import com.google.gson.Gson;
+
+import org.meowcat.edxposed.manager.util.json.JSONUtils;
+import org.meowcat.edxposed.manager.util.json.XposedTab;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EdDownloadActivity extends BaseActivity {
+
+ private TabsAdapter tabsAdapter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_ed_download);
+ setSupportActionBar(findViewById(R.id.toolbar));
+ ActionBar bar = getSupportActionBar();
+ if (bar != null) {
+ bar.setDisplayHomeAsUpEnabled(true);
+ }
+ ViewPager mPager = findViewById(R.id.pager);
+ TabLayout mTabLayout = findViewById(R.id.tab_layout);
+
+ tabsAdapter = new TabsAdapter(getSupportFragmentManager());
+ tabsAdapter.notifyDataSetChanged();
+ mPager.setAdapter(tabsAdapter);
+ mTabLayout.setupWithViewPager(mPager);
+ new JSONParser().execute();
+
+ if (!XposedApp.getPreferences().getBoolean("hide_install_warning", false)) {
+ @SuppressLint("InflateParams") final View dontShowAgainView = getLayoutInflater().inflate(R.layout.dialog_install_warning, null);
+
+ new MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.install_warning_title)
+ .setView(dontShowAgainView)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ CheckBox checkBox = dontShowAgainView.findViewById(android.R.id.checkbox);
+ if (checkBox.isChecked())
+ XposedApp.getPreferences().edit().putBoolean("hide_install_warning", true).apply();
+ })
+ .setCancelable(false)
+ .show();
+ }
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(@NonNull Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_installer, menu);
+ if (Build.VERSION.SDK_INT < 26) {
+ menu.findItem(R.id.dexopt_all).setVisible(false);
+ menu.findItem(R.id.speed_all).setVisible(false);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private class JSONParser extends AsyncTask {
+
+ private String newApkVersion = null;
+ private String newApkLink = null;
+ private String newApkChangelog = null;
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ try {
+ String originalJson = JSONUtils.getFileContent(JSONUtils.JSON_LINK);
+
+ final JSONUtils.XposedJson xposedJson = new Gson().fromJson(originalJson, JSONUtils.XposedJson.class);
+
+ List tabs = Stream.of(xposedJson.tabs)
+ .filter(value -> value.sdks.contains(Build.VERSION.SDK_INT)).toList();
+
+ for (XposedTab tab : tabs) {
+ tabsAdapter.addFragment(tab.name, BaseAdvancedInstaller.newInstance(tab));
+ }
+
+ newApkVersion = xposedJson.apk.version;
+ newApkLink = xposedJson.apk.link;
+ newApkChangelog = xposedJson.apk.changelog;
+
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(XposedApp.TAG, "AdvancedInstallerFragment -> " + e.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ super.onPostExecute(result);
+
+ try {
+ tabsAdapter.notifyDataSetChanged();
+
+ if (newApkVersion == null) return;
+
+ SharedPreferences prefs;
+ try {
+ prefs = EdDownloadActivity.this.getSharedPreferences(EdDownloadActivity.this.getPackageName() + "_preferences", MODE_PRIVATE);
+
+ prefs.edit().putString("changelog", newApkChangelog).apply();
+ } catch (NullPointerException ignored) {
+ }
+
+ Integer a = BuildConfig.VERSION_CODE;
+ Integer b = Integer.valueOf(newApkVersion);
+
+ if (a.compareTo(b) < 0) {
+ StatusInstallerFragment.setUpdate(newApkLink, newApkChangelog, EdDownloadActivity.this);
+ }
+
+ } catch (Exception ignored) {
+ }
+
+ }
+ }
+
+ private class TabsAdapter extends FragmentPagerAdapter {
+
+ private final ArrayList titles = new ArrayList<>();
+ private final ArrayList listFragment = new ArrayList<>();
+
+ @SuppressWarnings("deprecation")
+ TabsAdapter(FragmentManager mgr) {
+ super(mgr);
+ addFragment(getString(R.string.tabInstall), new StatusInstallerFragment());
+ }
+
+ void addFragment(String title, Fragment fragment) {
+ titles.add(title);
+ listFragment.add(fragment);
+ }
+
+ @Override
+ public int getCount() {
+ return listFragment.size();
+ }
+
+ @NonNull
+ @Override
+ public Fragment getItem(int position) {
+ return listFragment.get(position);
+ }
+
+ @Override
+ public String getPageTitle(int position) {
+ return titles.get(position);
+ }
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/LogsActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/LogsActivity.java
new file mode 100644
index 00000000..d63fada5
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/LogsActivity.java
@@ -0,0 +1,312 @@
+package org.meowcat.edxposed.manager;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.HorizontalScrollView;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.FileProvider;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Objects;
+
+public class LogsActivity extends BaseActivity {
+ private boolean errorLog = false;
+ private File mFileErrorLog = new File(XposedApp.BASE_DIR + "log/error.log");
+ private File mFileErrorLogOld = new File(
+ XposedApp.BASE_DIR + "log/error.log.old");
+ private File mFileErrorLogError = new File(XposedApp.BASE_DIR + "log/all.log");
+ private File mFileErrorLogOldError = new File(XposedApp.BASE_DIR + "log/all.log.old");
+ private TextView mTxtLog;
+ private ScrollView mSVLog;
+ private HorizontalScrollView mHSVLog;
+ private MenuItem mClickedMenuItem = null;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_logs);
+ setSupportActionBar(findViewById(R.id.toolbar));
+ ActionBar bar = getSupportActionBar();
+ if (bar != null) {
+ bar.setDisplayHomeAsUpEnabled(true);
+ }
+ mTxtLog = findViewById(R.id.txtLog);
+ mTxtLog.setTextIsSelectable(true);
+ mSVLog = findViewById(R.id.svLog);
+ mHSVLog = findViewById(R.id.hsvLog);
+
+ if (!XposedApp.getPreferences().getBoolean("hide_logcat_warning", false)) {
+ @SuppressLint("InflateParams") final View dontShowAgainView = getLayoutInflater().inflate(R.layout.dialog_install_warning, null);
+
+ TextView message = dontShowAgainView.findViewById(android.R.id.message);
+ message.setText(R.string.not_logcat);
+
+ new MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.install_warning_title)
+ .setView(dontShowAgainView)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ CheckBox checkBox = dontShowAgainView.findViewById(android.R.id.checkbox);
+ if (checkBox.isChecked())
+ XposedApp.getPreferences().edit().putBoolean("hide_logcat_warning", true).apply();
+ })
+ .setCancelable(false)
+ .show();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ reloadErrorLog();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(@NonNull Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_logs, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ mClickedMenuItem = item;
+ switch (item.getItemId()) {
+ case R.id.menu_logs:
+ item.setChecked(true);
+ errorLog = false;
+ reloadErrorLog();
+ break;
+ case R.id.menu_logs_err:
+ item.setChecked(true);
+ errorLog = true;
+ reloadErrorLog();
+ scrollDown();
+ break;
+ case R.id.menu_scroll_top:
+ scrollTop();
+ break;
+ case R.id.menu_scroll_down:
+ scrollDown();
+ break;
+ case R.id.menu_refresh:
+ reloadErrorLog();
+ return true;
+ case R.id.menu_send:
+ try {
+ send();
+ } catch (NullPointerException ignored) {
+ }
+ return true;
+ case R.id.menu_save:
+ save();
+ return true;
+ case R.id.menu_clear:
+ clear();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void scrollTop() {
+ mSVLog.post(() -> mSVLog.scrollTo(0, 0));
+ mHSVLog.post(() -> mHSVLog.scrollTo(0, 0));
+ }
+
+ private void scrollDown() {
+ mSVLog.post(() -> mSVLog.scrollTo(0, mTxtLog.getHeight()));
+ mHSVLog.post(() -> mHSVLog.scrollTo(0, 0));
+ }
+
+ private void reloadErrorLog() {
+ new LogsReader().execute(errorLog ? mFileErrorLogError : mFileErrorLog);
+ mSVLog.post(() -> mSVLog.scrollTo(0, mTxtLog.getHeight()));
+ mHSVLog.post(() -> mHSVLog.scrollTo(0, 0));
+ }
+
+ private void clear() {
+ try {
+ new FileOutputStream(errorLog ? mFileErrorLogError : mFileErrorLog).close();
+ (errorLog ? mFileErrorLogOldError : mFileErrorLogOld).delete();
+ mTxtLog.setText(R.string.log_is_empty);
+ Toast.makeText(this, R.string.logs_cleared,
+ Toast.LENGTH_SHORT).show();
+ reloadErrorLog();
+ } catch (IOException e) {
+ Toast.makeText(this, getResources().getString(R.string.logs_clear_failed) + "n" + e.getMessage(), Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void send() {
+ Uri uri = FileProvider.getUriForFile(Objects.requireNonNull(this), "org.meowcat.edxposed.manager.fileprovider", errorLog ? mFileErrorLogError : mFileErrorLog);
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_STREAM, uri);
+ sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ sendIntent.setType("application/html");
+ startActivity(Intent.createChooser(sendIntent, getResources().getString(R.string.menuSend)));
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions,
+ grantResults);
+ if (requestCode == XposedApp.WRITE_EXTERNAL_PERMISSION) {
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (mClickedMenuItem != null) {
+ new Handler().postDelayed(() -> onOptionsItemSelected(mClickedMenuItem), 500);
+ }
+ } else {
+ Toast.makeText(this, R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ @SuppressLint("DefaultLocale")
+ private void save() {
+ if (ActivityCompat.checkSelfPermission(Objects.requireNonNull(this), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, XposedApp.WRITE_EXTERNAL_PERMISSION);
+ return;
+ }
+
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ Toast.makeText(this, R.string.sdcard_not_writable, Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ Calendar now = Calendar.getInstance();
+ String filename = String.format(
+ "EdXposed_Verbose_%04d%02d%02d_%02d%02d%02d.log",
+ now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
+ now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
+ now.get(Calendar.MINUTE), now.get(Calendar.SECOND));
+
+ File targetFile = new File(XposedApp.createFolder(), filename);
+
+ try {
+ FileInputStream in = new FileInputStream(errorLog ? mFileErrorLogError : mFileErrorLog);
+ FileOutputStream out = new FileOutputStream(targetFile);
+ byte[] buffer = new byte[1024];
+ int len;
+ while ((len = in.read(buffer)) > 0) {
+ out.write(buffer, 0, len);
+ }
+ in.close();
+ out.close();
+
+ Toast.makeText(this, targetFile.toString(),
+ Toast.LENGTH_LONG).show();
+ } catch (IOException e) {
+ Toast.makeText(this, getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show();
+ }
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private class LogsReader extends AsyncTask {
+
+ private static final int MAX_LOG_SIZE = 1000 * 1024; // 1000 KB
+ private ProgressDialog mProgressDialog;
+
+ private long skipLargeFile(BufferedReader is, long length) throws IOException {
+ if (length < MAX_LOG_SIZE)
+ return 0;
+
+ long skipped = length - MAX_LOG_SIZE;
+ long yetToSkip = skipped;
+ do {
+ yetToSkip -= is.skip(yetToSkip);
+ } while (yetToSkip > 0);
+
+ int c;
+ do {
+ c = is.read();
+ if (c == -1)
+ break;
+ skipped++;
+ } while (c != '\n');
+
+ return skipped;
+
+ }
+
+ @Override
+ protected void onPreExecute() {
+ mTxtLog.setText("");
+ mProgressDialog = new ProgressDialog(LogsActivity.this);
+ mProgressDialog.setMessage(getString(R.string.loading));
+ mProgressDialog.setProgress(0);
+ mProgressDialog.show();
+ }
+
+ @Override
+ protected String doInBackground(File... log) {
+ Thread.currentThread().setPriority(Thread.NORM_PRIORITY + 2);
+
+ StringBuilder llog = new StringBuilder(15 * 10 * 1024);
+
+ if (XposedApp.getPreferences().getBoolean(
+ "disable_verbose_log", false) && errorLog) {
+ llog.append(LogsActivity.this.getResources().getString(R.string.logs_verbose_disabled));
+ return llog.toString();
+ }
+ try {
+ File logfile = log[0];
+ BufferedReader br;
+ br = new BufferedReader(new FileReader(logfile));
+ long skipped = skipLargeFile(br, logfile.length());
+ if (skipped > 0) {
+ llog.append(LogsActivity.this.getResources().getString(R.string.logs_too_long));
+ llog.append("\n-----------------\n");
+ }
+
+ char[] temp = new char[1024];
+ int read;
+ while ((read = br.read(temp)) > 0) {
+ llog.append(temp, 0, read);
+ }
+ br.close();
+ } catch (IOException e) {
+ llog.append(LogsActivity.this.getResources().getString(R.string.logs_cannot_read));
+ llog.append(e.getMessage());
+ }
+
+ return llog.toString();
+ }
+
+ @Override
+ protected void onPostExecute(String llog) {
+ mProgressDialog.dismiss();
+ mTxtLog.setText(llog);
+
+ if (llog.length() == 0)
+ mTxtLog.setText(R.string.log_is_empty);
+ }
+
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/MainActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/MainActivity.java
new file mode 100644
index 00000000..d98a1329
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/MainActivity.java
@@ -0,0 +1,179 @@
+package org.meowcat.edxposed.manager;
+
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.google.android.material.card.MaterialCardView;
+
+import org.meowcat.edxposed.manager.util.ModuleUtil;
+import org.meowcat.edxposed.manager.util.RepoLoader;
+
+public class MainActivity extends BaseActivity implements RepoLoader.RepoListener, ModuleUtil.ModuleListener {
+
+ private RepoLoader mRepoLoader;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ mRepoLoader = RepoLoader.getInstance();
+ ModuleUtil.getInstance().addListener(this);
+ mRepoLoader.addListener(this, false);
+ findViewById(R.id.activity_main_modules).setOnClickListener(v -> {
+ Intent intent = new Intent();
+ intent.setClass(getApplicationContext(), ModulesActivity.class);
+ startActivity(intent);
+ });
+ findViewById(R.id.activity_main_downloads).setOnClickListener(v -> {
+ Intent intent = new Intent();
+ intent.setClass(getApplicationContext(), DownloadActivity.class);
+ startActivity(intent);
+ });
+ findViewById(R.id.activity_main_apps).setOnClickListener(v -> {
+ Intent intent = new Intent();
+ intent.setClass(getApplicationContext(), BlackListActivity.class);
+ startActivity(intent);
+ });
+ findViewById(R.id.activity_main_status).setOnClickListener(v -> {
+ Intent intent = new Intent();
+ intent.setClass(getApplicationContext(), EdDownloadActivity.class);
+ startActivity(intent);
+ });
+ findViewById(R.id.activity_main_settings).setOnClickListener(v -> {
+ Intent intent = new Intent();
+ intent.setClass(getApplicationContext(), SettingsActivity.class);
+ startActivity(intent);
+ });
+ findViewById(R.id.activity_main_logs).setOnClickListener(v -> {
+ Intent intent = new Intent();
+ intent.setClass(getApplicationContext(), LogsActivity.class);
+ startActivity(intent);
+ });
+ findViewById(R.id.activity_main_about).setOnClickListener(v -> {
+ Intent intent = new Intent();
+ intent.setClass(getApplicationContext(), AboutActivity.class);
+ startActivity(intent);
+ });
+ String installedXposedVersion;
+ try {
+ installedXposedVersion = XposedApp.getXposedProp().getVersion();
+ } catch (NullPointerException e) {
+ installedXposedVersion = null;
+ }
+ MaterialCardView cardView = findViewById(R.id.activity_main_status);
+ TextView title = findViewById(R.id.activity_main_status_title);
+ ImageView icon = findViewById(R.id.activity_main_status_icon);
+ TextView details = findViewById(R.id.activity_main_status_summary);
+ if (installedXposedVersion != null) {
+ int installedXposedVersionInt = extractIntPart(installedXposedVersion);
+ if (installedXposedVersionInt == XposedApp.getXposedVersion()) {
+ String installedXposedVersionStr = installedXposedVersionInt + ".0";
+ title.setText(R.string.Activated);
+ details.setText(installedXposedVersion.replace(installedXposedVersionStr + "-", ""));
+ cardView.setCardBackgroundColor(getResources().getColor(R.color.download_status_update_available));
+ icon.setImageDrawable(getDrawable(R.drawable.ic_check_circle));
+ } else {
+ title.setText(R.string.Inactivate);
+ details.setText(R.string.installed_lollipop_inactive);
+ cardView.setCardBackgroundColor(getResources().getColor(R.color.amber_500));
+ icon.setImageDrawable(getDrawable(R.drawable.ic_warning));
+ }
+ } else {
+ title.setText(R.string.Install);
+ details.setText(R.string.InstallDetail);
+ cardView.setCardBackgroundColor(getResources().getColor(R.color.colorPrimary));
+ icon.setImageDrawable(getDrawable(R.drawable.ic_error));
+ }
+ notifyDataSetChanged();
+ }
+
+ private int extractIntPart(String str) {
+ int result = 0, length = str.length();
+ for (int offset = 0; offset < length; offset++) {
+ char c = str.charAt(offset);
+ if ('0' <= c && c <= '9')
+ result = result * 10 + (c - '0');
+ else
+ break;
+ }
+ return result;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+
+ //noinspection SimplifiableIfStatement
+ if (id == R.id.action_settings) {
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @SuppressLint("SetTextI18n")
+ private void notifyDataSetChanged() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ String frameworkUpdateVersion = mRepoLoader.getFrameworkUpdateVersion();
+ boolean moduleUpdateAvailable = mRepoLoader.hasModuleUpdates();
+ ModuleUtil.getInstance().getEnabledModules().size();
+ TextView description = findViewById(R.id.activity_main_modules_summary);
+ description.setText(String.format(getString(R.string.ModulesDetail), ModuleUtil.getInstance().getEnabledModules().size()));
+ if (frameworkUpdateVersion != null) {
+ description = findViewById(R.id.activity_main_status_summary);
+ description.setText(String.format(getString(R.string.welcome_framework_update_available), frameworkUpdateVersion));
+ }
+ description = findViewById(R.id.activity_main_download_summary);
+ if (moduleUpdateAvailable) {
+ description.setText(R.string.modules_updates_available);
+ } else {
+ description.setText(R.string.ModuleUptodate);
+ }
+ }
+ });
+ }
+
+
+ @Override
+ public void onInstalledModulesReloaded(ModuleUtil moduleUtil) {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, ModuleUtil.InstalledModule module) {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onRepoReloaded(RepoLoader loader) {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ ModuleUtil.getInstance().removeListener(this);
+ mRepoLoader.removeListener(this);
+ }
+
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/ModulesActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/ModulesActivity.java
new file mode 100644
index 00000000..04b59880
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/ModulesActivity.java
@@ -0,0 +1,654 @@
+package org.meowcat.edxposed.manager;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Filter;
+import android.widget.ImageView;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.view.menu.MenuBuilder;
+import androidx.appcompat.view.menu.MenuPopupHelper;
+import androidx.appcompat.widget.PopupMenu;
+import androidx.appcompat.widget.SearchView;
+import androidx.core.app.ActivityCompat;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import org.meowcat.edxposed.manager.repo.Module;
+import org.meowcat.edxposed.manager.repo.ModuleVersion;
+import org.meowcat.edxposed.manager.repo.ReleaseType;
+import org.meowcat.edxposed.manager.repo.RepoDb;
+import org.meowcat.edxposed.manager.util.DownloadsUtil;
+import org.meowcat.edxposed.manager.util.InstallApkUtil;
+import org.meowcat.edxposed.manager.util.ModuleUtil;
+import org.meowcat.edxposed.manager.util.NavUtil;
+import org.meowcat.edxposed.manager.util.RepoLoader;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
+
+public class ModulesActivity extends BaseActivity implements ModuleUtil.ModuleListener {
+
+ public static final String SETTINGS_CATEGORY = "de.robv.android.xposed.category.MODULE_SETTINGS";
+ private int installedXposedVersion;
+ private ApplicationFilter filter;
+ private SearchView mSearchView;
+ private SearchView.OnQueryTextListener mSearchListener;
+ private PackageManager mPm;
+ private DateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
+ private ModuleUtil mModuleUtil;
+ private ModuleAdapter mAdapter = null;
+ private MenuItem mClickedMenuItem = null;
+ private RecyclerView mListView;
+ private SwipeRefreshLayout mSwipeRefreshLayout;
+ private Runnable reloadModules = new Runnable() {
+ public void run() {
+ String queryStr = mSearchView != null ? mSearchView.getQuery().toString() : "";
+ Collection showList;
+ Collection fullList = mModuleUtil.getModules().values();
+ if (queryStr.length() == 0) {
+ showList = fullList;
+ } else {
+ showList = new ArrayList<>();
+ String filter = queryStr.toLowerCase();
+ for (ModuleUtil.InstalledModule info : fullList) {
+ if (lowercaseContains(InstallApkUtil.getAppLabel(info.app, mPm), filter)
+ || lowercaseContains(info.packageName, filter)) {
+ showList.add(info);
+ }
+ }
+ }
+ mAdapter.addAll(showList);
+ mAdapter.notifyDataSetChanged();
+ mModuleUtil.updateModulesList(false);
+ mSwipeRefreshLayout.setRefreshing(false);
+ }
+ };
+
+ private void filter(String constraint) {
+ filter.filter(constraint);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_modules);
+ setSupportActionBar(findViewById(R.id.toolbar));
+ ActionBar bar = getSupportActionBar();
+ if (bar != null) {
+ bar.setDisplayHomeAsUpEnabled(true);
+ }
+ filter = new ApplicationFilter();
+ mModuleUtil = ModuleUtil.getInstance();
+ mPm = getPackageManager();
+ installedXposedVersion = XposedApp.getXposedVersion();
+ if (Build.VERSION.SDK_INT >= 21) {
+ if (installedXposedVersion <= 0) {
+ addHeader();
+ }
+ } else {
+ //if (StatusInstallerFragment.DISABLE_FILE.exists()) installedXposedVersion = -1;
+ if (installedXposedVersion <= 0) {
+ addHeader();
+ }
+ }
+ mAdapter = new ModuleAdapter();
+ mModuleUtil.addListener(this);
+ mListView = findViewById(R.id.recyclerView);
+ mListView.setAdapter(mAdapter);
+ mListView.setLayoutManager(new LinearLayoutManager(this));
+ DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mListView.getContext(),
+ DividerItemDecoration.VERTICAL);
+ mListView.addItemDecoration(dividerItemDecoration);
+ mSwipeRefreshLayout = findViewById(R.id.swipeRefreshLayout);
+ mSwipeRefreshLayout.setOnRefreshListener(() -> reloadModules.run());
+ reloadModules.run();
+ mSearchListener = new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ filter(query);
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ filter(newText);
+ return false;
+ }
+ };
+
+ }
+
+ private void addHeader() {
+ //View notActiveNote = getLayoutInflater().inflate(R.layout.xposed_not_active_note, mListView, false);
+ //notActiveNote.setTag(NOT_ACTIVE_NOTE_TAG);
+ //mListView.addHeaderView(notActiveNote);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_modules, menu);
+ mSearchView = (SearchView) menu.findItem(R.id.app_search).getActionView();
+ mSearchView.setOnQueryTextListener(mSearchListener);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions,
+ grantResults);
+ if (requestCode == XposedApp.WRITE_EXTERNAL_PERMISSION) {
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (mClickedMenuItem != null) {
+ new Handler().postDelayed(() -> onOptionsItemSelected(mClickedMenuItem), 500);
+ }
+ } else {
+ Toast.makeText(this, R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ File enabledModulesPath = new File(XposedApp.createFolder(), "enabled_modules.list");
+ File installedModulesPath = new File(XposedApp.createFolder(), "installed_modules.list");
+ File listModules = new File(XposedApp.ENABLED_MODULES_LIST_FILE);
+
+ mClickedMenuItem = item;
+
+ if (checkPermissions())
+ return false;
+
+ switch (item.getItemId()) {
+ case R.id.export_enabled_modules:
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ return false;
+ }
+
+ if (ModuleUtil.getInstance().getEnabledModules().isEmpty()) {
+ Toast.makeText(this, getString(R.string.no_enabled_modules), Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ try {
+ XposedApp.createFolder();
+
+ FileInputStream in = new FileInputStream(listModules);
+ FileOutputStream out = new FileOutputStream(enabledModulesPath);
+
+ byte[] buffer = new byte[1024];
+ int len;
+ while ((len = in.read(buffer)) > 0) {
+ out.write(buffer, 0, len);
+ }
+ in.close();
+ out.close();
+ } catch (IOException e) {
+ Toast.makeText(this, getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show();
+ return false;
+ }
+
+ Toast.makeText(this, enabledModulesPath.toString(), Toast.LENGTH_LONG).show();
+ return true;
+ case R.id.export_installed_modules:
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ Toast.makeText(this, R.string.sdcard_not_writable, Toast.LENGTH_LONG).show();
+ return false;
+ }
+ Map installedModules = ModuleUtil.getInstance().getModules();
+
+ if (installedModules.isEmpty()) {
+ Toast.makeText(this, getString(R.string.no_installed_modules), Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ try {
+ XposedApp.createFolder();
+
+ FileWriter fw = new FileWriter(installedModulesPath);
+ BufferedWriter bw = new BufferedWriter(fw);
+ PrintWriter fileOut = new PrintWriter(bw);
+
+ Set keys = installedModules.keySet();
+ for (String key1 : keys) {
+ fileOut.println(key1);
+ }
+
+ fileOut.close();
+ } catch (IOException e) {
+ Toast.makeText(this, getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show();
+ return false;
+ }
+
+ Toast.makeText(this, installedModulesPath.toString(), Toast.LENGTH_LONG).show();
+ return true;
+ case R.id.import_installed_modules:
+ return importModules(installedModulesPath);
+ case R.id.import_enabled_modules:
+ return importModules(enabledModulesPath);
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private boolean checkPermissions() {
+ if (ActivityCompat.checkSelfPermission(Objects.requireNonNull(this), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, XposedApp.WRITE_EXTERNAL_PERMISSION);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean importModules(File path) {
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ Toast.makeText(this, R.string.sdcard_not_writable, Toast.LENGTH_LONG).show();
+ return false;
+ }
+ InputStream ips = null;
+ RepoLoader repoLoader = RepoLoader.getInstance();
+ List list = new ArrayList<>();
+ if (!path.exists()) {
+ Toast.makeText(this, getString(R.string.no_backup_found),
+ Toast.LENGTH_LONG).show();
+ return false;
+ }
+ try {
+ ips = new FileInputStream(path);
+ } catch (FileNotFoundException e) {
+ Log.e(XposedApp.TAG, "ModulesFragment -> " + e.getMessage());
+ }
+
+ if (path.length() == 0) {
+ Toast.makeText(this, R.string.file_is_empty,
+ Toast.LENGTH_LONG).show();
+ return false;
+ }
+
+ try {
+ assert ips != null;
+ InputStreamReader ipsr = new InputStreamReader(ips);
+ BufferedReader br = new BufferedReader(ipsr);
+ String line;
+ while ((line = br.readLine()) != null) {
+ Module m = repoLoader.getModule(line);
+
+ if (m == null) {
+ Toast.makeText(this, getString(R.string.download_details_not_found,
+ line), Toast.LENGTH_SHORT).show();
+ } else {
+ list.add(m);
+ }
+ }
+ br.close();
+ } catch (ActivityNotFoundException | IOException e) {
+ Toast.makeText(this, e.toString(), Toast.LENGTH_SHORT).show();
+ }
+
+ for (final Module m : list) {
+ ModuleVersion mv = null;
+ for (int i = 0; i < m.versions.size(); i++) {
+ ModuleVersion mvTemp = m.versions.get(i);
+
+ if (mvTemp.relType == ReleaseType.STABLE) {
+ mv = mvTemp;
+ break;
+ }
+ }
+
+ if (mv != null) {
+ DownloadsUtil.addModule(this, m.name, mv.downloadLink, false, (context, info) -> new InstallApkUtil(this, info).execute());
+ }
+ }
+
+ ModuleUtil.getInstance().reloadInstalledModules();
+
+ return true;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mModuleUtil.removeListener(this);
+ mListView.setAdapter(null);
+ mAdapter = null;
+ }
+
+ @Override
+ public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, ModuleUtil.InstalledModule module) {
+ mModuleUtil.updateModulesList(false);
+ runOnUiThread(reloadModules);
+ }
+
+ @Override
+ public void onInstalledModulesReloaded(ModuleUtil moduleUtil) {
+ mModuleUtil.updateModulesList(false);
+ runOnUiThread(reloadModules);
+ }
+
+ @SuppressLint("RestrictedApi")
+ private void showMenu(@NonNull Context context,
+ @NonNull View anchor,
+ @NonNull ApplicationInfo info) {
+ PopupMenu appMenu = new PopupMenu(context, anchor);
+ appMenu.inflate(R.menu.context_menu_modules);
+ ModuleUtil.InstalledModule installedModule = ModuleUtil.getInstance().getModule(info.packageName);
+ if (installedModule == null) {
+ return;
+ }
+ try {
+ String support = RepoDb
+ .getModuleSupport(installedModule.packageName);
+ if (NavUtil.parseURL(support) == null)
+ appMenu.getMenu().removeItem(R.id.menu_support);
+ } catch (RepoDb.RowNotFoundException e) {
+ appMenu.getMenu().removeItem(R.id.menu_download_updates);
+ appMenu.getMenu().removeItem(R.id.menu_support);
+ }
+ appMenu.setOnMenuItemClickListener(menuItem -> {
+ ModuleUtil.InstalledModule module = ModuleUtil.getInstance().getModule(info.packageName);
+ if (module == null) {
+ return false;
+ }
+ switch (menuItem.getItemId()) {
+ case R.id.menu_launch:
+ String packageName = module.packageName;
+ if (packageName == null) {
+ return false;
+ }
+ Intent launchIntent = getSettingsIntent(packageName);
+ if (launchIntent != null) {
+ startActivity(launchIntent);
+ } else {
+ Toast.makeText(this, getString(R.string.module_no_ui), Toast.LENGTH_LONG).show();
+ }
+ return true;
+
+ case R.id.menu_download_updates:
+ Intent detailsIntent = new Intent(this, DownloadDetailsActivity.class);
+ detailsIntent.setData(Uri.fromParts("package", module.packageName, null));
+ startActivity(detailsIntent);
+ return true;
+
+ case R.id.menu_support:
+ NavUtil.startURL(this, Uri.parse(RepoDb.getModuleSupport(module.packageName)));
+ return true;
+
+ case R.id.menu_app_store:
+ Uri uri = Uri.parse("market://details?id=" + module.packageName);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ try {
+ startActivity(intent);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ return true;
+
+ case R.id.menu_app_info:
+ startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", module.packageName, null)));
+ return true;
+
+ case R.id.menu_uninstall:
+ startActivity(new Intent(Intent.ACTION_UNINSTALL_PACKAGE, Uri.fromParts("package", module.packageName, null)));
+ return true;
+ }
+ return true;
+ });
+ MenuPopupHelper menuHelper = new MenuPopupHelper(context, (MenuBuilder) appMenu.getMenu(), anchor);
+ menuHelper.setForceShowIcon(true);
+ menuHelper.show();
+ }
+
+ private Intent getSettingsIntent(String packageName) {
+ // taken from
+ // ApplicationPackageManager.getLaunchIntentForPackage(String)
+ // first looks for an Xposed-specific category, falls back to
+ // getLaunchIntentForPackage
+ PackageManager pm = getPackageManager();
+
+ Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
+ intentToResolve.addCategory(SETTINGS_CATEGORY);
+ intentToResolve.setPackage(packageName);
+ List ris = pm.queryIntentActivities(intentToResolve, 0);
+
+ if (ris.size() <= 0) {
+ return pm.getLaunchIntentForPackage(packageName);
+ }
+
+ Intent intent = new Intent(intentToResolve);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name);
+ return intent;
+ }
+
+ public void onItemClick(View view) {
+ if (getFragmentManager() != null) {
+ try {
+ showMenu(this, view, Objects.requireNonNull(this).getPackageManager().getApplicationInfo((String) view.getTag(), 0));
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ String packageName = (String) view.getTag();
+ if (packageName == null)
+ return;
+
+ Intent launchIntent = getSettingsIntent(packageName);
+ if (launchIntent != null) {
+ startActivity(launchIntent);
+ } else {
+ Toast.makeText(this, getString(R.string.module_no_ui), Toast.LENGTH_LONG).show();
+ }
+ }
+ } else {
+ String packageName = (String) view.getTag();
+ if (packageName == null) {
+ return;
+ }
+ Intent launchIntent = getSettingsIntent(packageName);
+ if (launchIntent != null) {
+ startActivity(launchIntent);
+ } else {
+ Toast.makeText(this, getString(R.string.module_no_ui), Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ private boolean lowercaseContains(String s, CharSequence filter) {
+ return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter);
+ }
+
+ private class ModuleAdapter extends RecyclerView.Adapter {
+ Collection items;
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_module, parent, false);
+ return new ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ //View view = holder.itemView;
+ ModuleUtil.InstalledModule item = (ModuleUtil.InstalledModule) items.toArray()[position];
+ holder.itemView.setOnClickListener(v -> ModulesActivity.this.onItemClick(holder.itemView));
+ holder.itemView.setTag(item.packageName);
+
+ holder.appName.setText(item.getAppName());
+
+ TextView version = holder.appVersion;
+ version.setText(Objects.requireNonNull(item).versionName);
+ version.setSelected(true);
+ version.setTextColor(Color.parseColor("#808080"));
+
+ TextView packageTv = holder.appPackage;
+ packageTv.setText(item.packageName);
+ packageTv.setSelected(true);
+
+ TextView installTimeTv = holder.appInstallTime;
+ installTimeTv.setText(dateformat.format(new Date(item.installTime)));
+ installTimeTv.setSelected(true);
+
+ TextView updateTv = holder.appUpdateTime;
+ updateTv.setText(dateformat.format(new Date(item.updateTime)));
+ updateTv.setSelected(true);
+
+ holder.appIcon.setImageDrawable(item.getIcon());
+
+ TextView descriptionText = holder.appDescription;
+ if (!item.getDescription().isEmpty()) {
+ descriptionText.setText(item.getDescription());
+ //descriptionText.setTextColor(ThemeUtil.getThemeColor(this, android.R.attr.textColorSecondary));
+ } else {
+ descriptionText.setText(getString(R.string.module_empty_description));
+ descriptionText.setTextColor(getResources().getColor(R.color.warning));
+ }
+
+ Switch mSwitch = holder.mSwitch;
+ mSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ String packageName = item.packageName;
+ boolean changed = mModuleUtil.isModuleEnabled(packageName) ^ isChecked;
+ if (changed) {
+ mModuleUtil.setModuleEnabled(packageName, isChecked);
+ mModuleUtil.updateModulesList(true);
+ }
+ });
+ mSwitch.setChecked(mModuleUtil.isModuleEnabled(item.packageName));
+ TextView warningText = holder.warningText;
+
+ if (item.minVersion == 0) {
+ if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) {
+ mSwitch.setEnabled(false);
+ }
+ warningText.setText(getString(R.string.no_min_version_specified));
+ warningText.setVisibility(View.VISIBLE);
+ } else if (installedXposedVersion > 0 && item.minVersion > installedXposedVersion) {
+ if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) {
+ mSwitch.setEnabled(false);
+ }
+ warningText.setText(String.format(getString(R.string.warning_xposed_min_version), item.minVersion));
+ warningText.setVisibility(View.VISIBLE);
+ } else if (item.minVersion < ModuleUtil.MIN_MODULE_VERSION) {
+ if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) {
+ mSwitch.setEnabled(false);
+ }
+ warningText.setText(String.format(getString(R.string.warning_min_version_too_low), item.minVersion, ModuleUtil.MIN_MODULE_VERSION));
+ warningText.setVisibility(View.VISIBLE);
+ } else if (item.isInstalledOnExternalStorage()) {
+ if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) {
+ mSwitch.setEnabled(false);
+ }
+ warningText.setText(getString(R.string.warning_installed_on_external_storage));
+ warningText.setVisibility(View.VISIBLE);
+ } else if (installedXposedVersion == 0 || (installedXposedVersion == -1)) {
+ if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) {
+ mSwitch.setEnabled(false);
+ }
+ warningText.setText(getString(R.string.not_installed_no_lollipop));
+ warningText.setVisibility(View.VISIBLE);
+ } else {
+ mSwitch.setEnabled(true);
+ warningText.setVisibility(View.GONE);
+ }
+ }
+
+ void addAll(Collection items) {
+ this.items = items;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemCount() {
+ if (items != null) {
+ return items.size();
+ } else {
+ return 0;
+ }
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+ ImageView appIcon;
+ TextView appName;
+ TextView appPackage;
+ TextView appDescription;
+ TextView appVersion;
+ TextView appInstallTime;
+ TextView appUpdateTime;
+ TextView warningText;
+ Switch mSwitch;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ appIcon = itemView.findViewById(R.id.icon);
+ appName = itemView.findViewById(R.id.title);
+ appDescription = itemView.findViewById(R.id.description);
+ appPackage = itemView.findViewById(R.id.package_name);
+ appVersion = itemView.findViewById(R.id.version_name);
+ appInstallTime = itemView.findViewById(R.id.tvInstallTime);
+ appUpdateTime = itemView.findViewById(R.id.tvUpdateTime);
+ warningText = itemView.findViewById(R.id.warning);
+ mSwitch = itemView.findViewById(R.id.checkbox);
+ }
+ }
+ }
+
+ class ApplicationFilter extends Filter {
+
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ runOnUiThread(reloadModules);
+ return null;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ runOnUiThread(reloadModules);
+ }
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/SettingsActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/SettingsActivity.java
new file mode 100644
index 00000000..cefb46c6
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/SettingsActivity.java
@@ -0,0 +1,371 @@
+package org.meowcat.edxposed.manager;
+
+import android.annotation.SuppressLint;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.FileUtils;
+import android.widget.Toast;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.preference.Preference;
+import androidx.preference.SwitchPreference;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.takisoft.preferencex.PreferenceFragmentCompat;
+import com.topjohnwu.superuser.Shell;
+
+import org.meowcat.edxposed.manager.util.RepoLoader;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+
+public class SettingsActivity extends BaseActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_settings);
+ setSupportActionBar(findViewById(R.id.toolbar));
+ ActionBar bar = getSupportActionBar();
+ if (bar != null) {
+ bar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.container, new SettingsFragment()).commit();
+ }
+
+ }
+
+
+ @SuppressWarnings({"ResultOfMethodCallIgnored", "deprecation"})
+ public static class SettingsFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener, SharedPreferences.OnSharedPreferenceChangeListener {
+ static final File mDisableResourcesFlag = new File(XposedApp.BASE_DIR + "conf/disable_resources");
+ static final File mDynamicModulesFlag = new File(XposedApp.BASE_DIR + "conf/dynamicmodules");
+ static final File mWhiteListModeFlag = new File(XposedApp.BASE_DIR + "conf/usewhitelist");
+ static final File mBlackWhiteListModeFlag = new File(XposedApp.BASE_DIR + "conf/blackwhitelist");
+ static final File mDeoptBootFlag = new File(XposedApp.BASE_DIR + "conf/deoptbootimage");
+ static final File mDisableVerboseLogsFlag = new File(XposedApp.BASE_DIR + "conf/disable_verbose_log");
+ static final File mDisableModulesLogsFlag = new File(XposedApp.BASE_DIR + "conf/disable_modules_log");
+ static final File mVerboseLogProcessID = new File(XposedApp.BASE_DIR + "log/all.pid");
+ static final File mModulesLogProcessID = new File(XposedApp.BASE_DIR + "log/error.pid");
+
+ private Preference stopVerboseLog;
+ private Preference stopLog;
+
+ public SettingsFragment() {
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ @SuppressLint({"WorldReadableFiles", "WorldWriteableFiles"})
+ static void setFilePermissionsFromMode(String name, int mode) {
+ int perms = FileUtils.S_IRUSR | FileUtils.S_IWUSR
+ | FileUtils.S_IRGRP | FileUtils.S_IWGRP;
+ if ((mode & MODE_WORLD_READABLE) != 0) {
+ perms |= FileUtils.S_IROTH;
+ }
+ if ((mode & MODE_WORLD_WRITEABLE) != 0) {
+ perms |= FileUtils.S_IWOTH;
+ }
+ FileUtils.setPermissions(name, perms, -1, -1);
+ }
+
+ @SuppressLint({"ObsoleteSdkInt", "WorldReadableFiles"})
+ @Override
+ public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(R.xml.prefs);
+
+ stopVerboseLog = findPreference("stop_verbose_log");
+ stopLog = findPreference("stop_log");
+
+ //noinspection ConstantConditions
+ findPreference("release_type_global").setOnPreferenceChangeListener((preference, newValue) -> {
+ RepoLoader.getInstance().setReleaseTypeGlobal((String) newValue);
+ return true;
+ });
+
+ SwitchPreference prefWhiteListMode = findPreference("white_list_switch");
+ Objects.requireNonNull(prefWhiteListMode).setChecked(mWhiteListModeFlag.exists());
+ prefWhiteListMode.setOnPreferenceChangeListener((preference, newValue) -> {
+ boolean enabled = (Boolean) newValue;
+ if (enabled) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(mWhiteListModeFlag.getPath());
+ setFilePermissionsFromMode(mWhiteListModeFlag.getPath(), MODE_WORLD_READABLE);
+ } catch (FileNotFoundException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ try {
+ mWhiteListModeFlag.createNewFile();
+ } catch (IOException e1) {
+ Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+ } else {
+ mWhiteListModeFlag.delete();
+ }
+ return (enabled == mWhiteListModeFlag.exists());
+ });
+
+ SwitchPreference prefVerboseLogs = findPreference("disable_verbose_log");
+ Objects.requireNonNull(prefVerboseLogs).setChecked(mDisableVerboseLogsFlag.exists());
+ prefVerboseLogs.setOnPreferenceChangeListener((preference, newValue) -> {
+ boolean enabled = (Boolean) newValue;
+ if (enabled) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(mDisableVerboseLogsFlag.getPath());
+ setFilePermissionsFromMode(mDisableVerboseLogsFlag.getPath(), MODE_WORLD_READABLE);
+ } catch (FileNotFoundException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ try {
+ mDisableVerboseLogsFlag.createNewFile();
+ } catch (IOException e1) {
+ Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+ } else {
+ mDisableVerboseLogsFlag.delete();
+ }
+ return (enabled == mDisableVerboseLogsFlag.exists());
+ });
+
+ SwitchPreference prefModulesLogs = findPreference("disable_modules_log");
+ Objects.requireNonNull(prefModulesLogs).setChecked(mDisableModulesLogsFlag.exists());
+ prefModulesLogs.setOnPreferenceChangeListener((preference, newValue) -> {
+ boolean enabled = (Boolean) newValue;
+ if (enabled) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(mDisableModulesLogsFlag.getPath());
+ setFilePermissionsFromMode(mDisableModulesLogsFlag.getPath(), MODE_WORLD_READABLE);
+ } catch (FileNotFoundException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ try {
+ mDisableModulesLogsFlag.createNewFile();
+ } catch (IOException e1) {
+ Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+ } else {
+ mDisableModulesLogsFlag.delete();
+ }
+ return (enabled == mDisableModulesLogsFlag.exists());
+ });
+
+ SwitchPreference prefBlackWhiteListMode = findPreference("black_white_list_switch");
+ Objects.requireNonNull(prefBlackWhiteListMode).setChecked(mBlackWhiteListModeFlag.exists());
+ prefBlackWhiteListMode.setOnPreferenceChangeListener((preference, newValue) -> {
+ boolean enabled = (Boolean) newValue;
+ if (enabled) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(mBlackWhiteListModeFlag.getPath());
+ setFilePermissionsFromMode(mBlackWhiteListModeFlag.getPath(), MODE_WORLD_READABLE);
+ } catch (FileNotFoundException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ try {
+ mBlackWhiteListModeFlag.createNewFile();
+ } catch (IOException e1) {
+ Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+ } else {
+ mBlackWhiteListModeFlag.delete();
+ }
+ return (enabled == mBlackWhiteListModeFlag.exists());
+ });
+
+ SwitchPreference prefEnableDeopt = findPreference("enable_boot_image_deopt");
+ Objects.requireNonNull(prefEnableDeopt).setChecked(mDeoptBootFlag.exists());
+ prefEnableDeopt.setOnPreferenceChangeListener((preference, newValue) -> {
+ boolean enabled = (Boolean) newValue;
+ if (enabled) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(mDeoptBootFlag.getPath());
+ setFilePermissionsFromMode(mDeoptBootFlag.getPath(), MODE_WORLD_READABLE);
+ } catch (FileNotFoundException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ try {
+ mDeoptBootFlag.createNewFile();
+ } catch (IOException e1) {
+ Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+ } else {
+ mDeoptBootFlag.delete();
+ }
+ return (enabled == mDeoptBootFlag.exists());
+ });
+
+ SwitchPreference prefDynamicResources = findPreference("is_dynamic_modules");
+ Objects.requireNonNull(prefDynamicResources).setChecked(mDynamicModulesFlag.exists());
+ prefDynamicResources.setOnPreferenceChangeListener((preference, newValue) -> {
+ boolean enabled = (Boolean) newValue;
+ if (enabled) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(mDynamicModulesFlag.getPath());
+ setFilePermissionsFromMode(mDynamicModulesFlag.getPath(), MODE_WORLD_READABLE);
+ } catch (FileNotFoundException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ try {
+ mDynamicModulesFlag.createNewFile();
+ } catch (IOException e1) {
+ Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+ } else {
+ mDynamicModulesFlag.delete();
+ }
+ return (enabled == mDynamicModulesFlag.exists());
+ });
+
+ SwitchPreference prefDisableResources = findPreference("disable_resources");
+ Objects.requireNonNull(prefDisableResources).setChecked(mDisableResourcesFlag.exists());
+ prefDisableResources.setOnPreferenceChangeListener((preference, newValue) -> {
+ boolean enabled = (Boolean) newValue;
+ if (enabled) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(mDisableResourcesFlag.getPath());
+ setFilePermissionsFromMode(mDisableResourcesFlag.getPath(), MODE_WORLD_READABLE);
+ } catch (FileNotFoundException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
+ try {
+ mDisableResourcesFlag.createNewFile();
+ } catch (IOException e1) {
+ Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+ } else {
+ mDisableResourcesFlag.delete();
+ }
+ return (enabled == mDisableResourcesFlag.exists());
+ });
+
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (key.contains("theme") || key.equals("ignore_chinese")) {
+ AppCompatDelegate.setDefaultNightMode(XposedApp.getPreferences().getInt("theme", 0));
+ Objects.requireNonNull(getActivity()).recreate();
+ }
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ SettingsActivity act = (SettingsActivity) getActivity();
+ if (act == null)
+ return false;
+
+ if (preference.getKey().equals(stopVerboseLog.getKey())) {
+ new Runnable() {
+ @Override
+ public void run() {
+ areYouSure(R.string.stop_verbose_log_summary, (dialog, which) -> {
+
+ Shell.su("kill $(cat " + mVerboseLogProcessID.getAbsolutePath() + ")").exec();
+
+ });
+ }
+ };
+ } else if (preference.getKey().equals(stopLog.getKey())) {
+ new Runnable() {
+ @Override
+ public void run() {
+ areYouSure(R.string.stop_log_summary, (dialog, which) -> Shell.su("kill $(cat " + mModulesLogProcessID.getAbsolutePath() + ")").exec());
+ }
+ };
+ }
+ return true;
+ }
+
+ private void areYouSure(int contentTextId, DialogInterface.OnClickListener listener) {
+ new MaterialAlertDialogBuilder(Objects.requireNonNull(getActivity())).setTitle(R.string.areyousure)
+ .setMessage(contentTextId)
+ .setPositiveButton(android.R.string.yes, listener)
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+ }
+
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/StatusInstallerFragment.java b/app/src/main/java/org/meowcat/edxposed/manager/StatusInstallerFragment.java
new file mode 100644
index 00000000..c7af6264
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/StatusInstallerFragment.java
@@ -0,0 +1,327 @@
+package org.meowcat.edxposed.manager;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.FileUtils;
+import android.text.Html;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Objects;
+
+@SuppressLint("StaticFieldLeak")
+public class StatusInstallerFragment extends Fragment {
+
+ public static final File DISABLE_FILE = new File(XposedApp.BASE_DIR + "conf/disabled");
+ private static Activity sActivity;
+ private static String mUpdateLink;
+ private static View mUpdateView;
+ private static View mUpdateButton;
+
+ static void setUpdate(final String link, final String changelog, Context mContext) {
+ mUpdateLink = link;
+
+ mUpdateView.setVisibility(View.VISIBLE);
+ mUpdateButton.setVisibility(View.VISIBLE);
+ mUpdateButton.setOnClickListener(v -> new MaterialAlertDialogBuilder(sActivity)
+ .setTitle(R.string.changes)
+ .setMessage(Html.fromHtml(changelog))
+ .setPositiveButton(R.string.update, (dialog, which) -> update(mContext))
+ .setNegativeButton(R.string.later, null).show());
+ }
+
+ private static void update(Context mContext) {
+ Uri uri = Uri.parse(mUpdateLink);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ mContext.startActivity(intent);
+ }
+
+ private static String getCompleteArch() {
+ String info = "";
+
+ try {
+ FileReader fr = new FileReader("/proc/cpuinfo");
+ BufferedReader br = new BufferedReader(fr);
+ String text;
+ while ((text = br.readLine()) != null) {
+ if (!text.startsWith("processor")) break;
+ }
+ br.close();
+ String[] array = text != null ? text.split(":\\s+", 2) : new String[0];
+ if (array.length >= 2) {
+ info += array[1] + " ";
+ }
+ } catch (IOException ignored) {
+ }
+
+ info += Build.SUPPORTED_ABIS[0];
+ return info + " (" + getArch() + ")";
+ }
+
+ private static String getArch() {
+ if (Build.CPU_ABI.equals("arm64-v8a")) {
+ return "arm64";
+ } else if (Build.CPU_ABI.equals("x86_64")) {
+ return "x86_64";
+ } else if (Build.CPU_ABI.equals("mips64")) {
+ return "mips64";
+ } else if (Build.CPU_ABI.startsWith("x86") || Build.CPU_ABI2.startsWith("x86")) {
+ return "x86";
+ } else if (Build.CPU_ABI.startsWith("mips")) {
+ return "mips";
+ } else if (Build.CPU_ABI.startsWith("armeabi-v5") || Build.CPU_ABI.startsWith("armeabi-v6")) {
+ return "armv5";
+ } else {
+ return "arm";
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ @SuppressLint({"WorldReadableFiles", "WorldWriteableFiles"})
+ private static void setFilePermissionsFromMode(String name, int mode) {
+ int perms = FileUtils.S_IRUSR | FileUtils.S_IWUSR
+ | FileUtils.S_IRGRP | FileUtils.S_IWGRP;
+ if ((mode & Context.MODE_WORLD_READABLE) != 0) {
+ perms |= FileUtils.S_IROTH;
+ }
+ if ((mode & Context.MODE_WORLD_WRITEABLE) != 0) {
+ perms |= FileUtils.S_IWOTH;
+ }
+ FileUtils.setPermissions(name, perms, -1, -1);
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ sActivity = getActivity();
+ }
+
+ @SuppressLint("WorldReadableFiles")
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.status_installer, container, false);
+
+ mUpdateView = v.findViewById(R.id.updateView);
+ mUpdateButton = v.findViewById(R.id.click_to_update);
+
+
+ String installedXposedVersion;
+ try {
+ installedXposedVersion = XposedApp.getXposedProp().getVersion();
+ } catch (NullPointerException e) {
+ installedXposedVersion = null;
+ }
+
+ TextView api = v.findViewById(R.id.api);
+ TextView framework = v.findViewById(R.id.framework);
+ TextView manager = v.findViewById(R.id.manager);
+ TextView androidSdk = v.findViewById(R.id.android_version);
+ TextView manufacturer = v.findViewById(R.id.ic_manufacturer);
+ TextView cpu = v.findViewById(R.id.cpu);
+
+ String mAppVer = "v" + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")";
+ manager.setText(mAppVer);
+ if (installedXposedVersion != null) {
+ int installedXposedVersionInt = extractIntPart(installedXposedVersion);
+ String installedXposedVersionStr = installedXposedVersionInt + ".0";
+ api.setText(installedXposedVersionStr);
+ framework.setText(installedXposedVersion.replace(installedXposedVersionStr + "-", ""));
+ }
+
+ androidSdk.setText(getString(R.string.android_sdk, getAndroidVersion(), Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
+ manufacturer.setText(getUIFramework());
+ cpu.setText(getCompleteArch());
+
+ determineVerifiedBootState(v);
+
+ refreshKnownIssue();
+ return v;
+ }
+
+ private void determineVerifiedBootState(View v) {
+ try {
+ @SuppressLint("PrivateApi") Class> c = Class.forName("android.os.SystemProperties");
+ Method m = c.getDeclaredMethod("get", String.class, String.class);
+ m.setAccessible(true);
+
+ String propSystemVerified = (String) m.invoke(null, "partition.system.verified", "0");
+ String propState = (String) m.invoke(null, "ro.boot.verifiedbootstate", "");
+ File fileDmVerityModule = new File("/sys/module/dm_verity");
+
+ boolean verified = !propSystemVerified.equals("0");
+ boolean detected = !propState.isEmpty() || fileDmVerityModule.exists();
+
+ TextView tv = v.findViewById(R.id.dmverity);
+ if (verified) {
+ tv.setText(R.string.verified_boot_active);
+ tv.setTextColor(getResources().getColor(R.color.warning));
+ } else if (detected) {
+ tv.setText(R.string.verified_boot_deactivated);
+ v.findViewById(R.id.dmverity_explanation).setVisibility(View.GONE);
+ } else {
+ tv.setText(R.string.verified_boot_none);
+ tv.setTextColor(getResources().getColor(R.color.warning));
+ v.findViewById(R.id.dmverity_explanation).setVisibility(View.GONE);
+ }
+ } catch (Exception e) {
+ Log.e(XposedApp.TAG, "Could not detect Verified Boot state", e);
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private boolean checkAppInstalled(Context context, String pkgName) {
+ if (pkgName == null || pkgName.isEmpty()) {
+ return false;
+ }
+ final PackageManager packageManager = context.getPackageManager();
+ List info = packageManager.getInstalledPackages(0);
+ if (info == null || info.isEmpty()) {
+ return false;
+ }
+ for (int i = 0; i < info.size(); i++) {
+ if (pkgName.equals(info.get(i).packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @SuppressLint("StringFormatInvalid")
+ private void refreshKnownIssue() {
+ String issueName = null;
+ String issueLink = null;
+ final ApplicationInfo appInfo = Objects.requireNonNull(getActivity()).getApplicationInfo();
+ final File baseDir = new File(XposedApp.BASE_DIR);
+ final File baseDirCanonical = getCanonicalFile(baseDir);
+ final File baseDirActual = new File(Build.VERSION.SDK_INT >= 24 ? appInfo.deviceProtectedDataDir : appInfo.dataDir);
+ final File baseDirActualCanonical = getCanonicalFile(baseDirActual);
+
+ if (new File("/system/framework/core.jar.jex").exists()) {
+ issueName = "Aliyun OS";
+ issueLink = "https://forum.xda-developers.com/showpost.php?p=52289793&postcount=5";
+// } else if (Build.VERSION.SDK_INT < 24 && (new File("/data/miui/DexspyInstaller.jar").exists() || checkClassExists("miui.dexspy.DexspyInstaller"))) {
+// issueName = "MIUI/Dexspy";
+// issueLink = "https://forum.xda-developers.com/showpost.php?p=52291098&postcount=6";
+// } else if (Build.VERSION.SDK_INT < 24 && new File("/system/framework/twframework.jar").exists()) {
+// issueName = "Samsung TouchWiz ROM";
+// issueLink = "https://forum.xda-developers.com/showthread.php?t=3034811";
+ } else if (!baseDirCanonical.equals(baseDirActualCanonical)) {
+ Log.e(XposedApp.TAG, "Base directory: " + getPathWithCanonicalPath(baseDir, baseDirCanonical));
+ Log.e(XposedApp.TAG, "Expected: " + getPathWithCanonicalPath(baseDirActual, baseDirActualCanonical));
+ issueName = getString(R.string.known_issue_wrong_base_directory, getPathWithCanonicalPath(baseDirActual, baseDirActualCanonical));
+ } else if (!baseDir.exists()) {
+ issueName = getString(R.string.known_issue_missing_base_directory);
+ issueLink = "https://github.com/rovo89/XposedInstaller/issues/393";
+ } else if (checkAppInstalled(getContext(), "com.solohsu.android.edxp.manager")) {
+ issueName = getString(R.string.edxp_installer_installed);
+ issueLink = getString(R.string.about_support);
+ }
+
+ }
+
+ private String getAndroidVersion() {
+ switch (Build.VERSION.SDK_INT) {
+// case 16:
+// case 17:
+// case 18:
+// return "Jelly Bean";
+// case 19:
+// return "KitKat";
+ case 21:
+ case 22:
+ return "Lollipop";
+ case 23:
+ return "Marshmallow";
+ case 24:
+ case 25:
+ return "Nougat";
+ case 26:
+ case 27:
+ return "Oreo";
+ case 28:
+ return "Pie";
+ case 29:
+ return "Q";
+ case 30:
+ return "R";
+ }
+ return "Unknown";
+ }
+
+ private String getUIFramework() {
+ String manufacturer = Character.toUpperCase(Build.MANUFACTURER.charAt(0)) + Build.MANUFACTURER.substring(1);
+ if (!Build.BRAND.equals(Build.MANUFACTURER)) {
+ manufacturer += " " + Character.toUpperCase(Build.BRAND.charAt(0)) + Build.BRAND.substring(1);
+ }
+ manufacturer += " " + Build.MODEL + " ";
+ if (new File("/system/framework/twframework.jar").exists() || new File("/system/framework/samsung-services.jar").exists()) {
+ manufacturer += "(TouchWiz)";
+ } else if (new File("/system/framework/framework-miui-res.apk").exists() || new File("/system/app/miui/miui.apk").exists() || new File("/system/app/miuisystem/miuisystem.apk").exists()) {
+ manufacturer += "(Mi UI)";
+ } else if (new File("/system/priv-app/oneplus-framework-res/oneplus-framework-res.apk").exists()) {
+ manufacturer += "(Oxygen/Hydrogen OS)";
+ } else if (new File("/system/framework/com.samsung.device.jar").exists() || new File("/system/framework/sec_platform_library.jar").exists()) {
+ manufacturer += "(One UI)";
+ }
+ /*if (manufacturer.contains("Samsung")) {
+ manufacturer += new File("/system/framework/twframework.jar").exists() ||
+ new File("/system/framework/samsung-services.jar").exists()
+ ? "(TouchWiz)" : "(AOSP-based ROM)";
+ } else if (manufacturer.contains("Xiaomi")) {
+ manufacturer += new File("/system/framework/framework-miui-res.apk").exists() ? "(MIUI)" : "(AOSP-based ROM)";
+ }*/
+ return manufacturer;
+ }
+
+ private File getCanonicalFile(File file) {
+ try {
+ return file.getCanonicalFile();
+ } catch (IOException e) {
+ Log.e(XposedApp.TAG, "Failed to get canonical file for " + file.getAbsolutePath(), e);
+ return file;
+ }
+ }
+
+ private String getPathWithCanonicalPath(File file, File canonical) {
+ if (file.equals(canonical)) {
+ return file.getAbsolutePath();
+ } else {
+ return file.getAbsolutePath() + " \u2192 " + canonical.getAbsolutePath();
+ }
+ }
+
+ private int extractIntPart(String str) {
+ int result = 0, length = str.length();
+ for (int offset = 0; offset < length; offset++) {
+ char c = str.charAt(offset);
+ if ('0' <= c && c <= '9')
+ result = result * 10 + (c - '0');
+ else
+ break;
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/XposedApp.java b/app/src/main/java/org/meowcat/edxposed/manager/XposedApp.java
new file mode 100644
index 00000000..6c976a47
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/XposedApp.java
@@ -0,0 +1,244 @@
+package org.meowcat.edxposed.manager;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Application;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import org.meowcat.edxposed.manager.receivers.PackageChangeReceiver;
+import org.meowcat.edxposed.manager.util.ModuleUtil;
+import org.meowcat.edxposed.manager.util.NotificationUtil;
+import org.meowcat.edxposed.manager.util.RepoLoader;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Objects;
+
+import de.robv.android.xposed.installer.util.InstallZipUtil;
+
+public class XposedApp extends de.robv.android.xposed.installer.XposedApp implements Application.ActivityLifecycleCallbacks {
+ public static final String TAG = "EdXposedManager";
+ @SuppressLint("SdCardPath")
+ private static final String BASE_DIR_LEGACY = "/data/data/" + BuildConfig.APPLICATION_ID + "/";
+ public static final String BASE_DIR = Build.VERSION.SDK_INT >= 24
+ ? "/data/user_de/0/" + BuildConfig.APPLICATION_ID + "/" : BASE_DIR_LEGACY;
+ public static final String ENABLED_MODULES_LIST_FILE = (Build.VERSION.SDK_INT >= 24
+ ? "/data/user_de/0/" + BuildConfig.APPLICATION_ID + "/" : BASE_DIR_LEGACY) + "conf/enabled_modules.list";
+ public static int WRITE_EXTERNAL_PERMISSION = 69;
+ @SuppressLint("StaticFieldLeak")
+ private static XposedApp mInstance = null;
+ private static Thread mUiThread;
+ private static Handler mMainHandler;
+ private SharedPreferences mPref;
+ private Activity mCurrentActivity = null;
+ private boolean mIsUiLoaded = false;
+
+ public static XposedApp getInstance() {
+ return mInstance;
+ }
+
+ public static InstallZipUtil.XposedProp getXposedProp() {
+ return de.robv.android.xposed.installer.XposedApp.getInstance().mXposedProp;
+ }
+
+ public static void runOnUiThread(Runnable action) {
+ if (Thread.currentThread() != mUiThread) {
+ mMainHandler.post(action);
+ } else {
+ action.run();
+ }
+ }
+
+ public static File createFolder() {
+ File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Download/EdXposedManager/");
+
+ if (!dir.exists()) dir.mkdir();
+
+ return dir;
+ }
+
+// public static void postOnUiThread(Runnable action) {
+// mMainHandler.post(action);
+// }
+
+ public static Integer getXposedVersion() {
+ return getActiveXposedVersion();
+ }
+
+ public static SharedPreferences getPreferences() {
+ return mInstance.mPref;
+ }
+
+ public static int getColor(Context context) {
+ SharedPreferences prefs = context.getSharedPreferences(context.getPackageName() + "_preferences", MODE_PRIVATE);
+ int defaultColor = context.getResources().getColor(R.color.colorPrimary);
+
+ return prefs.getInt("colors", defaultColor);
+ }
+
+ public static String getDownloadPath() {
+ return getPreferences().getString("download_location", Environment.getExternalStorageDirectory() + "/Download/EdXposedManager/");
+ }
+
+ public static void mkdirAndChmod(String dir, int permissions) {
+ dir = BASE_DIR + dir;
+ //noinspection ResultOfMethodCallIgnored
+ new File(dir).mkdir();
+ FileUtils.setPermissions(dir, permissions, -1, -1);
+ }
+
+ public void onCreate() {
+ super.onCreate();
+ mInstance = this;
+ mUiThread = Thread.currentThread();
+ mMainHandler = new Handler();
+
+ mPref = PreferenceManager.getDefaultSharedPreferences(this);
+
+ de.robv.android.xposed.installer.XposedApp.getInstance().reloadXposedProp();
+ createDirectories();
+ delete(new File(Environment.getExternalStorageDirectory() + "/Download/EdXposedManager/.temp"));
+ NotificationUtil.init();
+ registerReceivers();
+
+ registerActivityLifecycleCallbacks(this);
+
+ @SuppressLint("SimpleDateFormat") DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
+ Date date = new Date();
+
+ if (!Objects.requireNonNull(mPref.getString("date", "")).equals(dateFormat.format(date))) {
+ mPref.edit().putString("date", dateFormat.format(date)).apply();
+
+ try {
+ Log.i(TAG, String.format("EdXposedManager - %s - %s", BuildConfig.VERSION_CODE, getPackageManager().getPackageInfo(getPackageName(), 0).versionName));
+ } catch (PackageManager.NameNotFoundException ignored) {
+ }
+ }
+
+ if (mPref.getBoolean("force_english", false)) {
+ Resources res = getResources();
+ DisplayMetrics dm = res.getDisplayMetrics();
+ android.content.res.Configuration conf = res.getConfiguration();
+ conf.locale = Locale.ENGLISH;
+ res.updateConfiguration(conf, dm);
+ }
+
+ RepoLoader.getInstance().triggerFirstLoadIfNecessary();
+ }
+
+ private void registerReceivers() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addDataScheme("package");
+ registerReceiver(new PackageChangeReceiver(), filter);
+
+ PendingIntent.getBroadcast(this, 0,
+ new Intent(this, PackageChangeReceiver.class), 0);
+ }
+
+ private void delete(File file) {
+ if (file != null) {
+ if (file.isDirectory()) {
+ File[] files = file.listFiles();
+ if (files != null) for (File f : file.listFiles()) delete(f);
+ }
+ file.delete();
+ }
+ }
+
+ @SuppressWarnings("JavaReflectionMemberAccess")
+ @SuppressLint({"PrivateApi", "NewApi"})
+ private void createDirectories() {
+ //FileUtils.setPermissions(BASE_DIR, 00777, -1, -1);
+ mkdirAndChmod("conf", 00777);
+ mkdirAndChmod("log", 00777);
+
+ if (Build.VERSION.SDK_INT >= 24) {
+ try {
+ @SuppressLint("SoonBlockedPrivateApi") Method deleteDir = FileUtils.class.getDeclaredMethod("deleteContentsAndDir", File.class);
+ deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "bin"));
+ deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "conf"));
+ deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "log"));
+ } catch (ReflectiveOperationException e) {
+ Log.w(de.robv.android.xposed.installer.XposedApp.TAG, "Failed to delete obsolete directories", e);
+ }
+ }
+ }
+
+ public void updateProgressIndicator(final SwipeRefreshLayout refreshLayout) {
+ final boolean isLoading = RepoLoader.getInstance().isLoading() || ModuleUtil.getInstance().isLoading();
+ runOnUiThread(() -> {
+ synchronized (XposedApp.this) {
+ if (mCurrentActivity != null) {
+ mCurrentActivity.setProgressBarIndeterminateVisibility(isLoading);
+ if (refreshLayout != null)
+ refreshLayout.setRefreshing(isLoading);
+ }
+ }
+ });
+ }
+
+ @Override
+ public synchronized void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {
+ if (mIsUiLoaded)
+ return;
+
+ RepoLoader.getInstance().triggerFirstLoadIfNecessary();
+ mIsUiLoaded = true;
+ }
+
+ @Override
+ public void onActivityStarted(@NonNull Activity activity) {
+
+ }
+
+ @Override
+ public synchronized void onActivityResumed(@NonNull Activity activity) {
+ mCurrentActivity = activity;
+ updateProgressIndicator(null);
+ }
+
+ @Override
+ public synchronized void onActivityPaused(Activity activity) {
+ activity.setProgressBarIndeterminateVisibility(false);
+ mCurrentActivity = null;
+ }
+
+ @Override
+ public void onActivityStopped(@NonNull Activity activity) {
+
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
+
+ }
+
+ @Override
+ public void onActivityDestroyed(@NonNull Activity activity) {
+
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppAdapter.java b/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppAdapter.java
new file mode 100644
index 00000000..b71983d2
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppAdapter.java
@@ -0,0 +1,271 @@
+package org.meowcat.edxposed.manager.adapters;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.Filter;
+import android.widget.ImageView;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.meowcat.edxposed.manager.R;
+import org.meowcat.edxposed.manager.XposedApp;
+import org.meowcat.edxposed.manager.util.InstallApkUtil;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class AppAdapter extends RecyclerView.Adapter {
+
+ protected final Context context;
+ private final ApplicationInfo.DisplayNameComparator displayNameComparator;
+ public Callback callback;
+ private List fullList, showList;
+ private DateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
+ private List checkedList;
+ private PackageManager pm;
+ private ApplicationFilter filter;
+ private Comparator cmp;
+
+ AppAdapter(Context context) {
+ this.context = context;
+ fullList = showList = Collections.emptyList();
+ checkedList = Collections.emptyList();
+ filter = new ApplicationFilter();
+ pm = context.getPackageManager();
+ displayNameComparator = new ApplicationInfo.DisplayNameComparator(pm);
+ cmp = displayNameComparator;
+ refresh();
+ }
+
+ public void setCallback(Callback callback) {
+ this.callback = callback;
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(context).inflate(R.layout.item_app, parent, false);
+ return new ViewHolder(v);
+ }
+
+ private void loadApps() {
+ fullList = pm.getInstalledApplications(PackageManager.GET_META_DATA);
+ if (!XposedApp.getPreferences().getBoolean("show_modules", true)) {
+ List rmList = new ArrayList<>();
+ for (ApplicationInfo info : fullList) {
+ if (info.metaData != null && info.metaData.containsKey("xposedmodule") || AppHelper.FORCE_WHITE_LIST_MODULE.contains(info.packageName)) {
+ rmList.add(info);
+ }
+ }
+ if (rmList.size() > 0) {
+ fullList.removeAll(rmList);
+ }
+ }
+ AppHelper.makeSurePath();
+ checkedList = generateCheckedList();
+ sortApps();
+ if (callback != null) {
+ callback.onDataReady();
+ }
+ }
+
+ /**
+ * Called during {@link #loadApps()} in non-UI thread.
+ *
+ * @return list of package names which should be checked when shown
+ */
+ protected List generateCheckedList() {
+ return Collections.emptyList();
+ }
+
+ private void sortApps() {
+ switch (XposedApp.getPreferences().getInt("list_sort", 0)) {
+ case 7:
+ cmp = Collections.reverseOrder((ApplicationInfo a, ApplicationInfo b) -> {
+ try {
+ return Long.compare(pm.getPackageInfo(a.packageName, 0).lastUpdateTime, pm.getPackageInfo(b.packageName, 0).lastUpdateTime);
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ return displayNameComparator.compare(a, b);
+ }
+ });
+ break;
+ case 6:
+ cmp = (ApplicationInfo a, ApplicationInfo b) -> {
+ try {
+ return Long.compare(pm.getPackageInfo(a.packageName, 0).lastUpdateTime, pm.getPackageInfo(b.packageName, 0).lastUpdateTime);
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ return displayNameComparator.compare(a, b);
+ }
+ };
+ break;
+ case 5:
+ cmp = Collections.reverseOrder((ApplicationInfo a, ApplicationInfo b) -> {
+ try {
+ return Long.compare(pm.getPackageInfo(a.packageName, 0).firstInstallTime, pm.getPackageInfo(b.packageName, 0).firstInstallTime);
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ return displayNameComparator.compare(a, b);
+ }
+ });
+ break;
+ case 4:
+ cmp = (ApplicationInfo a, ApplicationInfo b) -> {
+ try {
+ return Long.compare(pm.getPackageInfo(a.packageName, 0).firstInstallTime, pm.getPackageInfo(b.packageName, 0).firstInstallTime);
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ return displayNameComparator.compare(a, b);
+ }
+ };
+ break;
+ case 3:
+ cmp = Collections.reverseOrder((a, b) -> a.packageName.compareTo(b.packageName));
+ break;
+ case 2:
+ cmp = (a, b) -> a.packageName.compareTo(b.packageName);
+ break;
+ case 1:
+ cmp = Collections.reverseOrder(displayNameComparator);
+ break;
+ case 0:
+ default:
+ cmp = displayNameComparator;
+ break;
+ }
+ Collections.sort(fullList, (a, b) -> {
+ if (XposedApp.getPreferences().getBoolean("enabled_top", true)) {
+ boolean aChecked = checkedList.contains(a.packageName);
+ boolean bChecked = checkedList.contains(b.packageName);
+ if (aChecked == bChecked) {
+ return cmp.compare(a, b);
+ } else if (aChecked) {
+ return -1;
+ } else {
+ return 1;
+ }
+ } else {
+ return cmp.compare(a, b);
+ }
+ });
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ ApplicationInfo info = showList.get(position);
+ holder.appIcon.setImageDrawable(info.loadIcon(pm));
+ holder.appName.setText(InstallApkUtil.getAppLabel(info, pm));
+ try {
+ holder.appVersion.setText(pm.getPackageInfo(info.packageName, 0).versionName);
+ holder.appInstallTime.setText(dateformat.format(new Date(pm.getPackageInfo(info.packageName, 0).firstInstallTime)));
+ holder.appUpdateTime.setText(dateformat.format(new Date(pm.getPackageInfo(info.packageName, 0).lastUpdateTime)));
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ holder.appPackage.setText(info.packageName);
+ //holder.appPackage.setTextColor(ThemeUtil.getThemeColor(context, android.R.attr.textColorSecondary));
+
+ holder.mSwitch.setOnCheckedChangeListener(null);
+ holder.mSwitch.setChecked(checkedList.contains(info.packageName));
+ holder.mSwitch.setOnCheckedChangeListener((v, isChecked) ->
+ onCheckedChange(v, isChecked, info));
+ holder.infoLayout.setOnClickListener(v -> {
+ if (callback != null) {
+ callback.onItemClick(v, info);
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return showList.size();
+ }
+
+ public void filter(String constraint) {
+ filter.filter(constraint);
+ }
+
+ public void refresh() {
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(this::loadApps);
+ }
+
+ protected void onCheckedChange(CompoundButton buttonView, boolean isChecked, ApplicationInfo info) {
+ // override this to implements your functions
+ }
+
+ public interface Callback {
+ void onDataReady();
+
+ void onItemClick(View v, ApplicationInfo info);
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+
+ View infoLayout;
+ ImageView appIcon;
+ TextView appName;
+ TextView appPackage;
+ TextView appVersion;
+ TextView appInstallTime;
+ TextView appUpdateTime;
+ Switch mSwitch;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ infoLayout = itemView.findViewById(R.id.info_layout);
+ appIcon = itemView.findViewById(R.id.app_icon);
+ appName = itemView.findViewById(R.id.app_name);
+ appPackage = itemView.findViewById(R.id.package_name);
+ appVersion = itemView.findViewById(R.id.version_name);
+ appInstallTime = itemView.findViewById(R.id.tvInstallTime);
+ appUpdateTime = itemView.findViewById(R.id.tvUpdateTime);
+ mSwitch = itemView.findViewById(R.id.checkbox);
+ }
+ }
+
+ class ApplicationFilter extends Filter {
+
+ private boolean lowercaseContains(String s, CharSequence filter) {
+ return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter);
+ }
+
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ if (constraint == null || constraint.length() == 0) {
+ showList = fullList;
+ } else {
+ showList = new ArrayList<>();
+ String filter = constraint.toString().toLowerCase();
+ for (ApplicationInfo info : fullList) {
+ if (lowercaseContains(InstallApkUtil.getAppLabel(info, pm), filter)
+ || lowercaseContains(info.packageName, filter)) {
+ showList.add(info);
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ notifyDataSetChanged();
+ }
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppHelper.java b/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppHelper.java
new file mode 100644
index 00000000..a4c860fa
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppHelper.java
@@ -0,0 +1,362 @@
+package org.meowcat.edxposed.manager.adapters;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.net.Uri;
+import android.os.FileUtils;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.view.menu.MenuBuilder;
+import androidx.appcompat.view.menu.MenuPopupHelper;
+import androidx.appcompat.widget.PopupMenu;
+import androidx.fragment.app.FragmentManager;
+
+import org.meowcat.edxposed.manager.BuildConfig;
+import org.meowcat.edxposed.manager.R;
+import org.meowcat.edxposed.manager.XposedApp;
+import org.meowcat.edxposed.manager.util.CompileUtil;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
+
+@SuppressWarnings("deprecation")
+public class AppHelper {
+
+ public static final String TAG = XposedApp.TAG;
+
+ private static final String BASE_PATH = XposedApp.BASE_DIR;
+ private static final String WHITE_LIST_PATH = "conf/whitelist/";
+ private static final String BLACK_LIST_PATH = "conf/blacklist/";
+ private static final String COMPAT_LIST_PATH = "conf/compatlist/";
+ private static final String WHITE_LIST_MODE = "conf/usewhitelist";
+ private static final String BLACK_LIST_MODE = "conf/blackwhitelist";
+
+ private static final List FORCE_WHITE_LIST = new ArrayList<>(Collections.singletonList(BuildConfig.APPLICATION_ID));
+ private static final List SAFETYNET_BLACK_LIST = new ArrayList<>(Arrays.asList("com.google.android.gms", "com.google.android.gsf"));
+ static List FORCE_WHITE_LIST_MODULE = new ArrayList<>(FORCE_WHITE_LIST);
+
+ @SuppressWarnings("OctalInteger")
+ static void makeSurePath() {
+ XposedApp.mkdirAndChmod(WHITE_LIST_PATH, 00777);
+ XposedApp.mkdirAndChmod(BLACK_LIST_PATH, 00777);
+ XposedApp.mkdirAndChmod(COMPAT_LIST_PATH, 00777);
+ }
+
+ public static boolean isWhiteListMode() {
+ return new File(BASE_PATH + WHITE_LIST_MODE).exists();
+ }
+
+ public static boolean isBlackListMode() {
+ return new File(BASE_PATH + BLACK_LIST_MODE).exists();
+ }
+
+ private static boolean addWhiteList(String packageName) {
+ if (SAFETYNET_BLACK_LIST.contains(packageName)) {
+ if (XposedApp.getPreferences().getBoolean("pass_safetynet", false)) {
+ removeWhiteList(packageName);
+ return false;
+ }
+ }
+ return whiteListFileName(packageName, true);
+ }
+
+ private static boolean addBlackList(String packageName) {
+ if (FORCE_WHITE_LIST_MODULE.contains(packageName)) {
+ removeBlackList(packageName);
+ return false;
+ }
+ return blackListFileName(packageName, true);
+ }
+
+ private static boolean removeWhiteList(String packageName) {
+ if (FORCE_WHITE_LIST_MODULE.contains(packageName)) {
+ return false;
+ }
+ return whiteListFileName(packageName, false);
+ }
+
+ private static boolean removeBlackList(String packageName) {
+ if (SAFETYNET_BLACK_LIST.contains(packageName)) {
+ if (XposedApp.getPreferences().getBoolean("pass_safetynet", false)) {
+ return false;
+ }
+ }
+ return blackListFileName(packageName, false);
+ }
+
+ static List getBlackList() {
+ File file = new File(BASE_PATH + BLACK_LIST_PATH);
+ File[] files = file.listFiles();
+ if (files == null) {
+ return new ArrayList<>();
+ }
+ List s = new ArrayList<>();
+ for (File file1 : files) {
+ if (!file1.isDirectory()) {
+ s.add(file1.getName());
+ }
+ }
+ for (String pn : FORCE_WHITE_LIST_MODULE) {
+ if (s.contains(pn)) {
+ s.remove(pn);
+ removeBlackList(pn);
+ }
+ }
+ if (XposedApp.getPreferences().getBoolean("pass_safetynet", false)) {
+ for (String pn : SAFETYNET_BLACK_LIST) {
+ if (!s.contains(pn)) {
+ s.add(pn);
+ addBlackList(pn);
+ }
+ }
+ }
+ return s;
+ }
+
+ static List getWhiteList() {
+ File file = new File(BASE_PATH + WHITE_LIST_PATH);
+ File[] files = file.listFiles();
+ if (files == null) {
+ return FORCE_WHITE_LIST_MODULE;
+ }
+ List result = new ArrayList<>();
+ for (File file1 : files) {
+ result.add(file1.getName());
+ }
+ for (String pn : FORCE_WHITE_LIST_MODULE) {
+ if (!result.contains(pn)) {
+ result.add(pn);
+ addWhiteList(pn);
+ }
+ }
+ if (XposedApp.getPreferences().getBoolean("pass_safetynet", false)) {
+ for (String pn : SAFETYNET_BLACK_LIST) {
+ if (result.contains(pn)) {
+ result.remove(pn);
+ removeWhiteList(pn);
+ }
+ }
+ }
+ return result;
+ }
+
+ @SuppressLint("WorldReadableFiles")
+ private static Boolean whiteListFileName(String packageName, boolean isAdd) {
+ boolean returns = true;
+ File file = new File(BASE_PATH + WHITE_LIST_PATH + packageName);
+ if (isAdd) {
+ if (!file.exists()) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(file.getPath());
+ setFilePermissionsFromMode(file.getPath(), Context.MODE_WORLD_READABLE);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ try {
+ returns = file.createNewFile();
+ } catch (IOException e1) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+ } else {
+ if (file.exists()) {
+ returns = file.delete();
+ }
+ }
+ return returns;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ @SuppressLint({"WorldReadableFiles", "WorldWriteableFiles"})
+ private static void setFilePermissionsFromMode(String name, int mode) {
+ int perms = FileUtils.S_IRUSR | FileUtils.S_IWUSR
+ | FileUtils.S_IRGRP | FileUtils.S_IWGRP;
+ if ((mode & Context.MODE_WORLD_READABLE) != 0) {
+ perms |= FileUtils.S_IROTH;
+ }
+ if ((mode & Context.MODE_WORLD_WRITEABLE) != 0) {
+ perms |= FileUtils.S_IWOTH;
+ }
+ FileUtils.setPermissions(name, perms, -1, -1);
+ }
+
+ @SuppressLint("WorldReadableFiles")
+ private static Boolean blackListFileName(String packageName, boolean isAdd) {
+ boolean returns = true;
+ File file = new File(BASE_PATH + BLACK_LIST_PATH + packageName);
+ if (isAdd) {
+ if (!file.exists()) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(file.getPath());
+ setFilePermissionsFromMode(file.getPath(), Context.MODE_WORLD_READABLE);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ try {
+ returns = file.createNewFile();
+ } catch (IOException e1) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+ } else {
+ if (file.exists()) {
+ returns = file.delete();
+ }
+ }
+ return returns;
+ }
+
+ @SuppressLint("WorldReadableFiles")
+ private static Boolean compatListFileName(String packageName, boolean isAdd) {
+ boolean returns = true;
+ File file = new File(BASE_PATH + COMPAT_LIST_PATH + packageName);
+ if (isAdd) {
+ if (!file.exists()) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(file.getPath());
+ setFilePermissionsFromMode(file.getPath(), Context.MODE_WORLD_READABLE);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ try {
+ returns = file.createNewFile();
+ } catch (IOException e1) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+ } else {
+ if (file.exists()) {
+ returns = file.delete();
+ }
+ }
+ return returns;
+ }
+
+ static boolean addPackageName(boolean isWhiteListMode, String packageName) {
+ return isWhiteListMode ? addWhiteList(packageName) : addBlackList(packageName);
+ }
+
+ static boolean removePackageName(boolean isWhiteListMode, String packageName) {
+ return isWhiteListMode ? removeWhiteList(packageName) : removeBlackList(packageName);
+ }
+
+ @SuppressLint("RestrictedApi")
+ public static void showMenu(@NonNull Context context,
+ @NonNull FragmentManager fragmentManager,
+ @NonNull View anchor,
+ @NonNull ApplicationInfo info) {
+ PopupMenu appMenu = new PopupMenu(context, anchor);
+ appMenu.inflate(R.menu.menu_app_item);
+ appMenu.setOnMenuItemClickListener(menuItem -> {
+ switch (menuItem.getItemId()) {
+ case R.id.app_menu_launch:
+ Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(info.packageName);
+ if (launchIntent != null) {
+ context.startActivity(launchIntent);
+ } else {
+ Toast.makeText(context, context.getString(R.string.module_no_ui), Toast.LENGTH_LONG).show();
+ }
+ break;
+ case R.id.app_menu_stop:
+ try {
+ ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ Objects.requireNonNull(manager).killBackgroundProcesses(info.packageName);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ break;
+ case R.id.app_menu_compile_speed:
+ CompileUtil.compileSpeed(context, fragmentManager, info);
+ break;
+ case R.id.app_menu_compile_dexopt:
+ CompileUtil.compileDexopt(context, fragmentManager, info);
+ break;
+ case R.id.app_menu_compile_reset:
+ CompileUtil.reset(context, fragmentManager, info);
+ break;
+ case R.id.app_menu_store:
+ Uri uri = Uri.parse("market://details?id=" + info.packageName);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ try {
+ context.startActivity(intent);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ break;
+ case R.id.app_menu_info:
+ context.startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", info.packageName, null)));
+ break;
+ case R.id.app_menu_uninstall:
+ context.startActivity(new Intent(Intent.ACTION_UNINSTALL_PACKAGE, Uri.fromParts("package", info.packageName, null)));
+ break;
+ }
+ return true;
+ });
+ MenuPopupHelper menuHelper = new MenuPopupHelper(context, (MenuBuilder) appMenu.getMenu(), anchor);
+ menuHelper.setForceShowIcon(true);
+ menuHelper.show();
+ }
+
+ static List getCompatList() {
+ File file = new File(BASE_PATH + COMPAT_LIST_PATH);
+ File[] files = file.listFiles();
+ if (files == null) {
+ return new ArrayList<>();
+ }
+ List s = new ArrayList<>();
+ for (File file1 : files) {
+ s.add(file1.getName());
+ }
+ return s;
+ }
+
+ static boolean addCompatList(String packageName) {
+ return compatListFileName(packageName, true);
+ }
+
+ static boolean removeCompatList(String packageName) {
+ return compatListFileName(packageName, false);
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/adapters/BlackListAdapter.java b/app/src/main/java/org/meowcat/edxposed/manager/adapters/BlackListAdapter.java
new file mode 100644
index 00000000..3b33a20b
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/adapters/BlackListAdapter.java
@@ -0,0 +1,63 @@
+package org.meowcat.edxposed.manager.adapters;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.widget.CompoundButton;
+
+import org.meowcat.edxposed.manager.R;
+import org.meowcat.edxposed.manager.XposedApp;
+import org.meowcat.edxposed.manager.util.ModuleUtil;
+import org.meowcat.edxposed.manager.util.ToastUtil;
+
+import java.util.Collection;
+import java.util.List;
+
+
+public class BlackListAdapter extends AppAdapter {
+
+ private volatile boolean isWhiteListMode;
+ private List checkedList;
+
+ public BlackListAdapter(Context context, boolean isWhiteListMode) {
+ super(context);
+ this.isWhiteListMode = isWhiteListMode;
+ }
+
+// public void setWhiteListMode(boolean isWhiteListMode) {
+// this.isWhiteListMode = isWhiteListMode;
+// }
+
+ @Override
+ protected List generateCheckedList() {
+ if (XposedApp.getPreferences().getBoolean("hook_modules", true)) {
+ Collection installedModules = ModuleUtil.getInstance().getModules().values();
+ for (ModuleUtil.InstalledModule info : installedModules) {
+ AppHelper.FORCE_WHITE_LIST_MODULE.add(info.packageName);
+ }
+ }
+ AppHelper.makeSurePath();
+ if (isWhiteListMode) {
+ checkedList = AppHelper.getWhiteList();
+ } else {
+ checkedList = AppHelper.getBlackList();
+ }
+ return checkedList;
+ }
+
+ @Override
+ protected void onCheckedChange(CompoundButton view, boolean isChecked, ApplicationInfo info) {
+ boolean success = isChecked ?
+ AppHelper.addPackageName(isWhiteListMode, info.packageName) :
+ AppHelper.removePackageName(isWhiteListMode, info.packageName);
+ if (success) {
+ if (isChecked) {
+ checkedList.add(info.packageName);
+ } else {
+ checkedList.remove(info.packageName);
+ }
+ } else {
+ ToastUtil.showShortToast(context, R.string.add_package_failed);
+ view.setChecked(!isChecked);
+ }
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/adapters/CursorRecyclerViewAdapter.java b/app/src/main/java/org/meowcat/edxposed/manager/adapters/CursorRecyclerViewAdapter.java
new file mode 100644
index 00000000..1b9499aa
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/adapters/CursorRecyclerViewAdapter.java
@@ -0,0 +1,127 @@
+package org.meowcat.edxposed.manager.adapters;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+public abstract class CursorRecyclerViewAdapter extends RecyclerView.Adapter {
+
+ private Context mContext;
+
+ private Cursor mCursor;
+
+ private boolean mDataValid;
+
+ private int mRowIdColumn;
+
+ private DataSetObserver mDataSetObserver;
+
+ public CursorRecyclerViewAdapter(Context context, Cursor cursor) {
+ mContext = context;
+ mCursor = cursor;
+ mDataValid = cursor != null;
+ mRowIdColumn = mDataValid ? mCursor.getColumnIndex("_id") : -1;
+ mDataSetObserver = new NotifyingDataSetObserver();
+ if (mCursor != null) {
+ mCursor.registerDataSetObserver(mDataSetObserver);
+ }
+ }
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ @Override
+ public int getItemCount() {
+ if (mDataValid && mCursor != null) {
+ return mCursor.getCount();
+ }
+ return 0;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) {
+ return mCursor.getLong(mRowIdColumn);
+ }
+ return 0;
+ }
+
+ @Override
+ public void setHasStableIds(boolean hasStableIds) {
+ super.setHasStableIds(true);
+ }
+
+ public abstract void onBindViewHolder(VH viewHolder, Cursor cursor);
+
+ @Override
+ public void onBindViewHolder(VH viewHolder, int position) {
+ if (!mDataValid) {
+ throw new IllegalStateException("this should only be called when the cursor is valid");
+ }
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("couldn't move cursor to position " + position);
+ }
+ onBindViewHolder(viewHolder, mCursor);
+ }
+
+ /**
+ * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
+ * closed.
+ */
+ public void changeCursor(Cursor cursor) {
+ Cursor old = swapCursor(cursor);
+ if (old != null) {
+ old.close();
+ }
+ }
+
+ /**
+ * Swap in a new Cursor, returning the old Cursor. Unlike
+ * {@link #changeCursor(Cursor)}, the returned old Cursor is not
+ * closed.
+ */
+ public Cursor swapCursor(Cursor newCursor) {
+ if (newCursor == mCursor) {
+ return null;
+ }
+ final Cursor oldCursor = mCursor;
+ if (oldCursor != null && mDataSetObserver != null) {
+ oldCursor.unregisterDataSetObserver(mDataSetObserver);
+ }
+ mCursor = newCursor;
+ if (mCursor != null) {
+ if (mDataSetObserver != null) {
+ mCursor.registerDataSetObserver(mDataSetObserver);
+ }
+ mRowIdColumn = newCursor.getColumnIndexOrThrow("_id");
+ mDataValid = true;
+ notifyDataSetChanged();
+ } else {
+ mRowIdColumn = -1;
+ mDataValid = false;
+ notifyDataSetChanged();
+ //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
+ }
+ return oldCursor;
+ }
+
+ private class NotifyingDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ mDataValid = true;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ mDataValid = false;
+ notifyDataSetChanged();
+ //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/receivers/BootReceiver.java b/app/src/main/java/org/meowcat/edxposed/manager/receivers/BootReceiver.java
new file mode 100644
index 00000000..26e2d906
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/receivers/BootReceiver.java
@@ -0,0 +1,58 @@
+package org.meowcat.edxposed.manager.receivers;
+
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import org.json.JSONObject;
+import org.meowcat.edxposed.manager.BuildConfig;
+import org.meowcat.edxposed.manager.XposedApp;
+import org.meowcat.edxposed.manager.util.NotificationUtil;
+import org.meowcat.edxposed.manager.util.json.JSONUtils;
+
+public class BootReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ new android.os.Handler().postDelayed(() -> {
+ if (!isOnline(context)) return;
+
+ new CheckUpdates().execute();
+ }, 60 * 60 * 1000 /*60 min*/);
+ }
+
+ private boolean isOnline(Context context) {
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo netInfo = cm.getActiveNetworkInfo();
+ return netInfo != null && netInfo.isConnectedOrConnecting();
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private class CheckUpdates extends AsyncTask {
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ String jsonString = JSONUtils.getFileContent(JSONUtils.JSON_LINK).replace("%XPOSED_ZIP%", "");
+
+ String newApkVersion = new JSONObject(jsonString).getJSONObject("apk").getString("version");
+
+ Integer a = BuildConfig.VERSION_CODE;
+ Integer b = Integer.valueOf(newApkVersion);
+
+ if (a.compareTo(b) < 0) {
+ NotificationUtil.showInstallerUpdateNotification();
+ }
+ } catch (Exception e) {
+ Log.d(XposedApp.TAG, e.getMessage());
+ }
+ return null;
+ }
+
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/receivers/DownloadReceiver.java b/app/src/main/java/org/meowcat/edxposed/manager/receivers/DownloadReceiver.java
new file mode 100644
index 00000000..3ad77e57
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/receivers/DownloadReceiver.java
@@ -0,0 +1,19 @@
+package org.meowcat.edxposed.manager.receivers;
+
+import android.app.DownloadManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import org.meowcat.edxposed.manager.util.DownloadsUtil;
+
+public class DownloadReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ String action = intent.getAction();
+ if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
+ long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0);
+ DownloadsUtil.triggerDownloadFinishedCallback(context, downloadId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/receivers/PackageChangeReceiver.java b/app/src/main/java/org/meowcat/edxposed/manager/receivers/PackageChangeReceiver.java
new file mode 100644
index 00000000..791ce376
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/receivers/PackageChangeReceiver.java
@@ -0,0 +1,80 @@
+package org.meowcat.edxposed.manager.receivers;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import org.meowcat.edxposed.manager.util.ModuleUtil;
+import org.meowcat.edxposed.manager.util.ModuleUtil.InstalledModule;
+import org.meowcat.edxposed.manager.util.NotificationUtil;
+
+import java.util.Objects;
+
+public class PackageChangeReceiver extends BroadcastReceiver {
+ private static ModuleUtil mModuleUtil = null;
+
+ private static String getPackageName(Intent intent) {
+ Uri uri = intent.getData();
+ return (uri != null) ? uri.getSchemeSpecificPart() : null;
+ }
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ if (Objects.requireNonNull(intent.getAction()).equals(Intent.ACTION_PACKAGE_REMOVED) && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false))
+ // Ignore existing packages being removed in order to be updated
+ return;
+
+ String packageName = getPackageName(intent);
+ if (packageName == null)
+ return;
+
+ if (intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED)) {
+ // make sure that the change is for the complete package, not only a
+ // component
+ String[] components = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
+ if (components != null) {
+ boolean isForPackage = false;
+ for (String component : components) {
+ if (packageName.equals(component)) {
+ isForPackage = true;
+ break;
+ }
+ }
+ if (!isForPackage)
+ return;
+ }
+ } else if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) {
+ NotificationUtil.cancel(packageName, NotificationUtil.NOTIFICATION_MODULE_NOT_ACTIVATED_YET);
+ return;
+ }
+
+ mModuleUtil = getModuleUtilInstance();
+
+ InstalledModule module = ModuleUtil.getInstance().reloadSingleModule(packageName);
+ if (module == null
+ || intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) {
+ // Package being removed, disable it if it was a previously active
+ // Xposed mod
+ if (mModuleUtil.isModuleEnabled(packageName)) {
+ mModuleUtil.setModuleEnabled(packageName, false);
+ mModuleUtil.updateModulesList(false);
+ }
+ return;
+ }
+
+ if (mModuleUtil.isModuleEnabled(packageName)) {
+ mModuleUtil.updateModulesList(false);
+ NotificationUtil.showModulesUpdatedNotification();
+ } else {
+ NotificationUtil.showNotActivatedNotification(packageName, module.getAppName());
+ }
+ }
+
+ private ModuleUtil getModuleUtilInstance() {
+ if (mModuleUtil == null) {
+ mModuleUtil = ModuleUtil.getInstance();
+ }
+ return mModuleUtil;
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/Module.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/Module.java
new file mode 100644
index 00000000..83e76989
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/Module.java
@@ -0,0 +1,28 @@
+package org.meowcat.edxposed.manager.repo;
+
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class Module {
+ @SuppressWarnings("WeakerAccess")
+ public final Repository repository;
+ public final List> moreInfo = new LinkedList<>();
+ public final List versions = new ArrayList<>();
+ final List screenshots = new ArrayList<>();
+ public String packageName;
+ public String name;
+ public String summary;
+ public String description;
+ public boolean descriptionIsHtml = false;
+ public String author;
+ public String support;
+ long created = -1;
+ long updated = -1;
+
+ Module(Repository repository) {
+ this.repository = repository;
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/ModuleVersion.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/ModuleVersion.java
new file mode 100644
index 00000000..299a0da0
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/ModuleVersion.java
@@ -0,0 +1,17 @@
+package org.meowcat.edxposed.manager.repo;
+
+public class ModuleVersion {
+ public final Module module;
+ public String name;
+ public int code;
+ public String downloadLink;
+ public String md5sum;
+ public String changelog;
+ public boolean changelogIsHtml = false;
+ public ReleaseType relType = ReleaseType.STABLE;
+ public long uploaded = -1;
+
+ /* package */ ModuleVersion(Module module) {
+ this.module = module;
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/ReleaseType.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/ReleaseType.java
new file mode 100644
index 00000000..2e549a9a
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/ReleaseType.java
@@ -0,0 +1,40 @@
+package org.meowcat.edxposed.manager.repo;
+
+
+import org.meowcat.edxposed.manager.R;
+
+public enum ReleaseType {
+ STABLE(R.string.reltype_stable, R.string.reltype_stable_summary), BETA(R.string.reltype_beta, R.string.reltype_beta_summary), EXPERIMENTAL(R.string.reltype_experimental, R.string.reltype_experimental_summary);
+
+ private static final ReleaseType[] sValuesCache = values();
+ private final int mTitleId;
+ private final int mSummaryId;
+
+ ReleaseType(int titleId, int summaryId) {
+ mTitleId = titleId;
+ mSummaryId = summaryId;
+ }
+
+ public static ReleaseType fromString(String value) {
+ if (value == null || value.equals("stable"))
+ return STABLE;
+ else if (value.equals("beta"))
+ return BETA;
+ else if (value.equals("experimental"))
+ return EXPERIMENTAL;
+ else
+ return STABLE;
+ }
+
+ public static ReleaseType fromOrdinal(int ordinal) {
+ return sValuesCache[ordinal];
+ }
+
+ public int getTitleId() {
+ return mTitleId;
+ }
+
+ public int getSummaryId() {
+ return mSummaryId;
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDb.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDb.java
new file mode 100644
index 00000000..427dba28
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDb.java
@@ -0,0 +1,492 @@
+package org.meowcat.edxposed.manager.repo;
+
+import android.annotation.SuppressLint;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import org.meowcat.edxposed.manager.BuildConfig;
+import org.meowcat.edxposed.manager.XposedApp;
+import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.InstalledModulesColumns;
+import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.InstalledModulesUpdatesColumns;
+import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.ModuleVersionsColumns;
+import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.ModulesColumns;
+import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.MoreInfoColumns;
+import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.OverviewColumns;
+import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.OverviewColumnsIndexes;
+import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.RepositoriesColumns;
+import org.meowcat.edxposed.manager.util.ModuleUtil;
+import org.meowcat.edxposed.manager.util.ModuleUtil.InstalledModule;
+import org.meowcat.edxposed.manager.util.RepoLoader;
+
+import java.io.File;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static android.content.Context.MODE_PRIVATE;
+
+public final class RepoDb extends SQLiteOpenHelper {
+ public static final int SORT_STATUS = 0;
+ public static final int SORT_UPDATED = 1;
+ private static final int SORT_CREATED = 2;
+
+ @SuppressLint("StaticFieldLeak")
+ private static Context context;
+ private static SQLiteDatabase sDb;
+
+ static {
+ RepoDb instance = new RepoDb(XposedApp.getInstance());
+ sDb = instance.getWritableDatabase();
+ sDb.execSQL("PRAGMA foreign_keys=ON");
+ instance.createTempTables(sDb);
+ }
+
+ private RepoDb(Context context) {
+ super(context, getDbPath(context), null, RepoDbDefinitions.DATABASE_VERSION);
+ RepoDb.context = context;
+ }
+
+ private static String getDbPath(Context context) {
+ return new File(context.getNoBackupFilesDir(), RepoDbDefinitions.DATABASE_NAME).getPath();
+ }
+
+ public static void beginTransation() {
+ sDb.beginTransaction();
+ }
+
+ public static void setTransactionSuccessful() {
+ sDb.setTransactionSuccessful();
+ }
+
+ public static void endTransation() {
+ sDb.endTransaction();
+ }
+
+ private static String getString(@SuppressWarnings("SameParameterValue") String table, @SuppressWarnings("SameParameterValue") String searchColumn, String searchValue, @SuppressWarnings("SameParameterValue") String resultColumn) {
+ String[] projection = new String[]{resultColumn};
+ String where = searchColumn + " = ?";
+ String[] whereArgs = new String[]{searchValue};
+ Cursor c = sDb.query(table, projection, where, whereArgs, null, null, null, "1");
+ if (c.moveToFirst()) {
+ String result = c.getString(c.getColumnIndexOrThrow(resultColumn));
+ c.close();
+ return result;
+ } else {
+ c.close();
+ throw new RowNotFoundException("Could not find " + table + "." + searchColumn + " with value '" + searchValue + "'");
+ }
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static long insertRepository(String url) {
+ ContentValues values = new ContentValues();
+ values.put(RepositoriesColumns.URL, url);
+ return sDb.insertOrThrow(RepositoriesColumns.TABLE_NAME, null, values);
+ }
+
+ public static void deleteRepositories() {
+ if (sDb != null)
+ sDb.delete(RepositoriesColumns.TABLE_NAME, null, null);
+ }
+
+ public static Map getRepositories() {
+ Map result = new LinkedHashMap<>(1);
+
+ String[] projection = new String[]{
+ RepositoriesColumns._ID,
+ RepositoriesColumns.URL,
+ RepositoriesColumns.TITLE,
+ RepositoriesColumns.PARTIAL_URL,
+ RepositoriesColumns.VERSION,
+ };
+
+ Cursor c = sDb.query(RepositoriesColumns.TABLE_NAME, projection, null, null, null, null, RepositoriesColumns._ID);
+ while (c.moveToNext()) {
+ Repository repo = new Repository();
+ long id = c.getLong(c.getColumnIndexOrThrow(RepositoriesColumns._ID));
+ repo.url = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.URL));
+ repo.name = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.TITLE));
+ repo.partialUrl = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.PARTIAL_URL));
+ repo.version = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.VERSION));
+ result.put(id, repo);
+ }
+ c.close();
+
+ return result;
+ }
+
+ public static void updateRepository(long repoId, Repository repository) {
+ ContentValues values = new ContentValues();
+ values.put(RepositoriesColumns.TITLE, repository.name);
+ values.put(RepositoriesColumns.PARTIAL_URL, repository.partialUrl);
+ values.put(RepositoriesColumns.VERSION, repository.version);
+ sDb.update(RepositoriesColumns.TABLE_NAME, values, RepositoriesColumns._ID + " = ?", new String[]{Long.toString(repoId)});
+ }
+
+ public static void updateRepositoryVersion(long repoId, String version) {
+ ContentValues values = new ContentValues();
+ values.put(RepositoriesColumns.VERSION, version);
+ sDb.update(RepositoriesColumns.TABLE_NAME, values, RepositoriesColumns._ID + " = ?", new String[]{Long.toString(repoId)});
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static long insertModule(long repoId, Module mod) {
+ ContentValues values = new ContentValues();
+ values.put(ModulesColumns.REPO_ID, repoId);
+ values.put(ModulesColumns.PKGNAME, mod.packageName);
+ values.put(ModulesColumns.TITLE, mod.name);
+ values.put(ModulesColumns.SUMMARY, mod.summary);
+ values.put(ModulesColumns.DESCRIPTION, mod.description);
+ values.put(ModulesColumns.DESCRIPTION_IS_HTML, mod.descriptionIsHtml);
+ values.put(ModulesColumns.AUTHOR, mod.author);
+ values.put(ModulesColumns.SUPPORT, mod.support);
+ values.put(ModulesColumns.CREATED, mod.created);
+ values.put(ModulesColumns.UPDATED, mod.updated);
+
+ ModuleVersion latestVersion = RepoLoader.getInstance().getLatestVersion(mod);
+
+ sDb.beginTransaction();
+ try {
+ long moduleId = sDb.insertOrThrow(ModulesColumns.TABLE_NAME, null, values);
+
+ long latestVersionId = -1;
+ for (ModuleVersion version : mod.versions) {
+ long versionId = insertModuleVersion(moduleId, version);
+ if (latestVersion == version)
+ latestVersionId = versionId;
+ }
+
+ if (latestVersionId > -1) {
+ values = new ContentValues();
+ values.put(ModulesColumns.LATEST_VERSION, latestVersionId);
+ sDb.update(ModulesColumns.TABLE_NAME, values, ModulesColumns._ID + " = ?", new String[]{Long.toString(moduleId)});
+ }
+
+ for (Pair moreInfoEntry : mod.moreInfo) {
+ insertMoreInfo(moduleId, moreInfoEntry.first, moreInfoEntry.second);
+ }
+
+ // TODO Add mod.screenshots
+
+ sDb.setTransactionSuccessful();
+ return moduleId;
+
+ } finally {
+ sDb.endTransaction();
+ }
+ }
+
+ private static long insertModuleVersion(long moduleId, ModuleVersion version) {
+ ContentValues values = new ContentValues();
+ values.put(ModuleVersionsColumns.MODULE_ID, moduleId);
+ values.put(ModuleVersionsColumns.NAME, version.name);
+ values.put(ModuleVersionsColumns.CODE, version.code);
+ values.put(ModuleVersionsColumns.DOWNLOAD_LINK, version.downloadLink);
+ values.put(ModuleVersionsColumns.MD5SUM, version.md5sum);
+ values.put(ModuleVersionsColumns.CHANGELOG, version.changelog);
+ values.put(ModuleVersionsColumns.CHANGELOG_IS_HTML, version.changelogIsHtml);
+ values.put(ModuleVersionsColumns.RELTYPE, version.relType.ordinal());
+ values.put(ModuleVersionsColumns.UPLOADED, version.uploaded);
+ return sDb.insertOrThrow(ModuleVersionsColumns.TABLE_NAME, null,
+ values);
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ private static long insertMoreInfo(long moduleId, String title, String value) {
+ ContentValues values = new ContentValues();
+ values.put(MoreInfoColumns.MODULE_ID, moduleId);
+ values.put(MoreInfoColumns.LABEL, title);
+ values.put(MoreInfoColumns.VALUE, value);
+ return sDb.insertOrThrow(MoreInfoColumns.TABLE_NAME, null, values);
+ }
+
+ public static void deleteAllModules(long repoId) {
+ sDb.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ?", new String[]{Long.toString(repoId)});
+ }
+
+ public static void deleteModule(long repoId, String packageName) {
+ sDb.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ? AND " + ModulesColumns.PKGNAME + " = ?", new String[]{Long.toString(repoId), packageName});
+ }
+
+ public static Module getModuleByPackageName(String packageName) {
+ // The module itself
+ String[] projection = new String[]{
+ ModulesColumns._ID,
+ ModulesColumns.REPO_ID,
+ ModulesColumns.PKGNAME,
+ ModulesColumns.TITLE,
+ ModulesColumns.SUMMARY,
+ ModulesColumns.DESCRIPTION,
+ ModulesColumns.DESCRIPTION_IS_HTML,
+ ModulesColumns.AUTHOR,
+ ModulesColumns.SUPPORT,
+ ModulesColumns.CREATED,
+ ModulesColumns.UPDATED,
+ };
+
+ String where = ModulesColumns.PREFERRED + " = 1 AND " + ModulesColumns.PKGNAME + " = ?";
+ String[] whereArgs = new String[]{packageName};
+
+ Cursor c = sDb.query(ModulesColumns.TABLE_NAME, projection, where, whereArgs, null, null, null, "1");
+ if (!c.moveToFirst()) {
+ c.close();
+ return null;
+ }
+
+ long moduleId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns._ID));
+ long repoId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.REPO_ID));
+
+ Module mod = new Module(RepoLoader.getInstance().getRepository(repoId));
+ mod.packageName = c.getString(c.getColumnIndexOrThrow(ModulesColumns.PKGNAME));
+ mod.name = c.getString(c.getColumnIndexOrThrow(ModulesColumns.TITLE));
+ mod.summary = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUMMARY));
+ mod.description = c.getString(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION));
+ mod.descriptionIsHtml = c.getInt(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION_IS_HTML)) > 0;
+ mod.author = c.getString(c.getColumnIndexOrThrow(ModulesColumns.AUTHOR));
+ mod.support = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUPPORT));
+ mod.created = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.CREATED));
+ mod.updated = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.UPDATED));
+
+ c.close();
+
+ // Versions
+ projection = new String[]{
+ ModuleVersionsColumns.NAME,
+ ModuleVersionsColumns.CODE, ModuleVersionsColumns.DOWNLOAD_LINK,
+ ModuleVersionsColumns.MD5SUM, ModuleVersionsColumns.CHANGELOG,
+ ModuleVersionsColumns.CHANGELOG_IS_HTML,
+ ModuleVersionsColumns.RELTYPE,
+ ModuleVersionsColumns.UPLOADED,
+ };
+
+ where = ModuleVersionsColumns.MODULE_ID + " = ?";
+ whereArgs = new String[]{Long.toString(moduleId)};
+
+ c = sDb.query(ModuleVersionsColumns.TABLE_NAME, projection, where, whereArgs, null, null, null);
+ while (c.moveToNext()) {
+ ModuleVersion version = new ModuleVersion(mod);
+ version.name = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.NAME));
+ version.code = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CODE));
+ version.downloadLink = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.DOWNLOAD_LINK));
+ version.md5sum = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.MD5SUM));
+ version.changelog = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG));
+ version.changelogIsHtml = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG_IS_HTML)) > 0;
+ version.relType = ReleaseType.fromOrdinal(c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.RELTYPE)));
+ version.uploaded = c.getLong(c.getColumnIndexOrThrow(ModuleVersionsColumns.UPLOADED));
+ mod.versions.add(version);
+ }
+ c.close();
+
+ // MoreInfo
+ projection = new String[]{
+ MoreInfoColumns.LABEL,
+ MoreInfoColumns.VALUE,
+ };
+
+ where = MoreInfoColumns.MODULE_ID + " = ?";
+ whereArgs = new String[]{Long.toString(moduleId)};
+
+ c = sDb.query(MoreInfoColumns.TABLE_NAME, projection, where, whereArgs, null, null, MoreInfoColumns._ID);
+ while (c.moveToNext()) {
+ String label = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.LABEL));
+ String value = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.VALUE));
+ mod.moreInfo.add(new Pair<>(label, value));
+ }
+ c.close();
+
+ return mod;
+ }
+
+ public static String getModuleSupport(String packageName) {
+ return getString(ModulesColumns.TABLE_NAME, ModulesColumns.PKGNAME, packageName, ModulesColumns.SUPPORT);
+ }
+
+ public static void updateModuleLatestVersion(String packageName) {
+ int maxShownReleaseType = RepoLoader.getInstance().getMaxShownReleaseType(packageName).ordinal();
+ sDb.execSQL("UPDATE " + ModulesColumns.TABLE_NAME
+ + " SET " + ModulesColumns.LATEST_VERSION
+ + " = (SELECT " + ModuleVersionsColumns._ID + " FROM " + ModuleVersionsColumns.TABLE_NAME + " AS v"
+ + " WHERE v." + ModuleVersionsColumns.MODULE_ID
+ + " = " + ModulesColumns.TABLE_NAME + "." + ModulesColumns._ID
+ + " AND reltype <= ? LIMIT 1)"
+ + " WHERE " + ModulesColumns.PKGNAME + " = ?",
+ new Object[]{maxShownReleaseType, packageName});
+ }
+
+ public static void updateAllModulesLatestVersion() {
+ sDb.beginTransaction();
+ try {
+ String[] projection = new String[]{ModulesColumns.PKGNAME};
+ Cursor c = sDb.query(true, ModulesColumns.TABLE_NAME, projection, null, null, null, null, null, null);
+ while (c.moveToNext()) {
+ updateModuleLatestVersion(c.getString(0));
+ }
+ c.close();
+ sDb.setTransactionSuccessful();
+ } finally {
+ sDb.endTransaction();
+ }
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static long insertInstalledModule(InstalledModule installed) {
+ ContentValues values = new ContentValues();
+ values.put(InstalledModulesColumns.PKGNAME, installed.packageName);
+ values.put(InstalledModulesColumns.VERSION_CODE, installed.versionCode);
+ values.put(InstalledModulesColumns.VERSION_NAME, installed.versionName);
+ return sDb.insertOrThrow(InstalledModulesColumns.TABLE_NAME, null, values);
+ }
+
+ public static void deleteInstalledModule(String packageName) {
+ sDb.delete(InstalledModulesColumns.TABLE_NAME, InstalledModulesColumns.PKGNAME + " = ?", new String[]{packageName});
+ }
+
+ public static void deleteAllInstalledModules() {
+ sDb.delete(InstalledModulesColumns.TABLE_NAME, null, null);
+ }
+
+ public static Cursor queryModuleOverview(int sortingOrder,
+ CharSequence filterText) {
+ // Columns
+ String[] projection = new String[]{
+ "m." + ModulesColumns._ID,
+ "m." + ModulesColumns.PKGNAME,
+ "m." + ModulesColumns.TITLE,
+ "m." + ModulesColumns.SUMMARY,
+ "m." + ModulesColumns.CREATED,
+ "m." + ModulesColumns.UPDATED,
+
+ "v." + ModuleVersionsColumns.NAME + " AS " + OverviewColumns.LATEST_VERSION,
+ "i." + InstalledModulesColumns.VERSION_NAME + " AS " + OverviewColumns.INSTALLED_VERSION,
+
+ "(CASE WHEN m." + ModulesColumns.PKGNAME + " = '" + ModuleUtil.getInstance().getFrameworkPackageName()
+ + "' THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_FRAMEWORK,
+
+ "(CASE WHEN i." + InstalledModulesColumns.VERSION_NAME + " IS NOT NULL"
+ + " THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_INSTALLED,
+
+ "(CASE WHEN v." + ModuleVersionsColumns.CODE + " > " + InstalledModulesColumns.VERSION_CODE
+ + " THEN 1 ELSE 0 END) AS " + OverviewColumns.HAS_UPDATE,
+ };
+
+ // Conditions
+ StringBuilder where = new StringBuilder(ModulesColumns.PREFERRED + " = 1");
+ String[] whereArgs = null;
+ if (!TextUtils.isEmpty(filterText)) {
+ where.append(" AND (m." + ModulesColumns.TITLE + " LIKE ?" + " OR m." + ModulesColumns.SUMMARY + " LIKE ?" + " OR m." + ModulesColumns.DESCRIPTION + " LIKE ?" + " OR m." + ModulesColumns.AUTHOR + " LIKE ?)");
+ String filterTextArg = "%" + filterText + "%";
+ whereArgs = new String[]{filterTextArg, filterTextArg, filterTextArg, filterTextArg};
+ } else {
+ SharedPreferences prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID + "_preferences", MODE_PRIVATE);
+
+ if (prefs.getBoolean("ignore_chinese", false)) {
+ for (char ch : "的一是不了人我在有他这为中设微模块淘".toCharArray()) {
+ where.append(" AND NOT (m." + ModulesColumns.TITLE + " LIKE '%").append(ch).append("%'").append(" OR m.").append(ModulesColumns.SUMMARY).append(" LIKE '%").append(ch).append("%'").append(" OR m.").append(ModulesColumns.DESCRIPTION).append(" LIKE '%").append(ch).append("%')");
+ }
+ }
+ }
+
+ // Sorting order
+ StringBuilder sbOrder = new StringBuilder();
+ if (sortingOrder == SORT_CREATED) {
+ sbOrder.append(OverviewColumns.CREATED);
+ sbOrder.append(" DESC,");
+ } else if (sortingOrder == SORT_UPDATED) {
+ sbOrder.append(OverviewColumns.UPDATED);
+ sbOrder.append(" DESC,");
+ }
+ sbOrder.append(OverviewColumns.IS_FRAMEWORK);
+ sbOrder.append(" DESC, ");
+ sbOrder.append(OverviewColumns.HAS_UPDATE);
+ sbOrder.append(" DESC, ");
+ sbOrder.append(OverviewColumns.IS_INSTALLED);
+ sbOrder.append(" DESC, ");
+ sbOrder.append("m.");
+ sbOrder.append(OverviewColumns.TITLE);
+ sbOrder.append(" COLLATE NOCASE, ");
+ sbOrder.append("m.");
+ sbOrder.append(OverviewColumns.PKGNAME);
+
+ // Query
+ Cursor c = sDb.query(
+ ModulesColumns.TABLE_NAME + " AS m"
+ + " LEFT JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v"
+ + " ON v." + ModuleVersionsColumns._ID + " = m." + ModulesColumns.LATEST_VERSION
+ + " LEFT JOIN " + InstalledModulesColumns.TABLE_NAME + " AS i"
+ + " ON i." + InstalledModulesColumns.PKGNAME + " = m." + ModulesColumns.PKGNAME,
+ projection, where.toString(), whereArgs, null, null, sbOrder.toString());
+
+ // Cache column indexes
+ OverviewColumnsIndexes.fillFromCursor(c);
+
+ return c;
+ }
+
+ public static String getFrameworkUpdateVersion() {
+ return getFirstUpdate(true);
+ }
+
+ public static boolean hasModuleUpdates() {
+ return getFirstUpdate(false) != null;
+ }
+
+ private static String getFirstUpdate(boolean framework) {
+ String[] projection = new String[]{InstalledModulesUpdatesColumns.LATEST_NAME};
+ String where = ModulesColumns.PKGNAME + (framework ? " = ?" : " != ?");
+ String[] whereArgs = new String[]{ModuleUtil.getInstance().getFrameworkPackageName()};
+ Cursor c = sDb.query(InstalledModulesUpdatesColumns.VIEW_NAME, projection, where, whereArgs, null, null, null, "1");
+ String latestVersion = null;
+ if (c.moveToFirst())
+ latestVersion = c.getString(c.getColumnIndexOrThrow(InstalledModulesUpdatesColumns.LATEST_NAME));
+ c.close();
+ return latestVersion;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_REPOSITORIES);
+ db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULES);
+ db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULE_VERSIONS);
+ db.execSQL(RepoDbDefinitions.SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID);
+ db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MORE_INFO);
+
+ RepoLoader.getInstance().clear(false);
+ }
+
+ private void createTempTables(SQLiteDatabase db) {
+ db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES);
+ db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // This is only a cache, so simply drop & recreate the tables
+ db.execSQL("DROP TABLE IF EXISTS " + RepositoriesColumns.TABLE_NAME);
+ db.execSQL("DROP TABLE IF EXISTS " + ModulesColumns.TABLE_NAME);
+ db.execSQL("DROP TABLE IF EXISTS " + ModuleVersionsColumns.TABLE_NAME);
+ db.execSQL("DROP TABLE IF EXISTS " + MoreInfoColumns.TABLE_NAME);
+
+ db.execSQL("DROP TABLE IF EXISTS " + InstalledModulesColumns.TABLE_NAME);
+ db.execSQL("DROP VIEW IF EXISTS " + InstalledModulesUpdatesColumns.VIEW_NAME);
+
+ onCreate(db);
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ onUpgrade(db, oldVersion, newVersion);
+ }
+
+ public static class RowNotFoundException extends RuntimeException {
+ private static final long serialVersionUID = -396324186622439535L;
+
+ RowNotFoundException(String reason) {
+ super(reason);
+ }
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDbDefinitions.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDbDefinitions.java
new file mode 100644
index 00000000..666b5513
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDbDefinitions.java
@@ -0,0 +1,216 @@
+package org.meowcat.edxposed.manager.repo;
+
+import android.database.Cursor;
+import android.provider.BaseColumns;
+
+public class RepoDbDefinitions {
+ static final int DATABASE_VERSION = 4;
+ static final String DATABASE_NAME = "repo_cache.db";
+ static final String SQL_CREATE_TABLE_REPOSITORIES = "CREATE TABLE "
+ + RepositoriesColumns.TABLE_NAME + " (" + RepositoriesColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT," + RepositoriesColumns.URL
+ + " TEXT NOT NULL, " + RepositoriesColumns.TITLE + " TEXT, "
+ + RepositoriesColumns.PARTIAL_URL + " TEXT, "
+ + RepositoriesColumns.VERSION + " TEXT, " + "UNIQUE ("
+ + RepositoriesColumns.URL + ") ON CONFLICT REPLACE)";
+ static final String SQL_CREATE_TABLE_MODULES = "CREATE TABLE "
+ + ModulesColumns.TABLE_NAME + " (" + ModulesColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+
+ ModulesColumns.REPO_ID + " INTEGER NOT NULL REFERENCES "
+ + RepositoriesColumns.TABLE_NAME + " ON DELETE CASCADE, "
+ + ModulesColumns.PKGNAME + " TEXT NOT NULL, " + ModulesColumns.TITLE
+ + " TEXT NOT NULL, " + ModulesColumns.SUMMARY + " TEXT, "
+ + ModulesColumns.DESCRIPTION + " TEXT, "
+ + ModulesColumns.DESCRIPTION_IS_HTML + " INTEGER DEFAULT 0, "
+ + ModulesColumns.AUTHOR + " TEXT, " + ModulesColumns.SUPPORT
+ + " TEXT, " + ModulesColumns.CREATED + " INTEGER DEFAULT -1, "
+ + ModulesColumns.UPDATED + " INTEGER DEFAULT -1, "
+ + ModulesColumns.PREFERRED + " INTEGER DEFAULT 1, "
+ + ModulesColumns.LATEST_VERSION + " INTEGER REFERENCES "
+ + ModuleVersionsColumns.TABLE_NAME + ", " + "UNIQUE ("
+ + ModulesColumns.PKGNAME + ", " + ModulesColumns.REPO_ID
+ + ") ON CONFLICT REPLACE)";
+ static final String SQL_CREATE_TABLE_MODULE_VERSIONS = "CREATE TABLE "
+ + ModuleVersionsColumns.TABLE_NAME + " ("
+ + ModuleVersionsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + ModuleVersionsColumns.MODULE_ID + " INTEGER NOT NULL REFERENCES "
+ + ModulesColumns.TABLE_NAME + " ON DELETE CASCADE, "
+ + ModuleVersionsColumns.NAME + " TEXT NOT NULL, "
+ + ModuleVersionsColumns.CODE + " INTEGER NOT NULL, "
+ + ModuleVersionsColumns.DOWNLOAD_LINK + " TEXT, "
+ + ModuleVersionsColumns.MD5SUM + " TEXT, "
+ + ModuleVersionsColumns.CHANGELOG + " TEXT, "
+ + ModuleVersionsColumns.CHANGELOG_IS_HTML + " INTEGER DEFAULT 0, "
+ + ModuleVersionsColumns.RELTYPE + " INTEGER DEFAULT 0, "
+ + ModuleVersionsColumns.UPLOADED + " INTEGER DEFAULT -1)";
+ static final String SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID = "CREATE INDEX "
+ + ModuleVersionsColumns.IDX_MODULE_ID + " ON "
+ + ModuleVersionsColumns.TABLE_NAME + " ("
+ + ModuleVersionsColumns.MODULE_ID + ")";
+ static final String SQL_CREATE_TABLE_MORE_INFO = "CREATE TABLE "
+ + MoreInfoColumns.TABLE_NAME + " (" + MoreInfoColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT," + MoreInfoColumns.MODULE_ID
+ + " INTEGER NOT NULL REFERENCES " + ModulesColumns.TABLE_NAME
+ + " ON DELETE CASCADE, " + MoreInfoColumns.LABEL
+ + " TEXT NOT NULL, " + MoreInfoColumns.VALUE + " TEXT)";
+ static final String SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES = "CREATE TEMP TABLE "
+ + InstalledModulesColumns.TABLE_NAME + " ("
+ + InstalledModulesColumns.PKGNAME
+ + " TEXT PRIMARY KEY ON CONFLICT REPLACE, "
+ + InstalledModulesColumns.VERSION_CODE + " INTEGER NOT NULL, "
+ + InstalledModulesColumns.VERSION_NAME + " TEXT)";
+ static final String SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES = "CREATE TEMP VIEW "
+ + InstalledModulesUpdatesColumns.VIEW_NAME + " AS SELECT " + "m."
+ + ModulesColumns._ID + " AS "
+ + InstalledModulesUpdatesColumns.MODULE_ID + ", " + "i."
+ + InstalledModulesColumns.PKGNAME + " AS "
+ + InstalledModulesUpdatesColumns.PKGNAME + ", " + "i."
+ + InstalledModulesColumns.VERSION_CODE + " AS "
+ + InstalledModulesUpdatesColumns.INSTALLED_CODE + ", " + "i."
+ + InstalledModulesColumns.VERSION_NAME + " AS "
+ + InstalledModulesUpdatesColumns.INSTALLED_NAME + ", " + "v."
+ + ModuleVersionsColumns._ID + " AS "
+ + InstalledModulesUpdatesColumns.LATEST_ID + ", " + "v."
+ + ModuleVersionsColumns.CODE + " AS "
+ + InstalledModulesUpdatesColumns.LATEST_CODE + ", " + "v."
+ + ModuleVersionsColumns.NAME + " AS "
+ + InstalledModulesUpdatesColumns.LATEST_NAME + " FROM "
+ + InstalledModulesColumns.TABLE_NAME + " AS i" + " INNER JOIN "
+ + ModulesColumns.TABLE_NAME + " AS m" + " ON m."
+ + ModulesColumns.PKGNAME + " = i." + InstalledModulesColumns.PKGNAME
+ + " INNER JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v"
+ + " ON v." + ModuleVersionsColumns._ID + " = m."
+ + ModulesColumns.LATEST_VERSION + " WHERE "
+ + InstalledModulesUpdatesColumns.LATEST_CODE + " > "
+ + InstalledModulesUpdatesColumns.INSTALLED_CODE + " AND "
+ + ModulesColumns.PREFERRED + " = 1";
+
+ //////////////////////////////////////////////////////////////////////////
+ public interface RepositoriesColumns extends BaseColumns {
+ String TABLE_NAME = "repositories";
+
+ String URL = "url";
+ String TITLE = "title";
+ String PARTIAL_URL = "partial_url";
+ String VERSION = "version";
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ public interface ModulesColumns extends BaseColumns {
+ String TABLE_NAME = "modules";
+
+ String REPO_ID = "repo_id";
+ String PKGNAME = "pkgname";
+ String TITLE = "title";
+ String SUMMARY = "summary";
+ String DESCRIPTION = "description";
+ String DESCRIPTION_IS_HTML = "description_is_html";
+ String AUTHOR = "author";
+ String SUPPORT = "support";
+ String CREATED = "created";
+ String UPDATED = "updated";
+
+ String PREFERRED = "preferred";
+ String LATEST_VERSION = "latest_version_id";
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ public interface ModuleVersionsColumns extends BaseColumns {
+ String TABLE_NAME = "module_versions";
+ String IDX_MODULE_ID = "module_versions_module_id_idx";
+
+ String MODULE_ID = "module_id";
+ String NAME = "name";
+ String CODE = "code";
+ String DOWNLOAD_LINK = "download_link";
+ String MD5SUM = "md5sum";
+ String CHANGELOG = "changelog";
+ String CHANGELOG_IS_HTML = "changelog_is_html";
+ String RELTYPE = "reltype";
+ String UPLOADED = "uploaded";
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ public interface MoreInfoColumns extends BaseColumns {
+ String TABLE_NAME = "more_info";
+
+ String MODULE_ID = "module_id";
+ String LABEL = "label";
+ String VALUE = "value";
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ public interface InstalledModulesColumns {
+ String TABLE_NAME = "installed_modules";
+
+ String PKGNAME = "pkgname";
+ String VERSION_CODE = "version_code";
+ String VERSION_NAME = "version_name";
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ public interface InstalledModulesUpdatesColumns {
+ String VIEW_NAME = InstalledModulesColumns.TABLE_NAME + "_updates";
+
+ String MODULE_ID = "module_id";
+ String PKGNAME = "pkgname";
+ String INSTALLED_CODE = "installed_code";
+ String INSTALLED_NAME = "installed_name";
+ String LATEST_ID = "latest_id";
+ String LATEST_CODE = "latest_code";
+ String LATEST_NAME = "latest_name";
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ public interface OverviewColumns extends BaseColumns {
+ String PKGNAME = ModulesColumns.PKGNAME;
+ String TITLE = ModulesColumns.TITLE;
+ String SUMMARY = ModulesColumns.SUMMARY;
+ String CREATED = ModulesColumns.CREATED;
+ String UPDATED = ModulesColumns.UPDATED;
+
+ String INSTALLED_VERSION = "installed_version";
+ String LATEST_VERSION = "latest_version";
+
+ String IS_FRAMEWORK = "is_framework";
+ String IS_INSTALLED = "is_installed";
+ String HAS_UPDATE = "has_update";
+ }
+
+ public static class OverviewColumnsIndexes {
+ public static int PKGNAME = -1;
+ public static int TITLE = -1;
+ public static int SUMMARY = -1;
+ public static int CREATED = -1;
+ public static int UPDATED = -1;
+ public static int INSTALLED_VERSION = -1;
+ public static int LATEST_VERSION = -1;
+ public static int IS_FRAMEWORK = -1;
+ public static int IS_INSTALLED = -1;
+ public static int HAS_UPDATE = -1;
+ private static boolean isFilled = false;
+
+ private OverviewColumnsIndexes() {
+ }
+
+ static void fillFromCursor(Cursor cursor) {
+ if (isFilled || cursor == null)
+ return;
+
+ PKGNAME = cursor.getColumnIndexOrThrow(OverviewColumns.PKGNAME);
+ TITLE = cursor.getColumnIndexOrThrow(OverviewColumns.TITLE);
+ SUMMARY = cursor.getColumnIndexOrThrow(OverviewColumns.SUMMARY);
+ CREATED = cursor.getColumnIndexOrThrow(OverviewColumns.CREATED);
+ UPDATED = cursor.getColumnIndexOrThrow(OverviewColumns.UPDATED);
+ INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION);
+ LATEST_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.LATEST_VERSION);
+ INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION);
+ IS_FRAMEWORK = cursor.getColumnIndexOrThrow(OverviewColumns.IS_FRAMEWORK);
+ IS_INSTALLED = cursor.getColumnIndexOrThrow(OverviewColumns.IS_INSTALLED);
+ HAS_UPDATE = cursor.getColumnIndexOrThrow(OverviewColumns.HAS_UPDATE);
+
+ isFilled = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoParser.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoParser.java
new file mode 100644
index 00000000..911fee90
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoParser.java
@@ -0,0 +1,322 @@
+package org.meowcat.edxposed.manager.repo;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LevelListDrawable;
+import android.text.Html;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.util.Log;
+import android.util.Pair;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.target.CustomTarget;
+import com.bumptech.glide.request.transition.Transition;
+
+import org.meowcat.edxposed.manager.XposedApp;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class RepoParser {
+ public final static String TAG = XposedApp.TAG;
+ private final static String NS = null;
+ private final XmlPullParser parser;
+ private RepoParserCallback mCallback;
+ private boolean mRepoEventTriggered = false;
+
+ private RepoParser(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ parser = factory.newPullParser();
+ parser.setInput(is, null);
+ parser.nextTag();
+ mCallback = callback;
+ }
+
+ public static void parse(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException {
+ new RepoParser(is, callback).readRepo();
+ }
+
+ public static Spanned parseSimpleHtml(final Context context, String source, final TextView textView) {
+ source = source.replaceAll("", "\t\u0095 ");
+ source = source.replaceAll("", "
");
+ Spanned html = Html.fromHtml(source, new Html.ImageGetter() {
+ @Override
+ public Drawable getDrawable(String source) {
+ LevelListDrawable levelListDrawable = new LevelListDrawable();
+ final Drawable[] drawable = new Drawable[1];
+ Glide.with(context).asBitmap().load(source).into(new CustomTarget() {
+ @Override
+ public void onResourceReady(@NonNull Bitmap bitmap, @Nullable Transition super Bitmap> transition) {
+ try {
+ drawable[0] = new BitmapDrawable(context.getResources(), bitmap);
+ Point size = new Point();
+ ((Activity) context).getWindowManager().getDefaultDisplay().getSize(size);
+ int multiplier = size.x / bitmap.getWidth();
+ if (multiplier <= 0) multiplier = 1;
+ levelListDrawable.addLevel(1, 1, drawable[0]);
+ levelListDrawable.setBounds(0, 0, bitmap.getWidth() * multiplier, bitmap.getHeight() * multiplier);
+ levelListDrawable.setLevel(1);
+ textView.setText(textView.getText());
+ } catch (Exception ignored) { /* Like a null bitmap, etc. */
+ }
+ }
+
+ @Override
+ public void onLoadCleared(@Nullable Drawable placeholder) {
+ }
+ });
+ return drawable[0];
+ }
+ }, null);
+
+ // trim trailing newlines
+ int len = html.length();
+ int end = len;
+ for (int i = len - 1; i >= 0; i--) {
+ if (html.charAt(i) != '\n')
+ break;
+ end = i;
+ }
+
+ if (end == len)
+ return html;
+ else
+ return new SpannableStringBuilder(html, 0, end);
+ }
+
+ private void readRepo() throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, NS, "repository");
+ Repository repository = new Repository();
+ repository.isPartial = "true".equals(parser.getAttributeValue(NS, "partial"));
+ repository.partialUrl = parser.getAttributeValue(NS, "partial-url");
+ repository.version = parser.getAttributeValue(NS, "version");
+
+ while (parser.nextTag() == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+ switch (tagName) {
+ case "name":
+ repository.name = parser.nextText();
+ break;
+ case "module":
+ triggerRepoEvent(repository);
+ Module module = readModule(repository);
+ if (module != null)
+ mCallback.onNewModule(module);
+ break;
+ case "remove-module":
+ triggerRepoEvent(repository);
+ String packageName = readRemoveModule();
+ if (packageName != null)
+ mCallback.onRemoveModule(packageName);
+ break;
+ default:
+ //skip(true);
+ skip(false);
+ break;
+ }
+ }
+
+ mCallback.onCompleted(repository);
+ }
+
+ private void triggerRepoEvent(Repository repository) {
+ if (mRepoEventTriggered)
+ return;
+
+ mCallback.onRepositoryMetadata(repository);
+ mRepoEventTriggered = true;
+ }
+
+ private Module readModule(Repository repository) throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, NS, "module");
+ final int startDepth = parser.getDepth();
+
+ Module module = new Module(repository);
+ module.packageName = parser.getAttributeValue(NS, "package");
+ if (module.packageName == null) {
+ logError("no package name defined");
+ leave(startDepth);
+ return null;
+ }
+
+ module.created = parseTimestamp("created");
+ module.updated = parseTimestamp("updated");
+
+ while (parser.nextTag() == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+ switch (tagName) {
+ case "name":
+ module.name = parser.nextText();
+ break;
+ case "author":
+ module.author = parser.nextText();
+ break;
+ case "summary":
+ module.summary = parser.nextText();
+ break;
+ case "description":
+ String isHtml = parser.getAttributeValue(NS, "html");
+ if (isHtml != null && isHtml.equals("true"))
+ module.descriptionIsHtml = true;
+ module.description = parser.nextText();
+ break;
+ case "screenshot":
+ module.screenshots.add(parser.nextText());
+ break;
+ case "moreinfo":
+ String label = parser.getAttributeValue(NS, "label");
+ String role = parser.getAttributeValue(NS, "role");
+ String value = parser.nextText();
+ module.moreInfo.add(new Pair<>(label, value));
+
+ if (role != null && role.contains("support"))
+ module.support = value;
+ break;
+ case "version":
+ ModuleVersion version = readModuleVersion(module);
+ if (version != null)
+ module.versions.add(version);
+ break;
+ default:
+ //skip(true);
+ skip(false);
+ break;
+ }
+ }
+
+ if (module.name == null) {
+ logError("packages need at least a name");
+ return null;
+ }
+
+ return module;
+ }
+
+ private long parseTimestamp(String attName) {
+ String value = parser.getAttributeValue(NS, attName);
+ if (value == null)
+ return -1;
+ try {
+ return Long.parseLong(value) * 1000L;
+ } catch (NumberFormatException ex) {
+ return -1;
+ }
+ }
+
+ private ModuleVersion readModuleVersion(Module module) throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, NS, "version");
+ final int startDepth = parser.getDepth();
+ ModuleVersion version = new ModuleVersion(module);
+
+ version.uploaded = parseTimestamp("uploaded");
+
+ while (parser.nextTag() == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+ switch (tagName) {
+ case "name":
+ version.name = parser.nextText();
+ break;
+ case "code":
+ try {
+ version.code = Integer.parseInt(parser.nextText());
+ } catch (NumberFormatException nfe) {
+ logError(nfe.getMessage());
+ leave(startDepth);
+ return null;
+ }
+ break;
+ case "reltype":
+ version.relType = ReleaseType.fromString(parser.nextText());
+ break;
+ case "download":
+ version.downloadLink = parser.nextText();
+ break;
+ case "md5sum":
+ version.md5sum = parser.nextText();
+ break;
+ case "changelog":
+ String isHtml = parser.getAttributeValue(NS, "html");
+ if (isHtml != null && isHtml.equals("true"))
+ version.changelogIsHtml = true;
+ version.changelog = parser.nextText();
+ break;
+ case "branch":
+ // obsolete
+// skip(false);
+// break;
+ default:
+ skip(false);
+ //skip(true);
+ break;
+ }
+ }
+
+ return version;
+ }
+
+ private String readRemoveModule() throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, NS, "remove-module");
+ final int startDepth = parser.getDepth();
+
+ String packageName = parser.getAttributeValue(NS, "package");
+ if (packageName == null) {
+ logError("no package name defined");
+ leave(startDepth);
+ return null;
+ }
+
+ return packageName;
+ }
+
+ private void skip(@SuppressWarnings("SameParameterValue") boolean showWarning) throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, null, null);
+ if (showWarning)
+ Log.w(TAG, "skipping unknown/erronous tag: " + parser.getPositionDescription());
+ int level = 1;
+ while (level > 0) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.END_TAG) {
+ level--;
+ } else if (eventType == XmlPullParser.START_TAG) {
+ level++;
+ }
+ }
+ }
+
+ private void leave(int targetDepth) throws XmlPullParserException, IOException {
+ Log.w(TAG, "leaving up to level " + targetDepth + ": " + parser.getPositionDescription());
+ while (parser.getDepth() > targetDepth) {
+ //noinspection StatementWithEmptyBody
+ while (parser.next() != XmlPullParser.END_TAG) {
+ // do nothing
+ }
+ }
+ }
+
+ private void logError(String error) {
+ Log.e(TAG, parser.getPositionDescription() + ": " + error);
+ }
+
+ public interface RepoParserCallback {
+ void onRepositoryMetadata(Repository repository);
+
+ void onNewModule(Module module);
+
+ void onRemoveModule(String packageName);
+
+ void onCompleted(Repository repository);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/Repository.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/Repository.java
new file mode 100644
index 00000000..f4d851ff
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/Repository.java
@@ -0,0 +1,12 @@
+package org.meowcat.edxposed.manager.repo;
+
+public class Repository {
+ public String name;
+ public String url;
+ public boolean isPartial = false;
+ public String partialUrl;
+ public String version;
+
+ Repository() {
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/CompileUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/CompileUtil.java
new file mode 100644
index 00000000..bbf3e413
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/CompileUtil.java
@@ -0,0 +1,43 @@
+package org.meowcat.edxposed.manager.util;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+
+import androidx.fragment.app.FragmentManager;
+
+import org.meowcat.edxposed.manager.CompileDialogFragment;
+import org.meowcat.edxposed.manager.R;
+
+public class CompileUtil {
+
+ private static final String COMPILE_COMMAND_PREFIX = "cmd package ";
+ private static final String COMPILE_RESET_COMMAND = COMPILE_COMMAND_PREFIX + "compile --reset ";
+ private static final String COMPILE_SPEED_COMMAND = COMPILE_COMMAND_PREFIX + "compile -f -m speed ";
+ private static final String COMPILE_DEXOPT_COMMAND = COMPILE_COMMAND_PREFIX + "force-dex-opt ";
+ private static final String TAG_COMPILE_DIALOG = "compile_dialog";
+
+ public static void reset(Context context, FragmentManager fragmentManager,
+ ApplicationInfo info) {
+ compilePackageInBg(fragmentManager, info,
+ context.getString(R.string.compile_reset_msg), COMPILE_RESET_COMMAND);
+ }
+
+ public static void compileSpeed(Context context, FragmentManager fragmentManager,
+ ApplicationInfo info) {
+ compilePackageInBg(fragmentManager, info,
+ context.getString(R.string.compile_speed_msg), COMPILE_SPEED_COMMAND);
+ }
+
+ public static void compileDexopt(Context context, FragmentManager fragmentManager,
+ ApplicationInfo info) {
+ compilePackageInBg(fragmentManager, info,
+ context.getString(R.string.compile_speed_msg), COMPILE_DEXOPT_COMMAND);
+ }
+
+ private static void compilePackageInBg(FragmentManager fragmentManager,
+ ApplicationInfo info, String msg, String... commands) {
+ CompileDialogFragment fragment = CompileDialogFragment.newInstance(info, msg, commands);
+ fragment.show(fragmentManager, TAG_COMPILE_DIALOG);
+ }
+
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/DownloadsUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/DownloadsUtil.java
new file mode 100644
index 00000000..12a83ee0
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/DownloadsUtil.java
@@ -0,0 +1,672 @@
+package org.meowcat.edxposed.manager.util;
+
+import android.annotation.SuppressLint;
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.core.os.EnvironmentCompat;
+
+import org.meowcat.edxposed.manager.R;
+import org.meowcat.edxposed.manager.XposedApp;
+import org.meowcat.edxposed.manager.repo.Module;
+import org.meowcat.edxposed.manager.repo.ModuleVersion;
+import org.meowcat.edxposed.manager.repo.ReleaseType;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class DownloadsUtil {
+ static final String MIME_TYPE_APK = "application/vnd.android.package-archive";
+ //private static final String MIME_TYPE_ZIP = "application/zip";
+ private static final Map mCallbacks = new HashMap<>();
+ @SuppressLint("StaticFieldLeak")
+ private static final XposedApp mApp = XposedApp.getInstance();
+ private static final SharedPreferences mPref = mApp
+ .getSharedPreferences("download_cache", Context.MODE_PRIVATE);
+
+ private static String DOWNLOAD_MODULES = "modules";
+
+ private static DownloadInfo add(Builder b) {
+ Context context = b.mContext;
+ removeAllForUrl(context, b.mUrl);
+
+ if (!b.mDialog) {
+ synchronized (mCallbacks) {
+ mCallbacks.put(b.mUrl, b.mCallback);
+ }
+ }
+
+ String savePath = "Download/EdXposedManager";
+ if (b.mModule) {
+ savePath += "/modules";
+ }
+
+ Request request = new Request(Uri.parse(b.mUrl));
+ request.setTitle(b.mTitle);
+ request.setMimeType(b.mMimeType.toString());
+ if (b.mSave) {
+ try {
+ request.setDestinationInExternalPublicDir(savePath, b.mTitle + b.mMimeType.getExtension());
+ } catch (IllegalStateException e) {
+ Toast.makeText(context, e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ } else if (b.mDestination != null) {
+ //noinspection ResultOfMethodCallIgnored
+ b.mDestination.getParentFile().mkdirs();
+ removeAllForLocalFile(context, b.mDestination);
+ request.setDestinationUri(Uri.fromFile(b.mDestination));
+ }
+
+ request.setNotificationVisibility(Request.VISIBILITY_VISIBLE);
+
+ DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+ long id = dm.enqueue(request);
+
+ if (b.mDialog) {
+ showDownloadDialog(b, id);
+ }
+
+ return getById(context, id);
+ }
+
+ private static File[] getDownloadDirs(String subDir) {
+ Context context = XposedApp.getInstance();
+ ArrayList dirs = new ArrayList<>(2);
+ for (File dir : ContextCompat.getExternalCacheDirs(context)) {
+ if (dir != null && EnvironmentCompat.getStorageState(dir).equals(Environment.MEDIA_MOUNTED)) {
+ dirs.add(new File(new File(dir, "downloads"), subDir));
+ }
+ }
+ dirs.add(new File(new File(context.getCacheDir(), "downloads"), subDir));
+ return dirs.toArray(new File[0]);
+ }
+
+ private static File getDownloadTarget(String subDir, String filename) {
+ return new File(getDownloadDirs(subDir)[0], filename);
+ }
+
+ private static File getDownloadTargetForUrl(String subDir, String url) {
+ return getDownloadTarget(subDir, Uri.parse(url).getLastPathSegment());
+ }
+
+ public static DownloadInfo addModule(Context context, String title, String url, boolean save, DownloadFinishedCallback callback) {
+ return new Builder(context)
+ .setTitle(title)
+ .setUrl(url)
+ .setDestinationFromUrl(DownloadsUtil.DOWNLOAD_MODULES)
+ .setCallback(callback)
+ .setSave(save)
+ .setModule(true)
+ .setMimeType(MIME_TYPES.APK)
+ .download();
+ }
+
+ private static void showDownloadDialog(final Builder b, final long id) {
+ final Context context = b.mContext;
+ final ProgressDialog dialog = new ProgressDialog(context);
+ dialog.setTitle(b.mTitle);
+ dialog.setMessage(context.getString(R.string.download_view_waiting));
+ dialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.download_view_cancel), (dialog1, which) -> dialog1.cancel());
+ dialog.setOnCancelListener(dialog12 -> removeById(context, id));
+
+ dialog.setProgress(0);
+ dialog.setCanceledOnTouchOutside(false);
+ dialog.setProgressNumberFormat(context.getString(R.string.download_progress));
+ dialog.show();
+
+ new Thread("DownloadDialog") {
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ return;
+ }
+
+ final DownloadInfo info = getById(context, id);
+ if (info == null) {
+ dialog.cancel();
+ return;
+ } else if (info.status == DownloadManager.STATUS_FAILED) {
+ dialog.cancel();
+ XposedApp.runOnUiThread(() -> Toast.makeText(context,
+ context.getString(R.string.download_view_failed, info.reason),
+ Toast.LENGTH_LONG).show());
+ return;
+ } else if (info.status == DownloadManager.STATUS_SUCCESSFUL) {
+ dialog.dismiss();
+ // Hack to reset stat information.
+ //noinspection ResultOfMethodCallIgnored
+ new File(info.localFilename).setExecutable(false);
+ if (b.mCallback != null) {
+ b.mCallback.onDownloadFinished(context, info);
+ }
+ return;
+ }
+
+ XposedApp.runOnUiThread(() -> {
+ if (info.totalSize <= 0 || info.status != DownloadManager.STATUS_RUNNING) {
+ dialog.setMessage(context.getString(R.string.download_view_waiting));
+ } else {
+ dialog.setMessage(context.getString(R.string.download_running));
+ dialog.setProgress(info.bytesDownloaded / 1024);
+ dialog.setMax(info.totalSize / 1024);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ public static ModuleVersion getStableVersion(Module m) {
+ for (int i = 0; i < m.versions.size(); i++) {
+ ModuleVersion mvTemp = m.versions.get(i);
+
+ if (mvTemp.relType == ReleaseType.STABLE) {
+ return mvTemp;
+ }
+ }
+ return null;
+ }
+
+ public static DownloadInfo getById(Context context, long id) {
+ DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+ Cursor c = dm.query(new Query().setFilterById(id));
+ if (!c.moveToFirst()) {
+ c.close();
+ return null;
+ }
+
+ int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI);
+ int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE);
+ int columnLastMod = c.getColumnIndexOrThrow(
+ DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
+ int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
+ int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
+ int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
+ int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
+ int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON);
+
+ int status = c.getInt(columnStatus);
+ String localFilename;
+ try {
+ localFilename = getFilenameFromUri(c.getString(columnLocalUri));
+ } catch (UnsupportedOperationException e) {
+ Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show();
+ return null;
+ }
+ if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) {
+ dm.remove(id);
+ c.close();
+ return null;
+ }
+
+ DownloadInfo info = new DownloadInfo(id, c.getString(columnUri),
+ c.getString(columnTitle), c.getLong(columnLastMod),
+ localFilename, status,
+ c.getInt(columnTotalSize), c.getInt(columnBytesDownloaded),
+ c.getInt(columnReason));
+ c.close();
+ return info;
+ }
+
+ public static DownloadInfo getLatestForUrl(Context context, String url) {
+ List all;
+ try {
+ all = getAllForUrl(context, url);
+ } catch (Throwable throwable) {
+ return null;
+ }
+ return Objects.requireNonNull(all).isEmpty() ? null : all.get(0);
+ }
+
+ private static List getAllForUrl(Context context, String url) {
+ DownloadManager dm = (DownloadManager) context
+ .getSystemService(Context.DOWNLOAD_SERVICE);
+ Cursor c = dm.query(new Query());
+ int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
+ int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI);
+ int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE);
+ int columnLastMod = c.getColumnIndexOrThrow(
+ DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
+ int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
+ int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
+ int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
+ int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
+ int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON);
+
+ List downloads = new ArrayList<>();
+ while (c.moveToNext()) {
+ if (!url.equals(c.getString(columnUri)))
+ continue;
+
+ int status = c.getInt(columnStatus);
+ String localFilename;
+ try {
+ localFilename = getFilenameFromUri(c.getString(columnLocalUri));
+ } catch (UnsupportedOperationException e) {
+ Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show();
+ return null;
+ }
+ if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) {
+ dm.remove(c.getLong(columnId));
+ continue;
+ }
+
+ downloads.add(new DownloadInfo(c.getLong(columnId),
+ c.getString(columnUri), c.getString(columnTitle),
+ c.getLong(columnLastMod), localFilename,
+ status, c.getInt(columnTotalSize),
+ c.getInt(columnBytesDownloaded), c.getInt(columnReason)));
+ }
+ c.close();
+
+ Collections.sort(downloads);
+ return downloads;
+ }
+
+ public static void removeById(Context context, long id) {
+ DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+ dm.remove(id);
+ }
+
+ private static void removeAllForUrl(Context context, String url) {
+ DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+ Cursor c = dm.query(new Query());
+ int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
+ int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI);
+
+ List idsList = new ArrayList<>(1);
+ while (c.moveToNext()) {
+ if (url.equals(c.getString(columnUri)))
+ idsList.add(c.getLong(columnId));
+ }
+ c.close();
+
+ if (idsList.isEmpty())
+ return;
+
+ long[] ids = new long[idsList.size()];
+ for (int i = 0; i < ids.length; i++)
+ ids[i] = idsList.get(i);
+
+ dm.remove(ids);
+ }
+
+ private static void removeAllForLocalFile(Context context, File file) {
+ //noinspection ResultOfMethodCallIgnored
+ file.delete();
+
+ String filename;
+ try {
+ filename = file.getCanonicalPath();
+ } catch (IOException e) {
+ Log.w(XposedApp.TAG, "Could not resolve path for " + file.getAbsolutePath(), e);
+ return;
+ }
+
+ DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+ Cursor c = dm.query(new Query());
+ int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
+ int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
+
+ List idsList = new ArrayList<>(1);
+ while (c.moveToNext()) {
+ String itemFilename;
+ try {
+ itemFilename = getFilenameFromUri(c.getString(columnLocalUri));
+ } catch (UnsupportedOperationException e) {
+ Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show();
+ itemFilename = null;
+ }
+ if (itemFilename != null) {
+ if (filename.equals(itemFilename)) {
+ idsList.add(c.getLong(columnId));
+ } else {
+ try {
+ if (filename.equals(new File(itemFilename).getCanonicalPath())) {
+ idsList.add(c.getLong(columnId));
+ }
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+ c.close();
+
+ if (idsList.isEmpty())
+ return;
+
+ long[] ids = new long[idsList.size()];
+ for (int i = 0; i < ids.length; i++)
+ ids[i] = idsList.get(i);
+
+ dm.remove(ids);
+ }
+
+// public static void removeOutdated(Context context, long cutoff) {
+// DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+// Cursor c = dm.query(new Query());
+// int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
+// int columnLastMod = c.getColumnIndexOrThrow(
+// DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
+//
+// List idsList = new ArrayList<>();
+// while (c.moveToNext()) {
+// if (c.getLong(columnLastMod) < cutoff)
+// idsList.add(c.getLong(columnId));
+// }
+// c.close();
+//
+// if (idsList.isEmpty())
+// return;
+//
+// long[] ids = new long[idsList.size()];
+// for (int i = 0; i < ids.length; i++)
+// ids[i] = idsList.get(0);
+//
+// dm.remove(ids);
+// }
+
+ public static void triggerDownloadFinishedCallback(Context context, long id) {
+ DownloadInfo info = getById(context, id);
+ if (info == null || info.status != DownloadManager.STATUS_SUCCESSFUL)
+ return;
+
+ DownloadFinishedCallback callback;
+ synchronized (mCallbacks) {
+ callback = mCallbacks.get(info.url);
+ }
+
+ if (callback == null)
+ return;
+
+ // Hack to reset stat information.
+ //noinspection ResultOfMethodCallIgnored
+ new File(info.localFilename).setExecutable(false);
+ callback.onDownloadFinished(context, info);
+ }
+
+ private static String getFilenameFromUri(String uriString) {
+ if (uriString == null) {
+ return null;
+ }
+ Uri uri = Uri.parse(uriString);
+ if (Objects.requireNonNull(uri.getScheme()).equals("file")) {
+ return uri.getPath();
+ } else if (uri.getScheme().equals("content")) {
+ Context context = XposedApp.getInstance();
+ try (Cursor c = context.getContentResolver().query(uri, new String[]{MediaStore.Files.FileColumns.DATA}, null, null, null)) {
+ Objects.requireNonNull(c).moveToFirst();
+ return c.getString(c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA));
+ }
+ } else {
+ throw new UnsupportedOperationException("Unexpected URI: " + uriString);
+ }
+ }
+
+ static SyncDownloadInfo downloadSynchronously(String url, File target) {
+ final boolean useNotModifiedTags = target.exists();
+
+ URLConnection connection = null;
+ InputStream in = null;
+ FileOutputStream out = null;
+ try {
+ connection = new URL(url).openConnection();
+ connection.setDoOutput(false);
+ connection.setConnectTimeout(30000);
+ connection.setReadTimeout(30000);
+
+ if (connection instanceof HttpURLConnection) {
+ // Disable transparent gzip encoding for gzipped files
+ if (url.endsWith(".gz")) {
+ connection.addRequestProperty("Accept-Encoding", "identity");
+ }
+
+ if (useNotModifiedTags) {
+ String modified = mPref.getString("download_" + url + "_modified", null);
+ String etag = mPref.getString("download_" + url + "_etag", null);
+
+ if (modified != null) {
+ connection.addRequestProperty("If-Modified-Since", modified);
+ }
+ if (etag != null) {
+ connection.addRequestProperty("If-None-Match", etag);
+ }
+ }
+ }
+
+ connection.connect();
+
+ if (connection instanceof HttpURLConnection) {
+ HttpURLConnection httpConnection = (HttpURLConnection) connection;
+ int responseCode = httpConnection.getResponseCode();
+ if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
+ return new SyncDownloadInfo(SyncDownloadInfo.STATUS_NOT_MODIFIED, null);
+ } else if (responseCode < 200 || responseCode >= 300) {
+ return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED,
+ mApp.getString(R.string.repo_download_failed_http,
+ url, responseCode,
+ httpConnection.getResponseMessage()));
+ }
+ }
+
+ in = connection.getInputStream();
+ out = new FileOutputStream(target);
+ byte[] buf = new byte[1024];
+ int read;
+ while ((read = in.read(buf)) != -1) {
+ out.write(buf, 0, read);
+ }
+
+ if (connection instanceof HttpURLConnection) {
+ HttpURLConnection httpConnection = (HttpURLConnection) connection;
+ String modified = httpConnection.getHeaderField("Last-Modified");
+ String etag = httpConnection.getHeaderField("ETag");
+
+ mPref.edit()
+ .putString("download_" + url + "_modified", modified)
+ .putString("download_" + url + "_etag", etag).apply();
+ }
+
+ return new SyncDownloadInfo(SyncDownloadInfo.STATUS_SUCCESS, null);
+
+ } catch (Throwable t) {
+ return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED,
+ mApp.getString(R.string.repo_download_failed, url,
+ t.getMessage()));
+
+ } finally {
+ if (connection instanceof HttpURLConnection)
+ ((HttpURLConnection) connection).disconnect();
+ if (in != null)
+ try {
+ in.close();
+ } catch (IOException ignored) {
+ }
+ if (out != null)
+ try {
+ out.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ static void clearCache(String url) {
+ if (url != null) {
+ mPref.edit().remove("download_" + url + "_modified")
+ .remove("download_" + url + "_etag").apply();
+ } else {
+ mPref.edit().clear().apply();
+ }
+ }
+
+ public enum MIME_TYPES {
+ APK {
+ @NonNull
+ public String toString() {
+ return MIME_TYPE_APK;
+ }
+
+ public String getExtension() {
+ return ".apk";
+ }
+ };
+// ZIP {
+// public String toString() {
+// return MIME_TYPE_ZIP;
+// }
+//
+// public String getExtension() {
+// return ".zip";
+// }
+// };
+
+ public String getExtension() {
+ return null;
+ }
+ }
+
+ public interface DownloadFinishedCallback {
+ void onDownloadFinished(Context context, DownloadInfo info);
+ }
+
+ public static class Builder {
+ private final Context mContext;
+ boolean mModule = false;
+ private String mTitle = null;
+ private String mUrl = null;
+ private DownloadFinishedCallback mCallback = null;
+ private MIME_TYPES mMimeType = MIME_TYPES.APK;
+ private File mDestination = null;
+ private boolean mDialog = false;
+ private boolean mSave = false;
+
+ public Builder(Context context) {
+ mContext = context;
+ }
+
+ public Builder setTitle(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ public Builder setUrl(String url) {
+ mUrl = url;
+ return this;
+ }
+
+ public Builder setCallback(DownloadFinishedCallback callback) {
+ mCallback = callback;
+ return this;
+ }
+
+ Builder setMimeType(@SuppressWarnings("SameParameterValue") MIME_TYPES mimeType) {
+ mMimeType = mimeType;
+ return this;
+ }
+
+ Builder setDestination(File file) {
+ mDestination = file;
+ return this;
+ }
+
+ Builder setDestinationFromUrl(String subDir) {
+ if (mUrl == null) {
+ throw new IllegalStateException("URL must be set first");
+ }
+ return setDestination(getDownloadTargetForUrl(subDir, mUrl));
+ }
+
+ public Builder setSave(boolean save) {
+ this.mSave = save;
+ return this;
+ }
+
+ public Builder setModule(boolean module) {
+ this.mModule = module;
+ return this;
+ }
+
+ public Builder setDialog(boolean dialog) {
+ mDialog = dialog;
+ return this;
+ }
+
+ public DownloadInfo download() {
+ return add(this);
+ }
+ }
+
+ public static class DownloadInfo implements Comparable {
+ public final long id;
+ public final String url;
+ public final String title;
+ public final String localFilename;
+ public final int status;
+ public final int totalSize;
+ public final int bytesDownloaded;
+ public final int reason;
+ final long lastModification;
+
+ private DownloadInfo(long id, String url, String title, long lastModification, String localFilename, int status, int totalSize, int bytesDownloaded, int reason) {
+ this.id = id;
+ this.url = url;
+ this.title = title;
+ this.lastModification = lastModification;
+ this.localFilename = localFilename;
+ this.status = status;
+ this.totalSize = totalSize;
+ this.bytesDownloaded = bytesDownloaded;
+ this.reason = reason;
+ }
+
+ @Override
+ public int compareTo(@NonNull DownloadInfo another) {
+ int compare = (int) (another.lastModification
+ - this.lastModification);
+ if (compare != 0)
+ return compare;
+ return this.url.compareTo(another.url);
+ }
+ }
+
+ public static class SyncDownloadInfo {
+ static final int STATUS_SUCCESS = 0;
+ static final int STATUS_NOT_MODIFIED = 1;
+ static final int STATUS_FAILED = 2;
+
+ public final int status;
+ final String errorMessage;
+
+ private SyncDownloadInfo(int status, String errorMessage) {
+ this.status = status;
+ this.errorMessage = errorMessage;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/HashUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/HashUtil.java
new file mode 100644
index 00000000..0322988b
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/HashUtil.java
@@ -0,0 +1,64 @@
+package org.meowcat.edxposed.manager.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class HashUtil {
+ private static String hash(String input, @SuppressWarnings("SameParameterValue") String algorithm) {
+ try {
+ MessageDigest md = MessageDigest.getInstance(algorithm);
+ byte[] messageDigest = md.digest(input.getBytes());
+ return toHexString(messageDigest);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ static String md5(String input) {
+ return hash(input, "MD5");
+ }
+
+// public static String sha1(String input) {
+// return hash(input, "SHA-1");
+// }
+
+ private static String hash(File file, @SuppressWarnings("SameParameterValue") String algorithm) throws IOException {
+ try {
+ MessageDigest md = MessageDigest.getInstance(algorithm);
+ InputStream is = new FileInputStream(file);
+ byte[] buffer = new byte[8192];
+ int read;
+ while ((read = is.read(buffer)) > 0) {
+ md.update(buffer, 0, read);
+ }
+ is.close();
+ byte[] messageDigest = md.digest();
+ return toHexString(messageDigest);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static String md5(File input) throws IOException {
+ return hash(input, "MD5");
+ }
+
+// public static String sha1(File input) throws IOException {
+// return hash(input, "SHA-1");
+// }
+
+ private static String toHexString(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ int unsignedB = b & 0xff;
+ if (unsignedB < 0x10)
+ sb.append("0");
+ sb.append(Integer.toHexString(unsignedB));
+ }
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/InstallApkUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/InstallApkUtil.java
new file mode 100644
index 00000000..c47bbf13
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/InstallApkUtil.java
@@ -0,0 +1,131 @@
+package org.meowcat.edxposed.manager.util;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+
+import androidx.core.content.FileProvider;
+
+import com.topjohnwu.superuser.Shell;
+
+import org.meowcat.edxposed.manager.R;
+import org.meowcat.edxposed.manager.XposedApp;
+
+import java.io.File;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+public class InstallApkUtil extends AsyncTask {
+
+ private static final int ERROR_ROOT_NOT_GRANTED = -99;
+
+ private final DownloadsUtil.DownloadInfo info;
+ @SuppressLint("StaticFieldLeak")
+ private final Context context;
+ private boolean isApkRootInstallOn;
+ private List output = new LinkedList<>();
+
+ public InstallApkUtil(Context context, DownloadsUtil.DownloadInfo info) {
+ this.context = context;
+ this.info = info;
+ }
+
+ public static String getAppLabel(ApplicationInfo info, PackageManager pm) {
+ try {
+ if (info.labelRes > 0) {
+ Resources res = pm.getResourcesForApplication(info);
+ Configuration config = new Configuration();
+ config.setLocale(Locale.getDefault());
+ res.updateConfiguration(config, res.getDisplayMetrics());
+ return res.getString(info.labelRes);
+ }
+ } catch (Exception ignored) {
+ }
+ return info.loadLabel(pm).toString();
+ }
+
+ static void installApkNormally(Context context, String localFilename) {
+ Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
+ installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ Uri uri;
+ if (Build.VERSION.SDK_INT >= 24) {
+ uri = FileProvider.getUriForFile(context, "moe.guo.edxpmanager.fileprovider", new File(localFilename));
+ installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ } else {
+ uri = Uri.fromFile(new File(localFilename));
+ }
+ installIntent.setDataAndType(uri, DownloadsUtil.MIME_TYPE_APK);
+ installIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.getApplicationInfo().packageName);
+ context.startActivity(installIntent);
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+
+ SharedPreferences prefs = XposedApp.getPreferences();
+ isApkRootInstallOn = prefs.getBoolean("install_with_su", false);
+
+ if (isApkRootInstallOn) {
+ NotificationUtil.showModuleInstallingNotification(info.title);
+ }
+ }
+
+ @Override
+ protected Integer doInBackground(Void... params) {
+ int returnCode = 0;
+ if (isApkRootInstallOn) {
+ try {
+ String path = "/data/local/tmp/";
+ String fileName = new File(info.localFilename).getName();
+ Shell.su("cat \"" + info.localFilename + "\">" + path + fileName).exec();
+ returnCode = Shell.su("pm install -r -f \"" + path + fileName + "\"").exec().getCode();
+ //noinspection ResultOfMethodCallIgnored
+ new File(path + fileName).delete();
+ } catch (IllegalStateException e) {
+ returnCode = ERROR_ROOT_NOT_GRANTED;
+ }
+ }
+ return returnCode;
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ super.onPostExecute(result);
+
+ if (isApkRootInstallOn) {
+ NotificationUtil.cancel(NotificationUtil.NOTIFICATION_MODULE_INSTALLING);
+
+ if (result.equals(ERROR_ROOT_NOT_GRANTED)) {
+ NotificationUtil.showModuleInstallNotification(R.string.installation_error, R.string.root_failed, info.localFilename);
+ return;
+ }
+
+ StringBuilder out = new StringBuilder();
+ for (String o : output) {
+ out.append(o);
+ out.append("\n");
+ }
+// Pattern failurePattern = Pattern.compile("(?m)^Failure\\s+\\[(.*?)]$");
+// Matcher failureMatcher = failurePattern.matcher(out);
+
+ if (result.equals(0)) {
+ NotificationUtil.showModuleInstallNotification(R.string.installation_successful, R.string.installation_successful_message, info.localFilename, info.title);
+ } else {
+ NotificationUtil.showModuleInstallNotification(R.string.installation_error, R.string.installation_error_message, info.localFilename, info.title, out);
+ installApkNormally(context, info.localFilename);
+ }
+ } else {
+ installApkNormally(context, info.localFilename);
+ }
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/LocaleUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/LocaleUtil.java
new file mode 100644
index 00000000..95997b83
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/LocaleUtil.java
@@ -0,0 +1,16 @@
+package org.meowcat.edxposed.manager.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+
+import java.util.Locale;
+
+public class LocaleUtil {
+ public static void setLocale(Context context, Locale locale) {
+ Resources resources = context.getResources();
+ Configuration configuration = resources.getConfiguration();
+ configuration.setLocale(locale);
+ resources.updateConfiguration(configuration, resources.getDisplayMetrics());
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/ModuleUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/ModuleUtil.java
new file mode 100644
index 00000000..5b03b052
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/ModuleUtil.java
@@ -0,0 +1,400 @@
+package org.meowcat.edxposed.manager.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.FileUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import org.meowcat.edxposed.manager.R;
+import org.meowcat.edxposed.manager.XposedApp;
+import org.meowcat.edxposed.manager.repo.ModuleVersion;
+import org.meowcat.edxposed.manager.repo.RepoDb;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@SuppressWarnings("OctalInteger")
+public final class ModuleUtil {
+ // xposedminversion below this
+ private static final String MODULES_LIST_FILE = XposedApp.BASE_DIR + "conf/modules.list";
+ private static final String PLAY_STORE_PACKAGE = "com.android.vending";
+ public static int MIN_MODULE_VERSION = 2; // reject modules with
+ private static ModuleUtil mInstance = null;
+ private final XposedApp mApp;
+ private final PackageManager mPm;
+ private final String mFrameworkPackageName;
+ private final List mListeners = new CopyOnWriteArrayList<>();
+ private SharedPreferences mPref;
+ private InstalledModule mFramework = null;
+ private Map mInstalledModules;
+ private boolean mIsReloading = false;
+ private Toast mToast;
+
+ private ModuleUtil() {
+ mApp = XposedApp.getInstance();
+ mPref = mApp.getSharedPreferences("enabled_modules", Context.MODE_PRIVATE);
+ mPm = mApp.getPackageManager();
+ mFrameworkPackageName = mApp.getPackageName();
+ }
+
+ public static synchronized ModuleUtil getInstance() {
+ if (mInstance == null) {
+ mInstance = new ModuleUtil();
+ mInstance.reloadInstalledModules();
+ }
+ return mInstance;
+ }
+
+ public static int extractIntPart(String str) {
+ int result = 0, length = str.length();
+ for (int offset = 0; offset < length; offset++) {
+ char c = str.charAt(offset);
+ if ('0' <= c && c <= '9')
+ result = result * 10 + (c - '0');
+ else
+ break;
+ }
+ return result;
+ }
+
+ @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
+ public void reloadInstalledModules() {
+ synchronized (this) {
+ if (mIsReloading)
+ return;
+ mIsReloading = true;
+ }
+
+ Map modules = new HashMap<>();
+ RepoDb.beginTransation();
+ try {
+ RepoDb.deleteAllInstalledModules();
+
+ for (PackageInfo pkg : mPm.getInstalledPackages(PackageManager.GET_META_DATA)) {
+ ApplicationInfo app = pkg.applicationInfo;
+ if (!app.enabled)
+ continue;
+
+ InstalledModule installed = null;
+ if (app.metaData != null && app.metaData.containsKey("xposedmodule")) {
+ installed = new InstalledModule(pkg, false);
+ modules.put(pkg.packageName, installed);
+ } else if (isFramework(pkg.packageName)) {
+ mFramework = installed = new InstalledModule(pkg, true);
+ }
+
+ if (installed != null)
+ RepoDb.insertInstalledModule(installed);
+ }
+
+ RepoDb.setTransactionSuccessful();
+ } finally {
+ RepoDb.endTransation();
+ }
+
+ mInstalledModules = modules;
+ synchronized (this) {
+ mIsReloading = false;
+ }
+ for (ModuleListener listener : mListeners) {
+ listener.onInstalledModulesReloaded(mInstance);
+ }
+ }
+
+ public InstalledModule reloadSingleModule(String packageName) {
+ PackageInfo pkg;
+ try {
+ pkg = mPm.getPackageInfo(packageName, PackageManager.GET_META_DATA);
+ } catch (NameNotFoundException e) {
+ RepoDb.deleteInstalledModule(packageName);
+ InstalledModule old = mInstalledModules.remove(packageName);
+ if (old != null) {
+ for (ModuleListener listener : mListeners) {
+ listener.onSingleInstalledModuleReloaded(mInstance, packageName, null);
+ }
+ }
+ return null;
+ }
+
+ ApplicationInfo app = pkg.applicationInfo;
+ if (app.enabled && app.metaData != null && app.metaData.containsKey("xposedmodule")) {
+ InstalledModule module = new InstalledModule(pkg, false);
+ RepoDb.insertInstalledModule(module);
+ mInstalledModules.put(packageName, module);
+ for (ModuleListener listener : mListeners) {
+ listener.onSingleInstalledModuleReloaded(mInstance, packageName,
+ module);
+ }
+ return module;
+ } else {
+ RepoDb.deleteInstalledModule(packageName);
+ InstalledModule old = mInstalledModules.remove(packageName);
+ if (old != null) {
+ for (ModuleListener listener : mListeners) {
+ listener.onSingleInstalledModuleReloaded(mInstance, packageName, null);
+ }
+ }
+ return null;
+ }
+ }
+
+ public synchronized boolean isLoading() {
+ return mIsReloading;
+ }
+
+ public InstalledModule getFramework() {
+ return mFramework;
+ }
+
+ public String getFrameworkPackageName() {
+ return mFrameworkPackageName;
+ }
+
+ private boolean isFramework(String packageName) {
+ return mFrameworkPackageName.equals(packageName);
+ }
+
+// public boolean isInstalled(String packageName) {
+// return mInstalledModules.containsKey(packageName) || isFramework(packageName);
+// }
+
+ public InstalledModule getModule(String packageName) {
+ return mInstalledModules.get(packageName);
+ }
+
+ public Map getModules() {
+ return mInstalledModules;
+ }
+
+ public void setModuleEnabled(String packageName, boolean enabled) {
+ if (enabled) {
+ mPref.edit().putInt(packageName, 1).apply();
+ } else {
+ mPref.edit().remove(packageName).apply();
+ }
+ }
+
+ public boolean isModuleEnabled(String packageName) {
+ return mPref.contains(packageName);
+ }
+
+ public List getEnabledModules() {
+ LinkedList result = new LinkedList<>();
+
+ for (String packageName : mPref.getAll().keySet()) {
+ InstalledModule module = getModule(packageName);
+ if (module != null)
+ result.add(module);
+ else
+ setModuleEnabled(packageName, false);
+ }
+
+ return result;
+ }
+
+ public synchronized void updateModulesList(boolean showToast) {
+ try {
+ Log.i(XposedApp.TAG, "ModuleUtil -> updating modules.list");
+ int installedXposedVersion = XposedApp.getXposedVersion();
+ boolean disabled = false;//StatusInstallerFragment.DISABLE_FILE.exists();
+ if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false) && !disabled && installedXposedVersion <= 0 && showToast) {
+ Toast.makeText(mApp, R.string.notinstalled, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ PrintWriter modulesList = new PrintWriter(MODULES_LIST_FILE);
+ PrintWriter enabledModulesList = new PrintWriter(XposedApp.ENABLED_MODULES_LIST_FILE);
+ List enabledModules = getEnabledModules();
+ for (InstalledModule module : enabledModules) {
+
+ if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false) && (!disabled && (module.minVersion > installedXposedVersion || module.minVersion < MIN_MODULE_VERSION)) && showToast) {
+ Toast.makeText(mApp, R.string.notinstalled, Toast.LENGTH_SHORT).show();
+ continue;
+ }
+
+ modulesList.println(module.app.sourceDir);
+
+ try {
+ String installer = mPm.getInstallerPackageName(module.app.packageName);
+ if (!PLAY_STORE_PACKAGE.equals(installer))
+ enabledModulesList.println(module.app.packageName);
+ } catch (Exception ignored) {
+ }
+ }
+ modulesList.close();
+ enabledModulesList.close();
+
+ FileUtils.setPermissions(MODULES_LIST_FILE, 00664, -1, -1);
+ FileUtils.setPermissions(XposedApp.ENABLED_MODULES_LIST_FILE, 00664, -1, -1);
+
+ if (showToast) {
+ showToast(R.string.xposed_module_list_updated);
+ }
+ } catch (IOException e) {
+ Log.e(XposedApp.TAG, "ModuleUtil -> cannot write " + MODULES_LIST_FILE, e);
+ Toast.makeText(mApp, "cannot write " + MODULES_LIST_FILE + e, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private void showToast(int message) {
+ if (mToast != null) {
+ mToast.cancel();
+ mToast = null;
+ }
+ mToast = Toast.makeText(mApp, mApp.getString(message), Toast.LENGTH_SHORT);
+ mToast.show();
+ }
+
+ public void addListener(ModuleListener listener) {
+ if (!mListeners.contains(listener))
+ mListeners.add(listener);
+ }
+
+ public void removeListener(ModuleListener listener) {
+ mListeners.remove(listener);
+ }
+
+ public interface ModuleListener {
+ /**
+ * Called whenever one (previously or now) installed module has been
+ * reloaded
+ */
+ void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module);
+
+ /**
+ * Called whenever all installed modules have been reloaded
+ */
+ void onInstalledModulesReloaded(ModuleUtil moduleUtil);
+ }
+
+ public class InstalledModule {
+ //private static final int FLAG_FORWARD_LOCK = 1 << 29;
+ public final String packageName;
+ public final String versionName;
+ public final long versionCode;
+ public final int minVersion;
+ public final long installTime;
+ public final long updateTime;
+ final boolean isFramework;
+ public ApplicationInfo app;
+ private String appName; // loaded lazyily
+ private String description; // loaded lazyily
+
+ private Drawable.ConstantState iconCache = null;
+
+ private InstalledModule(PackageInfo pkg, boolean isFramework) {
+ this.app = pkg.applicationInfo;
+ this.packageName = pkg.packageName;
+ this.isFramework = isFramework;
+ this.versionName = pkg.versionName;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ //noinspection deprecation
+ this.versionCode = pkg.versionCode;
+ } else {
+ this.versionCode = pkg.getLongVersionCode();
+ }
+ this.installTime = pkg.firstInstallTime;
+ this.updateTime = pkg.lastUpdateTime;
+
+ if (isFramework) {
+ this.minVersion = 0;
+ this.description = "";
+ } else {
+ int version = XposedApp.getXposedVersion();
+ if (version > 0 && XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) {
+ this.minVersion = version;
+ } else {
+ Object minVersionRaw = app.metaData.get("xposedminversion");
+ if (minVersionRaw instanceof Integer) {
+ this.minVersion = (Integer) minVersionRaw;
+ } else if (minVersionRaw instanceof String) {
+ this.minVersion = extractIntPart((String) minVersionRaw);
+ } else {
+ this.minVersion = 0;
+ }
+ }
+ }
+ }
+
+ public boolean isInstalledOnExternalStorage() {
+ return (app.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0;
+ }
+
+ /**
+ * @hide
+ */
+// public boolean isForwardLocked() {
+// return (app.flags & FLAG_FORWARD_LOCK) != 0;
+// }
+ public String getAppName() {
+ if (appName == null)
+ appName = app.loadLabel(mPm).toString();
+ return appName;
+ }
+
+ public String getDescription() {
+ if (this.description == null) {
+ Object descriptionRaw = app.metaData.get("xposeddescription");
+ String descriptionTmp = null;
+ if (descriptionRaw instanceof String) {
+ descriptionTmp = ((String) descriptionRaw).trim();
+ } else if (descriptionRaw instanceof Integer) {
+ try {
+ int resId = (Integer) descriptionRaw;
+ if (resId != 0)
+ descriptionTmp = mPm.getResourcesForApplication(app).getString(resId).trim();
+ } catch (Exception ignored) {
+ }
+ }
+ this.description = (descriptionTmp != null) ? descriptionTmp : "";
+ }
+ return this.description;
+ }
+
+ public boolean isUpdate(ModuleVersion version) {
+ return (version != null) && version.code > versionCode;
+ }
+
+ public Drawable getIcon() {
+ if (iconCache != null)
+ return iconCache.newDrawable();
+
+ Intent mIntent = new Intent(Intent.ACTION_MAIN);
+ //mIntent.addCategory(ModulesFragment.SETTINGS_CATEGORY);
+ mIntent.setPackage(app.packageName);
+ List ris = mPm.queryIntentActivities(mIntent, 0);
+
+ Drawable result;
+ if (ris == null || ris.size() <= 0)
+ result = app.loadIcon(mPm);
+ else
+ result = ris.get(0).activityInfo.loadIcon(mPm);
+ iconCache = result.getConstantState();
+
+ return result;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getAppName();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/NavUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/NavUtil.java
new file mode 100644
index 00000000..c2b87e09
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/NavUtil.java
@@ -0,0 +1,59 @@
+package org.meowcat.edxposed.manager.util;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.Browser;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.URLSpan;
+import android.text.util.Linkify;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.browser.customtabs.CustomTabsIntent;
+
+import org.meowcat.edxposed.manager.XposedApp;
+
+public final class NavUtil {
+
+ public static Uri parseURL(String str) {
+ if (str == null || str.isEmpty())
+ return null;
+
+ Spannable spannable = new SpannableString(str);
+ Linkify.addLinks(spannable, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES);
+
+ URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
+ return (spans.length > 0) ? Uri.parse(spans[0].getURL()) : null;
+ }
+
+ public static void startURL(Activity activity, Uri uri) {
+ if (!XposedApp.getPreferences().getBoolean("chrome_tabs", true)) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
+ activity.startActivity(intent);
+ return;
+ }
+
+ CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder();
+ customTabsIntent.setShowTitle(true);
+ customTabsIntent.setToolbarColor(XposedApp.getColor(activity));
+ customTabsIntent.build().launchUrl(activity, uri);
+ }
+
+ public static void startURL(Activity activity, String url) {
+ startURL(activity, parseURL(url));
+ }
+
+ @AnyThread
+ public static void showMessage(final @NonNull Context context, final CharSequence message) {
+ XposedApp.runOnUiThread(() -> new AlertDialog.Builder(context)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, null)
+ .show());
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/NotificationUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/NotificationUtil.java
new file mode 100644
index 00000000..9a0dbca9
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/NotificationUtil.java
@@ -0,0 +1,313 @@
+package org.meowcat.edxposed.manager.util;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.StringRes;
+import androidx.core.app.NotificationCompat;
+
+import com.topjohnwu.superuser.Shell;
+
+import org.meowcat.edxposed.manager.MainActivity;
+import org.meowcat.edxposed.manager.R;
+import org.meowcat.edxposed.manager.XposedApp;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public final class NotificationUtil {
+
+ public static final int NOTIFICATION_MODULE_NOT_ACTIVATED_YET = 0;
+ public static final int NOTIFICATION_MODULE_INSTALLING = 4;
+ private static final int NOTIFICATION_MODULES_UPDATED = 1;
+ private static final int NOTIFICATION_INSTALLER_UPDATE = 2;
+ private static final int NOTIFICATION_MODULE_INSTALLATION = 3;
+ private static final int PENDING_INTENT_OPEN_MODULES = 0;
+ private static final int PENDING_INTENT_OPEN_INSTALL = 1;
+ private static final int PENDING_INTENT_SOFT_REBOOT = 2;
+ private static final int PENDING_INTENT_REBOOT = 3;
+ private static final int PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT = 4;
+ private static final int PENDING_INTENT_ACTIVATE_MODULE = 5;
+ private static final int PENDING_INTENT_INSTALL_APK = 6;
+
+ private static final String COLORED_NOTIFICATION = "colored_notification";
+ private static final String HEADS_UP = "heads_up";
+ private static final String FRAGMENT_ID = "fragment";
+
+ private static final String NOTIFICATION_UPDATE_CHANNEL = "app_update_channel";
+ private static final String NOTIFICATION_MODULES_CHANNEL = "modules_channel";
+
+ private static Context sContext = null;
+ private static NotificationManager sNotificationManager;
+ private static SharedPreferences prefs;
+
+ public static void init() {
+ if (sContext != null) return;
+
+ sContext = XposedApp.getInstance();
+ prefs = XposedApp.getPreferences();
+ sNotificationManager = (NotificationManager) sContext.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(NOTIFICATION_UPDATE_CHANNEL, sContext.getString(R.string.download_section_update_available), NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationChannel channel1 = new NotificationChannel(NOTIFICATION_MODULES_CHANNEL, sContext.getString(R.string.nav_item_modules), NotificationManager.IMPORTANCE_DEFAULT);
+ sNotificationManager.createNotificationChannel(channel);
+ sNotificationManager.createNotificationChannel(channel1);
+ }
+ }
+
+ public static void cancel(int id) {
+ sNotificationManager.cancel(id);
+ }
+
+ public static void cancel(String tag, int id) {
+ sNotificationManager.cancel(tag, id);
+ }
+
+ public static void cancelAll() {
+ sNotificationManager.cancelAll();
+ }
+
+ public static void showNotActivatedNotification(String packageName, String appName) {
+ Intent intent = new Intent(sContext, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtra(FRAGMENT_ID, 1);
+ PendingIntent pModulesTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_MODULES, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ String title = sContext.getString(R.string.module_is_not_activated_yet);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(appName)
+ .setTicker(title).setContentIntent(pModulesTab)
+ .setVibrate(new long[]{0}).setAutoCancel(true)
+ .setSmallIcon(R.drawable.ic_notification);
+
+ if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21)
+ builder.setPriority(2);
+
+ if (prefs.getBoolean(COLORED_NOTIFICATION, false))
+ builder.setColor(XposedApp.getColor(sContext));
+
+ Intent iActivateAndReboot = new Intent(sContext, RebootReceiver.class);
+ iActivateAndReboot.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName);
+ PendingIntent pActivateAndReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT,
+ iActivateAndReboot, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ Intent iActivate = new Intent(sContext, RebootReceiver.class);
+ iActivate.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName);
+ iActivate.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE_AND_RETURN, true);
+ PendingIntent pActivate = PendingIntent.getBroadcast(sContext, PENDING_INTENT_ACTIVATE_MODULE,
+ iActivate, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle();
+ notiStyle.setBigContentTitle(title);
+ notiStyle.bigText(sContext.getString(R.string.module_is_not_activated_yet_detailed, appName));
+ builder.setStyle(notiStyle).setChannelId(NOTIFICATION_MODULES_CHANNEL);
+
+ // Only show the quick activation button if any module has been
+ // enabled before,
+ // to ensure that the user know the way to disable the module later.
+ if (!ModuleUtil.getInstance().getEnabledModules().isEmpty()) {
+ builder.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_apps, sContext.getString(R.string.activate_and_reboot), pActivateAndReboot).build());
+ builder.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_apps, sContext.getString(R.string.activate_only), pActivate).build());
+ }
+
+ sNotificationManager.notify(packageName, NOTIFICATION_MODULE_NOT_ACTIVATED_YET, builder.build());
+ }
+
+ public static void showModulesUpdatedNotification() {
+ Intent intent = new Intent(sContext, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(FRAGMENT_ID, 0);
+
+ PendingIntent pInstallTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_INSTALL,
+ intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ String title = sContext
+ .getString(R.string.xposed_module_updated_notification_title);
+ String message = sContext
+ .getString(R.string.xposed_module_updated_notification);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(message)
+ .setTicker(title).setContentIntent(pInstallTab)
+ .setVibrate(new long[]{0}).setAutoCancel(true)
+ .setSmallIcon(R.drawable.ic_notification);
+
+ if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21)
+ builder.setPriority(2);
+
+ if (prefs.getBoolean(COLORED_NOTIFICATION, false))
+ builder.setColor(XposedApp.getColor(sContext));
+
+ Intent iSoftReboot = new Intent(sContext, RebootReceiver.class);
+ iSoftReboot.putExtra(RebootReceiver.EXTRA_SOFT_REBOOT, true);
+ PendingIntent pSoftReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_SOFT_REBOOT,
+ iSoftReboot, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ Intent iReboot = new Intent(sContext, RebootReceiver.class);
+ PendingIntent pReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_REBOOT,
+ iReboot, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ builder.addAction(new NotificationCompat.Action.Builder(0, sContext.getString(R.string.reboot), pReboot).build());
+ builder.addAction(new NotificationCompat.Action.Builder(0, sContext.getString(R.string.soft_reboot), pSoftReboot).build());
+ builder.setChannelId(NOTIFICATION_MODULES_CHANNEL);
+
+ sNotificationManager.notify(null, NOTIFICATION_MODULES_UPDATED, builder.build());
+ }
+
+ static void showModuleInstallNotification(@StringRes int title, @StringRes int message, String path, Object... args) {
+ showModuleInstallNotification(sContext.getString(title), sContext.getString(message, args), path, title == R.string.installation_error);
+ }
+
+ private static void showModuleInstallNotification(String title, String message, String path, boolean error) {
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(
+ sContext).setContentTitle(title).setContentText(message)
+ .setVibrate(new long[]{0}).setAutoCancel(true)
+ .setSmallIcon(R.drawable.ic_notification);
+
+ if (error) {
+ Intent iInstallApk = new Intent(sContext, ApkReceiver.class);
+ iInstallApk.putExtra(ApkReceiver.EXTRA_APK_PATH, path);
+ PendingIntent pInstallApk = PendingIntent.getBroadcast(sContext, PENDING_INTENT_INSTALL_APK, iInstallApk, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ builder.addAction(new NotificationCompat.Action.Builder(0, sContext.getString(R.string.installation_apk_normal), pInstallApk).build());
+ }
+
+ if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21)
+ builder.setPriority(2);
+
+ if (prefs.getBoolean(COLORED_NOTIFICATION, false))
+ builder.setColor(XposedApp.getColor(sContext));
+
+ NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle();
+ notiStyle.setBigContentTitle(title);
+ notiStyle.bigText(message);
+ builder.setStyle(notiStyle).setChannelId(NOTIFICATION_MODULES_CHANNEL);
+
+ sNotificationManager.notify(null, NOTIFICATION_MODULE_INSTALLATION, builder.build());
+
+ new android.os.Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ cancel(NOTIFICATION_MODULE_INSTALLATION);
+ }
+ }, 10 * 1000);
+ }
+
+ public static void showModuleInstallingNotification(String appName) {
+ String title = sContext.getString(R.string.install_load);
+ String message = sContext.getString(R.string.install_load_apk, appName);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(message)
+ .setVibrate(new long[]{0}).setProgress(0, 0, true)
+ .setSmallIcon(R.drawable.ic_notification).setOngoing(true);
+
+ if (prefs.getBoolean(COLORED_NOTIFICATION, false))
+ builder.setColor(XposedApp.getColor(sContext));
+
+ NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle();
+ notiStyle.setBigContentTitle(title);
+ notiStyle.bigText(message);
+ builder.setStyle(notiStyle).setChannelId(NOTIFICATION_MODULES_CHANNEL);
+
+ sNotificationManager.notify(null, NOTIFICATION_MODULE_INSTALLING, builder.build());
+ }
+
+ public static void showInstallerUpdateNotification() {
+ Intent intent = new Intent(sContext, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(FRAGMENT_ID, 0);
+
+ PendingIntent pInstallTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_INSTALL,
+ intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ String title = sContext.getString(R.string.app_name);
+ String message = sContext.getString(R.string.newVersion);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(message)
+ .setTicker(title).setContentIntent(pInstallTab)
+ .setVibrate(new long[]{0}).setAutoCancel(true)
+ .setSmallIcon(R.drawable.ic_notification);
+
+ if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21)
+ builder.setPriority(2);
+
+ if (prefs.getBoolean(COLORED_NOTIFICATION, false))
+ builder.setColor(XposedApp.getColor(sContext));
+
+ NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle();
+ notiStyle.setBigContentTitle(title);
+ notiStyle.bigText(message);
+ builder.setStyle(notiStyle).setChannelId(NOTIFICATION_UPDATE_CHANNEL);
+
+ sNotificationManager.notify(null, NOTIFICATION_INSTALLER_UPDATE, builder.build());
+ }
+
+ public static class RebootReceiver extends BroadcastReceiver {
+ public static String EXTRA_SOFT_REBOOT = "soft";
+ public static String EXTRA_ACTIVATE_MODULE = "activate_module";
+ public static String EXTRA_ACTIVATE_MODULE_AND_RETURN = "activate_module_and_return";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ /*
+ * Close the notification bar in order to see the toast that module
+ * was enabled successfully. Furthermore, if SU permissions haven't
+ * been granted yet, the SU dialog will be prompted behind the
+ * expanded notification panel and is therefore not visible to the
+ * user.
+ */
+ sContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ cancelAll();
+
+ if (intent.hasExtra(EXTRA_ACTIVATE_MODULE)) {
+ String packageName = intent.getStringExtra(EXTRA_ACTIVATE_MODULE);
+ ModuleUtil moduleUtil = ModuleUtil.getInstance();
+ moduleUtil.setModuleEnabled(packageName, true);
+ moduleUtil.updateModulesList(false);
+ Toast.makeText(sContext, R.string.module_activated, Toast.LENGTH_SHORT).show();
+
+ if (intent.hasExtra(EXTRA_ACTIVATE_MODULE_AND_RETURN)) return;
+ }
+
+ if (!Shell.rootAccess()) {
+ Log.e(XposedApp.TAG, "NotificationUtil -> Could not start root shell");
+ return;
+ }
+
+ List messages = new LinkedList<>();
+ boolean isSoftReboot = intent.getBooleanExtra(EXTRA_SOFT_REBOOT,
+ false);
+ int returnCode = isSoftReboot
+ ? Shell.su("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote").exec().getCode()
+ : Shell.su("reboot").exec().getCode();
+
+ if (returnCode != 0) {
+ Log.e(XposedApp.TAG, "NotificationUtil -> Could not reboot");
+ }
+ }
+ }
+
+ public static class ApkReceiver extends BroadcastReceiver {
+ public static final String EXTRA_APK_PATH = "path";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ /*
+ * Close the notification bar in order to see the toast that module
+ * was enabled successfully. Furthermore, if SU permissions haven't
+ * been granted yet, the SU dialog will be prompted behind the
+ * expanded notification panel and is therefore not visible to the
+ * user.
+ */
+ sContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+
+ if (intent.hasExtra(EXTRA_APK_PATH)) {
+ String path = intent.getStringExtra(EXTRA_APK_PATH);
+ InstallApkUtil.installApkNormally(context, path);
+ }
+ NotificationUtil.cancel(NotificationUtil.NOTIFICATION_MODULE_INSTALLATION);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/PrefixedSharedPreferences.java b/app/src/main/java/org/meowcat/edxposed/manager/util/PrefixedSharedPreferences.java
new file mode 100644
index 00000000..4dd2d902
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/PrefixedSharedPreferences.java
@@ -0,0 +1,161 @@
+package org.meowcat.edxposed.manager.util;
+
+import android.annotation.SuppressLint;
+import android.content.SharedPreferences;
+
+import androidx.preference.PreferenceManager;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class PrefixedSharedPreferences implements SharedPreferences {
+ private final SharedPreferences mBase;
+ private final String mPrefix;
+
+ public PrefixedSharedPreferences(SharedPreferences base, String prefix) {
+ mBase = base;
+ mPrefix = prefix + "_";
+ }
+
+ public static void injectToPreferenceManager(PreferenceManager manager, String prefix) {
+ SharedPreferences prefixedPrefs = new PrefixedSharedPreferences(manager.getSharedPreferences(), prefix);
+
+ try {
+ Field fieldSharedPref = PreferenceManager.class.getDeclaredField("mSharedPreferences");
+ fieldSharedPref.setAccessible(true);
+ fieldSharedPref.set(manager, prefixedPrefs);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ @Override
+ public Map getAll() {
+ Map baseResult = mBase.getAll();
+ Map prefixedResult = new HashMap(baseResult);
+ for (Entry entry : baseResult.entrySet()) {
+ prefixedResult.put(mPrefix + entry.getKey(), entry.getValue());
+ }
+ return prefixedResult;
+ }
+
+ @Override
+ public String getString(String key, String defValue) {
+ return mBase.getString(mPrefix + key, defValue);
+ }
+
+ @Override
+ public Set getStringSet(String key, Set defValues) {
+ return mBase.getStringSet(mPrefix + key, defValues);
+ }
+
+ @Override
+ public int getInt(String key, int defValue) {
+ return mBase.getInt(mPrefix + key, defValue);
+ }
+
+ @Override
+ public long getLong(String key, long defValue) {
+ return mBase.getLong(mPrefix + key, defValue);
+ }
+
+ @Override
+ public float getFloat(String key, float defValue) {
+ return mBase.getFloat(mPrefix + key, defValue);
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defValue) {
+ return mBase.getBoolean(mPrefix + key, defValue);
+ }
+
+ @Override
+ public boolean contains(String key) {
+ return mBase.contains(mPrefix + key);
+ }
+
+ @SuppressLint("CommitPrefEdits")
+ @Override
+ public Editor edit() {
+ return new EditorImpl(mBase.edit());
+ }
+
+ @Override
+ public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ throw new UnsupportedOperationException("listeners are not supported in this implementation");
+ }
+
+ @Override
+ public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ throw new UnsupportedOperationException("listeners are not supported in this implementation");
+ }
+
+ private class EditorImpl implements Editor {
+ private final Editor mEditorBase;
+
+ public EditorImpl(Editor base) {
+ mEditorBase = base;
+ }
+
+ @Override
+ public Editor putString(String key, String value) {
+ mEditorBase.putString(mPrefix + key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putStringSet(String key, Set values) {
+ mEditorBase.putStringSet(mPrefix + key, values);
+ return this;
+ }
+
+ @Override
+ public Editor putInt(String key, int value) {
+ mEditorBase.putInt(mPrefix + key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putLong(String key, long value) {
+ mEditorBase.putLong(mPrefix + key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putFloat(String key, float value) {
+ mEditorBase.putFloat(mPrefix + key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putBoolean(String key, boolean value) {
+ mEditorBase.putBoolean(mPrefix + key, value);
+ return this;
+ }
+
+ @Override
+ public Editor remove(String key) {
+ mEditorBase.remove(mPrefix + key);
+ return this;
+ }
+
+ @Override
+ public Editor clear() {
+ mEditorBase.clear();
+ return this;
+ }
+
+ @Override
+ public boolean commit() {
+ return mEditorBase.commit();
+ }
+
+ @Override
+ public void apply() {
+ mEditorBase.apply();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/RepoLoader.java b/app/src/main/java/org/meowcat/edxposed/manager/util/RepoLoader.java
new file mode 100644
index 00000000..d98dc5b4
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/RepoLoader.java
@@ -0,0 +1,437 @@
+package org.meowcat.edxposed.manager.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.sqlite.SQLiteException;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import org.meowcat.edxposed.manager.R;
+import org.meowcat.edxposed.manager.XposedApp;
+import org.meowcat.edxposed.manager.repo.Module;
+import org.meowcat.edxposed.manager.repo.ModuleVersion;
+import org.meowcat.edxposed.manager.repo.ReleaseType;
+import org.meowcat.edxposed.manager.repo.RepoDb;
+import org.meowcat.edxposed.manager.repo.RepoParser;
+import org.meowcat.edxposed.manager.repo.RepoParser.RepoParserCallback;
+import org.meowcat.edxposed.manager.repo.Repository;
+import org.meowcat.edxposed.manager.util.DownloadsUtil.SyncDownloadInfo;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.GZIPInputStream;
+
+public class RepoLoader {
+ private static final int UPDATE_FREQUENCY = 24 * 60 * 60 * 1000;
+ private static String DEFAULT_REPOSITORIES;
+ private static RepoLoader mInstance = null;
+ private final List mListeners = new CopyOnWriteArrayList<>();
+ private final Map mLocalReleaseTypesCache = new HashMap<>();
+ private XposedApp mApp = null;
+ private SharedPreferences mPref;
+ private SharedPreferences mModulePref;
+ private ConnectivityManager mConMgr;
+ private boolean mIsLoading = false;
+ private boolean mReloadTriggeredOnce = false;
+ private Map mRepositories = null;
+ private ReleaseType mGlobalReleaseType;
+ private SwipeRefreshLayout mSwipeRefreshLayout;
+
+ private RepoLoader() {
+ mInstance = this;
+ mApp = XposedApp.getInstance();
+ mPref = mApp.getSharedPreferences("repo", Context.MODE_PRIVATE);
+ DEFAULT_REPOSITORIES = XposedApp.getPreferences().getBoolean("custom_list", false) ? "https://cdn.jsdelivr.net/gh/ElderDrivers/Repository-Website@gh-pages/assets/full.xml.gz" : "https://dl-xda.xposed.info/repo/full.xml.gz";
+ mModulePref = mApp.getSharedPreferences("module_settings", Context.MODE_PRIVATE);
+ mConMgr = (ConnectivityManager) mApp.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mGlobalReleaseType = ReleaseType.fromString(XposedApp.getPreferences().getString("release_type_global", "stable"));
+ refreshRepositories();
+ }
+
+ public static synchronized RepoLoader getInstance() {
+ if (mInstance == null)
+ new RepoLoader();
+ return mInstance;
+ }
+
+ private boolean refreshRepositories() {
+ mRepositories = RepoDb.getRepositories();
+
+ // Unlikely case (usually only during initial load): DB state doesn't
+ // fit to configuration
+ boolean needReload = false;
+ String[] config = (mPref.getString("repositories", DEFAULT_REPOSITORIES) + "").split("\\|");
+ if (mRepositories.size() != config.length) {
+ needReload = true;
+ } else {
+ int i = 0;
+ for (Repository repo : mRepositories.values()) {
+ if (!repo.url.equals(config[i++])) {
+ needReload = true;
+ break;
+ }
+ }
+ }
+
+ if (!needReload)
+ return false;
+
+ clear(false);
+ for (String url : config) {
+ RepoDb.insertRepository(url);
+ }
+ mRepositories = RepoDb.getRepositories();
+ return true;
+ }
+
+ public void setReleaseTypeGlobal(String relTypeString) {
+ ReleaseType relType = ReleaseType.fromString(relTypeString);
+ if (mGlobalReleaseType == relType)
+ return;
+
+ mGlobalReleaseType = relType;
+
+ // Updating the latest version for all modules takes a moment
+ new Thread("DBUpdate") {
+ @Override
+ public void run() {
+ RepoDb.updateAllModulesLatestVersion();
+ notifyListeners();
+ }
+ }.start();
+ }
+
+ public void setReleaseTypeLocal(String packageName, String relTypeString) {
+ ReleaseType relType = (!TextUtils.isEmpty(relTypeString)) ? ReleaseType.fromString(relTypeString) : null;
+
+ if (getReleaseTypeLocal(packageName) == relType)
+ return;
+
+ synchronized (mLocalReleaseTypesCache) {
+ mLocalReleaseTypesCache.put(packageName, relType);
+ }
+
+ RepoDb.updateModuleLatestVersion(packageName);
+ notifyListeners();
+ }
+
+ private ReleaseType getReleaseTypeLocal(String packageName) {
+ synchronized (mLocalReleaseTypesCache) {
+ if (mLocalReleaseTypesCache.containsKey(packageName))
+ return mLocalReleaseTypesCache.get(packageName);
+
+ String value = mModulePref.getString(packageName + "_release_type",
+ null);
+ ReleaseType result = (!TextUtils.isEmpty(value)) ? ReleaseType.fromString(value) : null;
+ mLocalReleaseTypesCache.put(packageName, result);
+ return result;
+ }
+ }
+
+ public Repository getRepository(long repoId) {
+ return mRepositories.get(repoId);
+ }
+
+ public Module getModule(String packageName) {
+ return RepoDb.getModuleByPackageName(packageName);
+ }
+
+ public ModuleVersion getLatestVersion(Module module) {
+ if (module == null || module.versions.isEmpty())
+ return null;
+
+ for (ModuleVersion version : module.versions) {
+ if (version.downloadLink != null && isVersionShown(version))
+ return version;
+ }
+ return null;
+ }
+
+ public boolean isVersionShown(ModuleVersion version) {
+ return version.relType
+ .ordinal() <= getMaxShownReleaseType(version.module.packageName).ordinal();
+ }
+
+ public ReleaseType getMaxShownReleaseType(String packageName) {
+ ReleaseType localSetting = getReleaseTypeLocal(packageName);
+ if (localSetting != null)
+ return localSetting;
+ else
+ return mGlobalReleaseType;
+ }
+
+ public void triggerReload(final boolean force) {
+ mReloadTriggeredOnce = true;
+
+ if (force) {
+ resetLastUpdateCheck();
+ } else {
+ long lastUpdateCheck = mPref.getLong("last_update_check", 0);
+ if (System.currentTimeMillis() < lastUpdateCheck + UPDATE_FREQUENCY)
+ return;
+ }
+
+ NetworkInfo netInfo = mConMgr.getActiveNetworkInfo();
+ if (netInfo == null || !netInfo.isConnected())
+ return;
+
+ synchronized (this) {
+ if (mIsLoading)
+ return;
+ mIsLoading = true;
+ }
+ mApp.updateProgressIndicator(mSwipeRefreshLayout);
+
+ new Thread("RepositoryReload") {
+ public void run() {
+ final List messages = new LinkedList<>();
+ boolean hasChanged = downloadAndParseFiles(messages);
+
+ mPref.edit().putLong("last_update_check", System.currentTimeMillis()).apply();
+
+ if (!messages.isEmpty()) {
+ XposedApp.runOnUiThread(new Runnable() {
+ public void run() {
+ for (String message : messages) {
+ Toast.makeText(mApp, message, Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+ }
+
+ if (hasChanged)
+ notifyListeners();
+
+ synchronized (this) {
+ mIsLoading = false;
+ }
+ mApp.updateProgressIndicator(mSwipeRefreshLayout);
+ }
+ }.start();
+ }
+
+ public void setSwipeRefreshLayout(SwipeRefreshLayout mSwipeRefreshLayout) {
+ this.mSwipeRefreshLayout = mSwipeRefreshLayout;
+ }
+
+ public void triggerFirstLoadIfNecessary() {
+ if (!mReloadTriggeredOnce)
+ triggerReload(false);
+ }
+
+ public void resetLastUpdateCheck() {
+ mPref.edit().remove("last_update_check").apply();
+ }
+
+ public synchronized boolean isLoading() {
+ return mIsLoading;
+ }
+
+ public void clear(boolean notify) {
+ synchronized (this) {
+ // TODO Stop reloading repository when it should be cleared
+ if (mIsLoading)
+ return;
+
+ RepoDb.deleteRepositories();
+ mRepositories = new LinkedHashMap(0);
+ DownloadsUtil.clearCache(null);
+ resetLastUpdateCheck();
+ }
+
+ if (notify)
+ notifyListeners();
+ }
+
+ public void setRepositories(String... repos) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < repos.length; i++) {
+ if (i > 0)
+ sb.append("|");
+ sb.append(repos[i]);
+ }
+ mPref.edit().putString("repositories", sb.toString()).apply();
+ if (refreshRepositories())
+ triggerReload(true);
+ }
+
+ public boolean hasModuleUpdates() {
+ return RepoDb.hasModuleUpdates();
+ }
+
+ public String getFrameworkUpdateVersion() {
+ return RepoDb.getFrameworkUpdateVersion();
+ }
+
+ private File getRepoCacheFile(String repo) {
+ String filename = "repo_" + HashUtil.md5(repo) + ".xml";
+ if (repo.endsWith(".gz"))
+ filename += ".gz";
+ return new File(mApp.getCacheDir(), filename);
+ }
+
+ private boolean downloadAndParseFiles(List messages) {
+ // These variables don't need to be atomic, just mutable
+ final AtomicBoolean hasChanged = new AtomicBoolean(false);
+ final AtomicInteger insertCounter = new AtomicInteger();
+ final AtomicInteger deleteCounter = new AtomicInteger();
+
+ for (Entry repoEntry : mRepositories.entrySet()) {
+ final long repoId = repoEntry.getKey();
+ final Repository repo = repoEntry.getValue();
+
+ String url = (repo.partialUrl != null && repo.version != null) ? String.format(repo.partialUrl, repo.version) : repo.url;
+
+ File cacheFile = getRepoCacheFile(url);
+ SyncDownloadInfo info = DownloadsUtil.downloadSynchronously(url,
+ cacheFile);
+
+ Log.i(XposedApp.TAG, String.format(
+ "RepoLoader -> Downloaded %s with status %d (error: %s), size %d bytes",
+ url, info.status, info.errorMessage, cacheFile.length()));
+
+ if (info.status != SyncDownloadInfo.STATUS_SUCCESS) {
+ if (info.errorMessage != null)
+ messages.add(info.errorMessage);
+ continue;
+ }
+
+ InputStream in = null;
+ RepoDb.beginTransation();
+ try {
+ in = new FileInputStream(cacheFile);
+ if (url.endsWith(".gz"))
+ in = new GZIPInputStream(in);
+
+ RepoParser.parse(in, new RepoParserCallback() {
+ @Override
+ public void onRepositoryMetadata(Repository repository) {
+ if (!repository.isPartial) {
+ RepoDb.deleteAllModules(repoId);
+ hasChanged.set(true);
+ }
+ }
+
+ @Override
+ public void onNewModule(Module module) {
+ RepoDb.insertModule(repoId, module);
+ hasChanged.set(true);
+ insertCounter.incrementAndGet();
+ }
+
+ @Override
+ public void onRemoveModule(String packageName) {
+ RepoDb.deleteModule(repoId, packageName);
+ hasChanged.set(true);
+ deleteCounter.decrementAndGet();
+ }
+
+ @Override
+ public void onCompleted(Repository repository) {
+ if (!repository.isPartial) {
+ RepoDb.updateRepository(repoId, repository);
+ repo.name = repository.name;
+ repo.partialUrl = repository.partialUrl;
+ repo.version = repository.version;
+ } else {
+ RepoDb.updateRepositoryVersion(repoId, repository.version);
+ repo.version = repository.version;
+ }
+
+ Log.i(XposedApp.TAG, String.format(
+ "RepoLoader -> Updated repository %s to version %s (%d new / %d removed modules)",
+ repo.url, repo.version, insertCounter.get(),
+ deleteCounter.get()));
+ }
+ });
+
+ RepoDb.setTransactionSuccessful();
+ } catch (SQLiteException e) {
+ XposedApp.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ /*new MaterialDialog.Builder(DownloadFragment.sActivity)
+ .title(R.string.restart_needed)
+ .content(R.string.cache_cleaned)
+ .onPositive(new MaterialDialog.SingleButtonCallback() {
+ @Override
+ public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
+ Intent i = new Intent(DownloadFragment.sActivity, WelcomeActivity.class);
+ i.putExtra("fragment", 2);
+
+ PendingIntent pi = PendingIntent.getActivity(DownloadFragment.sActivity, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ AlarmManager mgr = (AlarmManager) mApp.getSystemService(Context.ALARM_SERVICE);
+ mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pi);
+ System.exit(0);
+ }
+ })
+ .positiveText(R.string.ok)
+ .canceledOnTouchOutside(false)
+ .show();*/
+ }
+ });
+
+ DownloadsUtil.clearCache(url);
+ } catch (Throwable t) {
+ Log.e(XposedApp.TAG, "RepoLoader -> Cannot load repository from " + url, t);
+ messages.add(mApp.getString(R.string.repo_load_failed, url, t.getMessage()));
+ DownloadsUtil.clearCache(url);
+ } finally {
+ if (in != null)
+ try {
+ in.close();
+ } catch (IOException ignored) {
+ }
+ cacheFile.delete();
+ RepoDb.endTransation();
+ }
+ }
+
+ // TODO Set ModuleColumns.PREFERRED for modules which appear in multiple
+ // repositories
+ return hasChanged.get();
+ }
+
+ public void addListener(RepoListener listener, boolean triggerImmediately) {
+ if (!mListeners.contains(listener))
+ mListeners.add(listener);
+
+ if (triggerImmediately)
+ listener.onRepoReloaded(this);
+ }
+
+ public void removeListener(RepoListener listener) {
+ mListeners.remove(listener);
+ }
+
+ private void notifyListeners() {
+ for (RepoListener listener : mListeners) {
+ listener.onRepoReloaded(mInstance);
+ }
+ }
+
+ public interface RepoListener {
+ /**
+ * Called whenever the list of modules from repositories has been
+ * successfully reloaded
+ */
+ void onRepoReloaded(RepoLoader loader);
+ }
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/ToastUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/ToastUtil.java
new file mode 100644
index 00000000..fa8d602a
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/ToastUtil.java
@@ -0,0 +1,18 @@
+package org.meowcat.edxposed.manager.util;
+
+import android.content.Context;
+import android.widget.Toast;
+
+import androidx.annotation.StringRes;
+
+public class ToastUtil {
+
+ public static void showShortToast(Context context, @StringRes int resId) {
+ Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
+ }
+
+ public static void showLongToast(Context context, @StringRes int resId) {
+ Toast.makeText(context, resId, Toast.LENGTH_LONG).show();
+ }
+
+}
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/CustomTabsURLSpan.java b/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/CustomTabsURLSpan.java
new file mode 100644
index 00000000..cd75146c
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/CustomTabsURLSpan.java
@@ -0,0 +1,26 @@
+package org.meowcat.edxposed.manager.util.chrome;
+
+import android.app.Activity;
+import android.text.style.URLSpan;
+import android.view.View;
+
+import org.meowcat.edxposed.manager.util.NavUtil;
+
+/**
+ * Created by Nikola D. on 12/23/2015.
+ */
+public class CustomTabsURLSpan extends URLSpan {
+
+ private Activity activity;
+
+ CustomTabsURLSpan(Activity activity, String url) {
+ super(url);
+ this.activity = activity;
+ }
+
+ @Override
+ public void onClick(View widget) {
+ String url = getURL();
+ NavUtil.startURL(activity, url);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/LinkTransformationMethod.java b/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/LinkTransformationMethod.java
new file mode 100644
index 00000000..40eea65c
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/LinkTransformationMethod.java
@@ -0,0 +1,50 @@
+package org.meowcat.edxposed.manager.util.chrome;
+
+import android.app.Activity;
+import android.graphics.Rect;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.method.TransformationMethod;
+import android.text.style.URLSpan;
+import android.text.util.Linkify;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Created by Nikola D. on 12/23/2015.
+ */
+public class LinkTransformationMethod implements TransformationMethod {
+
+ private Activity activity;
+
+ public LinkTransformationMethod(Activity activity) {
+ this.activity = activity;
+ }
+
+ @Override
+ public CharSequence getTransformation(CharSequence source, View view) {
+ if (view instanceof TextView) {
+ TextView textView = (TextView) view;
+ Linkify.addLinks(textView, Linkify.WEB_URLS);
+ if (textView.getText() == null || !(textView.getText() instanceof Spannable)) {
+ return source;
+ }
+ Spannable text = (Spannable) textView.getText();
+ URLSpan[] spans = text.getSpans(0, textView.length(), URLSpan.class);
+ for (int i = spans.length - 1; i >= 0; i--) {
+ URLSpan oldSpan = spans[i];
+ int start = text.getSpanStart(oldSpan);
+ int end = text.getSpanEnd(oldSpan);
+ String url = oldSpan.getURL();
+ text.removeSpan(oldSpan);
+ text.setSpan(new CustomTabsURLSpan(activity, url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ return text;
+ }
+ return source;
+ }
+
+ @Override
+ public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, Rect previouslyFocusedRect) {
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/json/JSONUtils.java b/app/src/main/java/org/meowcat/edxposed/manager/util/json/JSONUtils.java
new file mode 100644
index 00000000..5e771003
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/json/JSONUtils.java
@@ -0,0 +1,97 @@
+package org.meowcat.edxposed.manager.util.json;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.List;
+
+public class JSONUtils {
+
+ public static final String JSON_LINK = "http://edxp.meowcat.org/assets/version.json";
+
+ public static String getFileContent(String url) throws IOException {
+ HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection();
+ c.setRequestMethod("GET");
+ c.setInstanceFollowRedirects(false);
+ c.setDoOutput(false);
+ c.connect();
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream()));
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ sb.append(line);
+ }
+ br.close();
+
+ return sb.toString();
+ }
+
+// private static String getLatestVersion() throws IOException {
+// String site = getFileContent("http://dl-xda.xposed.info/framework/sdk" + Build.VERSION.SDK_INT + "/arm/");
+//
+// Pattern pattern = Pattern.compile("(href=\")([^?\"]*)\\.zip");
+// Matcher matcher = pattern.matcher(site);
+// String last = "";
+// while (matcher.find()) {
+// if (matcher.group().contains("test")) continue;
+// last = matcher.group();
+// }
+// last = last.replace("href=\"", "");
+// String[] file = last.split("-");
+//
+// return file[1].replace("v", "");
+// }
+//
+// public static String listZip() {
+// String latest;
+// try {
+// latest = getLatestVersion();
+// } catch (IOException e) {
+// // Got 404 response; no official Xposed zips available
+// return "";
+// }
+//
+// StringBuilder newJson = new StringBuilder(",\"" + Build.VERSION.SDK_INT + "\": [");
+// String[] arch = new String[]{
+// "arm",
+// "arm64",
+// "x86"
+// };
+//
+// for (String a : arch) {
+// newJson.append(installerToString(latest, a)).append(",");
+// }
+//
+// newJson = new StringBuilder(newJson.substring(0, newJson.length() - 1));
+// newJson.append("]");
+//
+// return newJson.toString();
+// }
+//
+// private static String installerToString(String latest, String architecture) {
+// String filename = "xposed-v" + latest + "-sdk" + Build.VERSION.SDK_INT + "-" + architecture;
+//
+// XposedZip installer = new XposedZip();
+// installer.name = filename;
+// installer.version = latest;
+// installer.architecture = architecture;
+// installer.link = "http://dl-xda.xposed.info/framework/sdk" + Build.VERSION.SDK_INT + "/" + architecture + "/" + filename + ".zip";
+//
+// return new Gson().toJson(installer);
+// }
+
+ public class XposedJson {
+ public List tabs;
+ public ApkRelease apk;
+ }
+
+ public class ApkRelease {
+ public String version;
+ public String changelog;
+ public String link;
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedTab.java b/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedTab.java
new file mode 100644
index 00000000..3c8b17a9
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedTab.java
@@ -0,0 +1,91 @@
+package org.meowcat.edxposed.manager.util.json;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
+public class XposedTab implements Parcelable {
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public XposedTab createFromParcel(Parcel in) {
+ return new XposedTab(in);
+ }
+
+ @Override
+ public XposedTab[] newArray(int size) {
+ return new XposedTab[size];
+ }
+ };
+
+ public List sdks = new ArrayList<>();
+ public String name;
+ public String author;
+ public String description;
+ public String support;
+ public String notice;
+ public boolean stable;
+ public boolean official;
+ public List installers = new ArrayList<>();
+ public List uninstallers = new ArrayList<>();
+// private HashMap compatibility = new HashMap<>();
+// private HashMap incompatibility = new HashMap<>();
+
+// public XposedTab() {
+// }
+
+ private XposedTab(Parcel in) {
+ name = in.readString();
+ author = in.readString();
+ description = in.readString();
+ support = in.readString();
+ notice = in.readString();
+ stable = in.readByte() != 0;
+ official = in.readByte() != 0;
+ }
+
+// public String getNotice() {
+// if (notice == null) return "";
+// return notice.get(Integer.toString(Build.VERSION.SDK_INT));
+// }
+
+// public String getCompatibility() {
+// if (compatibility == null) return "";
+// return compatibility.get(Integer.toString(Build.VERSION.SDK_INT));
+// }
+//
+// public String getIncompatibility() {
+// if (incompatibility == null) return "";
+// return incompatibility.get(Integer.toString(Build.VERSION.SDK_INT));
+// }
+
+// public String getSupport() {
+// if (support == null) return "";
+// return support.get(Integer.toString(Build.VERSION.SDK_INT));
+// }
+//
+// public List getInstallers() {
+// if (support == null) return new ArrayList<>();
+// return installers.get(Integer.toString(Build.VERSION.SDK_INT));
+// }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(name);
+ dest.writeString(author);
+ dest.writeString(description);
+ dest.writeString(support);
+ dest.writeString(notice);
+ dest.writeByte((byte) (stable ? 1 : 0));
+ dest.writeByte((byte) (official ? 1 : 0));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedZip.java b/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedZip.java
new file mode 100644
index 00000000..b9f68967
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedZip.java
@@ -0,0 +1,66 @@
+package org.meowcat.edxposed.manager.util.json;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.List;
+
+public class XposedZip {
+
+ public String name;
+ public String link;
+ public String version;
+ public String description;
+
+ public static class MyAdapter extends ArrayAdapter {
+
+ private final Context context;
+ List list;
+
+ public MyAdapter(Context context, List objects) {
+ super(context, android.R.layout.simple_dropdown_item_1line, objects);
+ this.context = context;
+ this.list = objects;
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ return getMyView(parent, position);
+ }
+
+ @Override
+ public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ return getMyView(parent, position);
+ }
+
+ private View getMyView(ViewGroup parent, int position) {
+ View row;
+ ItemHolder holder = new ItemHolder();
+
+ LayoutInflater inflater = ((Activity) context).getLayoutInflater();
+ row = inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
+
+ holder.name = row.findViewById(android.R.id.text1);
+
+ row.setTag(holder);
+
+ holder.name.setText(list.get(position).name);
+ return row;
+ }
+
+ private class ItemHolder {
+ TextView name;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/widget/DownloadView.java b/app/src/main/java/org/meowcat/edxposed/manager/widget/DownloadView.java
new file mode 100644
index 00000000..142a10e4
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/widget/DownloadView.java
@@ -0,0 +1,257 @@
+package org.meowcat.edxposed.manager.widget;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.DownloadManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.core.app.ActivityCompat;
+import androidx.fragment.app.Fragment;
+
+import org.meowcat.edxposed.manager.R;
+import org.meowcat.edxposed.manager.util.DownloadsUtil;
+import org.meowcat.edxposed.manager.util.DownloadsUtil.DownloadFinishedCallback;
+
+import java.util.Objects;
+
+import static org.meowcat.edxposed.manager.XposedApp.WRITE_EXTERNAL_PERMISSION;
+
+public class DownloadView extends LinearLayout {
+ @SuppressLint("StaticFieldLeak")
+ public static Button mClickedButton;
+ private final Button btnDownload;
+ private final Button btnDownloadCancel;
+ private final Button btnInstall;
+ private final Button btnRemove;
+ private final Button btnSave;
+ private final ProgressBar progressBar;
+ private final TextView txtInfo;
+ public Fragment fragment;
+ private DownloadsUtil.DownloadInfo mInfo = null;
+ private String mUrl = null;
+ private final Runnable refreshViewRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mUrl == null) {
+ btnDownload.setVisibility(View.GONE);
+ btnSave.setVisibility(View.GONE);
+ btnDownloadCancel.setVisibility(View.GONE);
+ btnRemove.setVisibility(View.GONE);
+ btnInstall.setVisibility(View.GONE);
+ progressBar.setVisibility(View.GONE);
+ txtInfo.setVisibility(View.VISIBLE);
+ txtInfo.setText(R.string.download_view_no_url);
+ } else if (mInfo == null) {
+ btnDownload.setVisibility(View.VISIBLE);
+ btnSave.setVisibility(View.VISIBLE);
+ btnDownloadCancel.setVisibility(View.GONE);
+ btnRemove.setVisibility(View.GONE);
+ btnInstall.setVisibility(View.GONE);
+ progressBar.setVisibility(View.GONE);
+ txtInfo.setVisibility(View.GONE);
+ } else {
+ switch (mInfo.status) {
+ case DownloadManager.STATUS_PENDING:
+ case DownloadManager.STATUS_PAUSED:
+ case DownloadManager.STATUS_RUNNING:
+ btnDownload.setVisibility(View.GONE);
+ btnSave.setVisibility(View.GONE);
+ btnDownloadCancel.setVisibility(View.VISIBLE);
+ btnRemove.setVisibility(View.GONE);
+ btnInstall.setVisibility(View.GONE);
+ progressBar.setVisibility(View.VISIBLE);
+ txtInfo.setVisibility(View.VISIBLE);
+ if (mInfo.totalSize <= 0 || mInfo.status != DownloadManager.STATUS_RUNNING) {
+ progressBar.setIndeterminate(true);
+ txtInfo.setText(R.string.download_view_waiting);
+ } else {
+ progressBar.setIndeterminate(false);
+ progressBar.setMax(mInfo.totalSize);
+ progressBar.setProgress(mInfo.bytesDownloaded);
+ txtInfo.setText(getContext().getString(
+ R.string.download_view_running,
+ mInfo.bytesDownloaded / 1024,
+ mInfo.totalSize / 1024));
+ }
+ break;
+
+ case DownloadManager.STATUS_FAILED:
+ btnDownload.setVisibility(View.VISIBLE);
+ btnSave.setVisibility(View.VISIBLE);
+ btnDownloadCancel.setVisibility(View.GONE);
+ btnRemove.setVisibility(View.GONE);
+ btnInstall.setVisibility(View.GONE);
+ progressBar.setVisibility(View.GONE);
+ txtInfo.setVisibility(View.VISIBLE);
+ txtInfo.setText(getContext().getString(
+ R.string.download_view_failed, mInfo.reason));
+ break;
+
+ case DownloadManager.STATUS_SUCCESSFUL:
+ btnDownload.setVisibility(View.GONE);
+ btnSave.setVisibility(View.GONE);
+ btnDownloadCancel.setVisibility(View.GONE);
+ btnRemove.setVisibility(View.VISIBLE);
+ btnInstall.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(View.GONE);
+ txtInfo.setVisibility(View.VISIBLE);
+ txtInfo.setText(R.string.download_view_successful);
+ break;
+ }
+ }
+ }
+ };
+ private String mTitle = null;
+ private DownloadFinishedCallback mCallback = null;
+
+ public DownloadView(Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ setFocusable(false);
+ setOrientation(LinearLayout.VERTICAL);
+
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ Objects.requireNonNull(inflater).inflate(R.layout.download_view, this, true);
+
+ btnDownload = findViewById(R.id.btnDownload);
+ btnDownloadCancel = findViewById(R.id.btnDownloadCancel);
+ btnRemove = findViewById(R.id.btnRemove);
+ btnInstall = findViewById(R.id.btnInstall);
+ btnSave = findViewById(R.id.save);
+
+ btnDownload.setOnClickListener(v -> {
+ mClickedButton = btnDownload;
+
+ mInfo = DownloadsUtil.addModule(getContext(), mTitle, mUrl, false, mCallback);
+ refreshViewFromUiThread();
+
+ if (mInfo != null)
+ new DownloadMonitor().start();
+ });
+
+ btnSave.setOnClickListener(v -> {
+ mClickedButton = btnSave;
+
+ if (checkPermissions())
+ return;
+
+ mInfo = DownloadsUtil.addModule(getContext(), mTitle, mUrl, true, (context1, info) -> Toast.makeText(context1, context1.getString(R.string.module_saved, info.localFilename), Toast.LENGTH_SHORT).show());
+ });
+
+ btnDownloadCancel.setOnClickListener(v -> {
+ if (mInfo == null)
+ return;
+
+ DownloadsUtil.removeById(getContext(), mInfo.id);
+ // UI update will happen automatically by the DownloadMonitor
+ });
+
+ btnRemove.setOnClickListener(v -> {
+ if (mInfo == null)
+ return;
+
+ DownloadsUtil.removeById(getContext(), mInfo.id);
+ // UI update will happen automatically by the DownloadMonitor
+ });
+
+ btnInstall.setOnClickListener(v -> {
+ if (mCallback == null)
+ return;
+
+ mCallback.onDownloadFinished(getContext(), mInfo);
+ });
+
+ progressBar = findViewById(R.id.progress);
+ txtInfo = findViewById(R.id.txtInfo);
+
+ refreshViewFromUiThread();
+ }
+
+ private boolean checkPermissions() {
+ if (ActivityCompat.checkSelfPermission(this.getContext(),
+ Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ fragment.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION);
+ return true;
+ }
+ return false;
+ }
+
+ private void refreshViewFromUiThread() {
+ refreshViewRunnable.run();
+ }
+
+ private void refreshView() {
+ post(refreshViewRunnable);
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public void setUrl(String url) {
+ mUrl = url;
+
+ if (mUrl != null)
+ mInfo = DownloadsUtil.getLatestForUrl(getContext(), mUrl);
+ else
+ mInfo = null;
+
+ refreshView();
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public void setTitle(String title) {
+ this.mTitle = title;
+ }
+
+ @SuppressWarnings("unused")
+ public DownloadFinishedCallback getDownloadFinishedCallback() {
+ return mCallback;
+ }
+
+ public void setDownloadFinishedCallback(DownloadFinishedCallback downloadFinishedCallback) {
+ this.mCallback = downloadFinishedCallback;
+ }
+
+ private class DownloadMonitor extends Thread {
+ DownloadMonitor() {
+ super("DownloadMonitor");
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ return;
+ }
+
+ try {
+ mInfo = DownloadsUtil.getById(getContext(), mInfo.id);
+ } catch (NullPointerException ignored) {
+ }
+
+ refreshView();
+ if (mInfo == null)
+ return;
+
+ if (mInfo.status != DownloadManager.STATUS_PENDING
+ && mInfo.status != DownloadManager.STATUS_PAUSED
+ && mInfo.status != DownloadManager.STATUS_RUNNING)
+ return;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/widget/IntegerListPreference.java b/app/src/main/java/org/meowcat/edxposed/manager/widget/IntegerListPreference.java
new file mode 100644
index 00000000..ac5c98b8
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/widget/IntegerListPreference.java
@@ -0,0 +1,60 @@
+package org.meowcat.edxposed.manager.widget;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.AttributeSet;
+
+public class IntegerListPreference extends com.takisoft.preferencex.SimpleMenuPreference {
+ public IntegerListPreference(Context context) {
+ super(context);
+ }
+
+ public IntegerListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public static int getIntValue(String value) {
+ if (value == null)
+ return 0;
+
+ return (int) ((value.startsWith("0x"))
+ ? Long.parseLong(value.substring(2), 16)
+ : Long.parseLong(value));
+ }
+
+ @Override
+ public void setValue(String value) {
+ super.setValue(value);
+ notifyChanged();
+ }
+
+ @Override
+ protected boolean persistString(String value) {
+ return value != null && persistInt(getIntValue(value));
+
+ }
+
+ @Override
+ protected String getPersistedString(String defaultReturnValue) {
+ SharedPreferences pref = getPreferenceManager().getSharedPreferences();
+ String key = getKey();
+ if (!shouldPersist() || !pref.contains(key))
+ return defaultReturnValue;
+
+ return String.valueOf(pref.getInt(key, 0));
+ }
+
+ @Override
+ public int findIndexOfValue(String value) {
+ CharSequence[] entryValues = getEntryValues();
+ int intValue = getIntValue(value);
+ if (value != null && entryValues != null) {
+ for (int i = entryValues.length - 1; i >= 0; i--) {
+ if (getIntValue(entryValues[i].toString()) == intValue) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/meowcat/edxposed/manager/widget/ListPreferenceSummaryFix.java b/app/src/main/java/org/meowcat/edxposed/manager/widget/ListPreferenceSummaryFix.java
new file mode 100644
index 00000000..ca183c1d
--- /dev/null
+++ b/app/src/main/java/org/meowcat/edxposed/manager/widget/ListPreferenceSummaryFix.java
@@ -0,0 +1,22 @@
+package org.meowcat.edxposed.manager.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.preference.ListPreference;
+
+public class ListPreferenceSummaryFix extends ListPreference {
+ public ListPreferenceSummaryFix(Context context) {
+ super(context);
+ }
+
+ public ListPreferenceSummaryFix(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void setValue(String value) {
+ super.setValue(value);
+ notifyChanged();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..1f6bb290
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_android.xml b/app/src/main/res/drawable/ic_android.xml
new file mode 100644
index 00000000..37cdbbf1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_android.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_apps.xml b/app/src/main/res/drawable/ic_apps.xml
new file mode 100644
index 00000000..2d903a92
--- /dev/null
+++ b/app/src/main/res/drawable/ic_apps.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_assignment.xml b/app/src/main/res/drawable/ic_assignment.xml
new file mode 100644
index 00000000..6fd06e9d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_assignment.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_bug.xml b/app/src/main/res/drawable/ic_bug.xml
new file mode 100644
index 00000000..7b4e21c1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bug.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml
new file mode 100644
index 00000000..7d778e52
--- /dev/null
+++ b/app/src/main/res/drawable/ic_check_circle.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_chip.xml b/app/src/main/res/drawable/ic_chip.xml
new file mode 100644
index 00000000..67847c5f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_chip.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_description.xml b/app/src/main/res/drawable/ic_description.xml
new file mode 100644
index 00000000..d87d4019
--- /dev/null
+++ b/app/src/main/res/drawable/ic_description.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_donate.xml b/app/src/main/res/drawable/ic_donate.xml
new file mode 100644
index 00000000..4cc67358
--- /dev/null
+++ b/app/src/main/res/drawable/ic_donate.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml
new file mode 100644
index 00000000..cce259fa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_error.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_framework.xml b/app/src/main/res/drawable/ic_framework.xml
new file mode 100644
index 00000000..76deadaa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_framework.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_get_app.xml b/app/src/main/res/drawable/ic_get_app.xml
new file mode 100644
index 00000000..09cd9664
--- /dev/null
+++ b/app/src/main/res/drawable/ic_get_app.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml
new file mode 100644
index 00000000..47f21458
--- /dev/null
+++ b/app/src/main/res/drawable/ic_github.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml
new file mode 100644
index 00000000..a3f80358
--- /dev/null
+++ b/app/src/main/res/drawable/ic_help.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml
new file mode 100644
index 00000000..3daf095b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_history.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml
new file mode 100644
index 00000000..1e1faf7e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_info.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_language.xml b/app/src/main/res/drawable/ic_language.xml
new file mode 100644
index 00000000..f9682223
--- /dev/null
+++ b/app/src/main/res/drawable/ic_language.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..0d025f9b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_manager.xml b/app/src/main/res/drawable/ic_manager.xml
new file mode 100644
index 00000000..92356905
--- /dev/null
+++ b/app/src/main/res/drawable/ic_manager.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_modules.xml b/app/src/main/res/drawable/ic_modules.xml
new file mode 100644
index 00000000..5b9d32ac
--- /dev/null
+++ b/app/src/main/res/drawable/ic_modules.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 00000000..bcd9d769
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml
new file mode 100644
index 00000000..d41fc012
--- /dev/null
+++ b/app/src/main/res/drawable/ic_person.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_phone.xml b/app/src/main/res/drawable/ic_phone.xml
new file mode 100644
index 00000000..9ab32d03
--- /dev/null
+++ b/app/src/main/res/drawable/ic_phone.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml
new file mode 100644
index 00000000..0d768f68
--- /dev/null
+++ b/app/src/main/res/drawable/ic_refresh.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml
new file mode 100644
index 00000000..2cc51d6f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_save.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_send.xml b/app/src/main/res/drawable/ic_send.xml
new file mode 100644
index 00000000..0e8967ef
--- /dev/null
+++ b/app/src/main/res/drawable/ic_send.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 00000000..61d75531
--- /dev/null
+++ b/app/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml
new file mode 100644
index 00000000..59b0e84d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml
new file mode 100644
index 00000000..707e960d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sort.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml
new file mode 100644
index 00000000..3bc2ab5e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_update.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_verified.xml b/app/src/main/res/drawable/ic_verified.xml
new file mode 100644
index 00000000..349944e4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_verified.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml
new file mode 100644
index 00000000..2eccab98
--- /dev/null
+++ b/app/src/main/res/drawable/ic_warning.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/outline_list_24.xml b/app/src/main/res/drawable/outline_list_24.xml
new file mode 100644
index 00000000..a862eb68
--- /dev/null
+++ b/app/src/main/res/drawable/outline_list_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/shortcut_ic_downloads.xml b/app/src/main/res/drawable/shortcut_ic_downloads.xml
new file mode 100644
index 00000000..9d5bf9de
--- /dev/null
+++ b/app/src/main/res/drawable/shortcut_ic_downloads.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/shortcut_ic_modules.xml b/app/src/main/res/drawable/shortcut_ic_modules.xml
new file mode 100644
index 00000000..fe33d631
--- /dev/null
+++ b/app/src/main/res/drawable/shortcut_ic_modules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml
new file mode 100644
index 00000000..19c69463
--- /dev/null
+++ b/app/src/main/res/layout/activity_about.xml
@@ -0,0 +1,796 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_black_list.xml b/app/src/main/res/layout/activity_black_list.xml
new file mode 100644
index 00000000..4fedbb6a
--- /dev/null
+++ b/app/src/main/res/layout/activity_black_list.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_download.xml b/app/src/main/res/layout/activity_download.xml
new file mode 100644
index 00000000..92d0831c
--- /dev/null
+++ b/app/src/main/res/layout/activity_download.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_download_details.xml b/app/src/main/res/layout/activity_download_details.xml
new file mode 100644
index 00000000..ded5bcce
--- /dev/null
+++ b/app/src/main/res/layout/activity_download_details.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_download_details_not_found.xml b/app/src/main/res/layout/activity_download_details_not_found.xml
new file mode 100644
index 00000000..a317c5d6
--- /dev/null
+++ b/app/src/main/res/layout/activity_download_details_not_found.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_ed_download.xml b/app/src/main/res/layout/activity_ed_download.xml
new file mode 100644
index 00000000..01ceee21
--- /dev/null
+++ b/app/src/main/res/layout/activity_ed_download.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_logs.xml b/app/src/main/res/layout/activity_logs.xml
new file mode 100644
index 00000000..d9bb4da4
--- /dev/null
+++ b/app/src/main/res/layout/activity_logs.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..6b099783
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_modules.xml b/app/src/main/res/layout/activity_modules.xml
new file mode 100644
index 00000000..4fedbb6a
--- /dev/null
+++ b/app/src/main/res/layout/activity_modules.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
new file mode 100644
index 00000000..4818969e
--- /dev/null
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/appbar_layout.xml b/app/src/main/res/layout/appbar_layout.xml
new file mode 100644
index 00000000..1d08ab8d
--- /dev/null
+++ b/app/src/main/res/layout/appbar_layout.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_install_warning.xml b/app/src/main/res/layout/dialog_install_warning.xml
new file mode 100644
index 00000000..748c81ba
--- /dev/null
+++ b/app/src/main/res/layout/dialog_install_warning.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/download_details.xml b/app/src/main/res/layout/download_details.xml
new file mode 100644
index 00000000..07e12c97
--- /dev/null
+++ b/app/src/main/res/layout/download_details.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/download_moreinfo.xml b/app/src/main/res/layout/download_moreinfo.xml
new file mode 100644
index 00000000..7742a3f6
--- /dev/null
+++ b/app/src/main/res/layout/download_moreinfo.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/download_view.xml b/app/src/main/res/layout/download_view.xml
new file mode 100644
index 00000000..b06c2025
--- /dev/null
+++ b/app/src/main/res/layout/download_view.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_compile_dialog.xml b/app/src/main/res/layout/fragment_compile_dialog.xml
new file mode 100644
index 00000000..086ee0f3
--- /dev/null
+++ b/app/src/main/res/layout/fragment_compile_dialog.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_app.xml b/app/src/main/res/layout/item_app.xml
new file mode 100644
index 00000000..d949bb95
--- /dev/null
+++ b/app/src/main/res/layout/item_app.xml
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_download.xml b/app/src/main/res/layout/item_download.xml
new file mode 100644
index 00000000..1ac6ce40
--- /dev/null
+++ b/app/src/main/res/layout/item_download.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_module.xml b/app/src/main/res/layout/item_module.xml
new file mode 100644
index 00000000..383e3f1a
--- /dev/null
+++ b/app/src/main/res/layout/item_module.xml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_version.xml b/app/src/main/res/layout/item_version.xml
new file mode 100644
index 00000000..e79ec0bc
--- /dev/null
+++ b/app/src/main/res/layout/item_version.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/single_installer_view.xml b/app/src/main/res/layout/single_installer_view.xml
new file mode 100644
index 00000000..30ff80aa
--- /dev/null
+++ b/app/src/main/res/layout/single_installer_view.xml
@@ -0,0 +1,341 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/status_installer.xml b/app/src/main/res/layout/status_installer.xml
new file mode 100644
index 00000000..7fd5bd0a
--- /dev/null
+++ b/app/src/main/res/layout/status_installer.xml
@@ -0,0 +1,355 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/sticky_header_download.xml b/app/src/main/res/layout/sticky_header_download.xml
new file mode 100644
index 00000000..92ddd79c
--- /dev/null
+++ b/app/src/main/res/layout/sticky_header_download.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/context_menu_modules.xml b/app/src/main/res/menu/context_menu_modules.xml
new file mode 100644
index 00000000..35c87796
--- /dev/null
+++ b/app/src/main/res/menu/context_menu_modules.xml
@@ -0,0 +1,23 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_app_item.xml b/app/src/main/res/menu/menu_app_item.xml
new file mode 100644
index 00000000..78940735
--- /dev/null
+++ b/app/src/main/res/menu/menu_app_item.xml
@@ -0,0 +1,27 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_app_list.xml b/app/src/main/res/menu/menu_app_list.xml
new file mode 100644
index 00000000..b412991f
--- /dev/null
+++ b/app/src/main/res/menu/menu_app_list.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_download.xml b/app/src/main/res/menu/menu_download.xml
new file mode 100644
index 00000000..8a4432ae
--- /dev/null
+++ b/app/src/main/res/menu/menu_download.xml
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_download_details.xml b/app/src/main/res/menu/menu_download_details.xml
new file mode 100644
index 00000000..18395914
--- /dev/null
+++ b/app/src/main/res/menu/menu_download_details.xml
@@ -0,0 +1,22 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_installer.xml b/app/src/main/res/menu/menu_installer.xml
new file mode 100644
index 00000000..322fa8c6
--- /dev/null
+++ b/app/src/main/res/menu/menu_installer.xml
@@ -0,0 +1,49 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_logs.xml b/app/src/main/res/menu/menu_logs.xml
new file mode 100644
index 00000000..54f6b901
--- /dev/null
+++ b/app/src/main/res/menu/menu_logs.xml
@@ -0,0 +1,56 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 00000000..47776106
--- /dev/null
+++ b/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/menu/menu_modules.xml b/app/src/main/res/menu/menu_modules.xml
new file mode 100644
index 00000000..43f55d92
--- /dev/null
+++ b/app/src/main/res/menu/menu_modules.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..898f3ed5
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..dffca360
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..64ba76f7
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..dae5e082
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..e5ed4659
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..14ed0af3
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..b0907cac
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..d8ae0315
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..2c18de9e
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..beed3cdd
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values-night/bool.xml b/app/src/main/res/values-night/bool.xml
new file mode 100644
index 00000000..63ba97b3
--- /dev/null
+++ b/app/src/main/res/values-night/bool.xml
@@ -0,0 +1,6 @@
+
+
+
+ false
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
new file mode 100644
index 00000000..ba9bfc51
--- /dev/null
+++ b/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,12 @@
+
+
+ #2196f3
+ #303030
+ #448aff
+ #F44336
+ @color/red_500
+ #4CAF50
+ #FFC107
+ #303030
+ #ea000000
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v23/colors.xml b/app/src/main/res/values-v23/colors.xml
new file mode 100644
index 00000000..a6b3daec
--- /dev/null
+++ b/app/src/main/res/values-v23/colors.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v27/colors.xml b/app/src/main/res/values-v27/colors.xml
new file mode 100644
index 00000000..cbf7f5d3
--- /dev/null
+++ b/app/src/main/res/values-v27/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #fff
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 00000000..b90d137d
--- /dev/null
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,316 @@
+
+
+ 设置
+ 已激活
+ 未激活
+ 安装
+ 点按安装 EdXposed
+ EdXposed 框架已激活
+ 模块
+ %d 模块已启用
+ 下载
+ %s 模块可更新
+ 所有模块已是最新版本
+ 应用
+ 日志
+ 设置
+ 关于
+
+ 发送
+ 保存到 SD 卡
+
+
+ 我的设备
+ EdXposed 框架有更新可用(%s)
+
+
+ 框架
+ 模块
+ 下载
+ 详细日志
+ 模块日志
+
+
+ 安装/更新
+ 卸载
+ 软重启
+ 重启
+ 您确定?
+ 重启失败, 请使用设备自带的重启功能
+ 请小心!
+ 某些情况下, 您的设备可能会在安装 EdXposed 过后变得无法正常启动\n\n如果您先前从未听说过「软变砖」或「无限重启」, 又或者您不知道如何从这些情况中恢复手机, 那么请"不要"安装或使用 EdXposed!\n\n无论如何, 都强烈建议您做好近期的数据备份
+ 不再提示
+ EdXposed 框架已安装, 但尚未激活\n请查看日志以了解详情
+ 有模块更新可用
+
+
+ 无法获取 root 权限\n\n请确认您的设备已正确 root 且没有阻止 shell 命令
+
+
+ EdXposed 模块尚未激活
+ %s 已安装, 但尚未激活
+ 激活并重启
+ 仅激活
+ EdXposed 模块已激活
+ (未提供介绍)
+ 该模块未提供用户界面
+ EdXposed 模块列表已更新\n变更将在重启后生效
+ EdXposed 模块已更新
+ 重启以使变更生效
+ 该模块需要更新版本的 EdXposed(%d), 因此无法被激活
+ 该模块未指定所需的 EdXposed 版本
+ 该模块针对 EdXposed %1$d 版本构建 , 由于不兼容 %2$d 版本中的变更, 现已被停用
+ 打开
+ 下载/更新
+ 支持
+ 应用信息
+ 在应用商店查看
+ 卸载
+
+
+ 搜索
+ 重新加载
+ "更新日志: "
+ 介绍
+ 版本
+ 设置
+ 稳定版
+ 稳定版(低故障率)
+ 测试版
+ 测试版(预计存在问题)
+ 实验版
+ 实验版(高故障率)
+ 使用全局设置
+ 作者: %s
+ 未知作者
+ 已安装(版本 %s)
+ 可更新(版本 %1$s \u2192 %2$s)
+ 添加于 %1$s \u00b7 更新于 %2$s
+ 排序方式
+ 按状态排序
+ 按最后更新排序
+ 按添加日期排序
+ EdXposed 框架
+ 有更新可用
+ 已安装
+ 未安装
+ 最近 24 小时
+ 最近 7 天
+ 最近 30 天
+ 超过 30 天前
+ 在仓库中找不到包名为 \"%s\" 的模块
+ 作者还没有上传过任何内容
+ 有新的测试版可用, 但根据全局或模块的设置未被显示
+
+
+ 下载和安装
+ 要显示的版本
+ 框架
+ 禁用资源钩子
+ 解决与主题引擎的冲突\n警告: 启用该选项将导致修改资源的模块无法工作
+ 应用
+ 主题
+ 亮色
+ 暗色
+ 默认视图
+
+
+ 模块支持
+ 在模块列表中点击任意模块并选择 \"%1$s\"
+ 框架/管理器 支持
+ 常见/已知问题
+ QQ 群组: 855219808
+ Telegram 群组: @Code_of_MeowCat
+ 捐赠
+ 如果您想对 EdXposed 表达感谢, 可以在此向我们捐赠
+
+
+ 无法将日志写入 SD 卡:
+ 无法找到 SD 卡或不可写入
+ 立即清理日志
+ 日志清理成功
+ 无法清理日志:
+ 日志为空
+
+
+ 主要开发者
+ 简体中文(中国)翻译
+ MlgmXyysd
+ 版本
+ 源代码
+ 使用的库
+
+
+ 下载并安装
+ 安装
+ 删除
+ 取消
+ 无可用的下载链接
+ 正在下载(%1$,d / %2$,d kB)
+ 等待下载中
+ 下载成功
+ 下载失败(错误 %d)
+
+
+ MD5 校验码错误(下载: %1$s, 预期: %2$s)
+ 无法读取下载的文件: %s
+ 下载的文件并非有效的 APK (或存在兼容性问题)
+ 文件包名不正确(下载: %1$s, 预期: %2$s)
+ 正在下载
+
+
+ 下载 %1$s 失败: %2$d(%3$s)
+ 下载 %1$s 失败: %2$s
+ 无法从 %1$s:\n%2$s 中加载仓库
+
+ 此模块因被安装在 SD 卡中而导致无法加载, 请将其移动到内部存储
+ 导出…
+ 导入…
+ 导出已启用模块列表
+ 导入已启用模块列表
+ 导出已安装模块列表
+ 导入已安装模块列表
+ 没有任何已安装的模块
+ 启用浮动通知
+ 对新安装或已更新的模块启用浮动通知
+ 找不到备份
+ 没有任何已启用的模块
+
+ 分享
+ 信息
+
+ 名称: %1$s
+ \n版本: %2$s
+
+
+ 名称: %1$s
+ \n日期: %2$s
+
+ 当未允许写入外部存储空间权限时, 该功能无法正常工作
+ 你将会下载一个 ZIP 格式文件, 请手动在 Magisk Manager 或 Recovery 中刷入
+ EdXposed Manager 有新版本可用!现在更新!
+ 更新
+ EdXposed 框架未安装
+ 启用模块更新通知
+
+ 更新日志
+ 滚动到顶部
+ 已添加书签
+ 已移除书签
+ 通知图标着色
+ 文件为空
+ 静默安装(root)
+ 勾选后在 EdXposed Manager 内安装模块时将不再显示安装提示
+ 模块已保存至 %1$s
+ 忽略更新
+ 忽略指定模块的更新
+ 重启至恢复模式
+ 重启至引导加载器
+ 重启至下载模式
+ 重启至救援模式
+ 正在加载…
+ Chrome 自定义标签页
+ 在内置浏览器中打开链接
+ 滚动到底部
+ 重启确认
+ 关闭状态检查
+ 关闭针对模块的\'EdXposed 安装状态\' 检查
+ 获取帮助
+ 稍后
+ 仅下载
+ 下载位置
+ 选择 APK 文件的下载位置(仅限模块)
+ 此处仅显示 EdXposed 及模块相关日志信息\n如果您想抓取系统日志, 可以尝试我们的 Log Catcher Magisk 模块
+
+
+ 应用
+ 白名单模式
+ 兼容模式
+ 白名单
+ 黑名单
+ 修改失败, 一个或多个设置阻止你继续修改
+ 取消优化
+ 以 Speed 模式优化应用
+ 以 Dexopt 模式优化应用
+ 清除中…
+ 优化中…
+ 即时模块列表
+ 启用后, 每个应用进程启动时重新加载最新的模块列表(免重启)
+ EdXposed 框架未安装
+ MlgmXyysd 所属团队
+ EdXposed 的开发团队
+ EdXposed 的主要开发者
+ EdXposed Manager 的主要开发者
+ 官网
+ 酷安
+ 小喵工作室
+ 白名单模式: 仅选中的应用加载 EdXposed 模块\n仅当启用应用名单模式时有效
+ 应用名单模式
+ 开启后,可在\"应用\"中设置名单\n黑名单模式: 选中的应用不会加载 EdXposed 模块\n注意: 使用应用名单可能会导致某些系统全局模块不能正常工作\n
+ 从模块列表中隐藏中文模块\n模块仍然可以被搜索到\n启用此选项可能会降低性能
+ 忽略中文字符
+ 引导验证(dm-verity)防止设备在修改系统分区时引导
+ 引导验证已启用
+ 引导验证已禁用
+ 无法获取引导验证状态
+ 完成!
+ 安装错误
+ 安装成功
+ 成功安装 %s
+ %1$s 安装失败 (%2$s)
+ 安装中…
+ 使用普通方式安装 APK
+ 正在安装 %s
+ 这是一个不稳定的构建\n安装风险由您自行承担
+ 这是一个非官方的构建\n安装风险由您自行承担
+ 备用下载方法
+ 如果下载模块时出现问题, 请开启此选项
+ 错误的配置文件夹: %1$s
+ 未知配置文件夹
+ 以 Dexopt 模式优化全部应用
+ 以 Speed 模式优化全部应用
+ 请稍后, 这将需要一段时间…
+ 你确定要优化全部应用吗?\n\n这将需要一段时间, 并且无法恢复\n\n提示: 你可以在应用名单中优化单个应用
+ 禁用系统代码优化
+ 禁用系统代码的优化能使所有系统方法都可以被 hook 到, 但是会降低系统流畅度\n主要用于对比测试某个方法无法 hook 到是否为系统优化导致
+
+ 禁用详细日志
+ 详细日志已禁用
+ 日志过长, 未完全显示
+ 无法读取日志: \n
+ 运行
+ 停止
+ 更新日志
+ 你必须先卸载 EdXposed Installer 才能继续使用 EdXposed Manager
+ 安装日期:
+ 更新日期:
+ 列表排序方式
+ 应用名称
+ 应用名称(降序)
+ 包体名称
+ 包体名称(降序)
+ 安装时间
+ 安装时间(降序)
+ 更新时间
+ 更新时间(降序)
+ 显示模块和管理器
+ 在应用列表内显示模块和管理器\n此选项并不会移除已勾选的应用标识
+ 优化失败或返回值为空
+ 强制添加模块
+ 强制将模块添加进白名单并移出黑名单\n关闭此选项并不会移除已勾选的应用标识\n仅当启用应用名单模式时有效
+ 使用镜像化的模块列表
+ 镜像化的模块列表可以帮助加速加载\n但新提交的模块显示会有延迟(大约24小时)
+ 未安装
+ 通过 SafetyNet 校验
+ 强制将 GMS 和 GSF 加入黑名单并移出白名单\n关闭此选项并不会移除已勾选的应用标识\n仅当启用应用名单模式时有效
+ Telegram 频道: @EdXposed
+ 应用名单模式未启用,您可以在设置中启用
+ 参数为空
+ 重启至系统
+ 在下次启动之前,模块日志将不再被记录
+ 在下次启动之前,详细日志将不再被记录
+ 禁用模块日志
+ 模块抛出的的异常堆栈仍将正常抓取
+ 跟随系统
+ 使用纯黑深色主题
+
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 00000000..4437f00d
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,90 @@
+
+
+
+
+ - @string/download_sorting_status
+ - @string/download_sorting_updated
+ - @string/download_sorting_created
+
+
+
+ - @string/follow_system
+ - @string/settings_theme_light
+ - @string/settings_theme_dark
+
+
+
+ - @string/nav_item_install
+ - @string/nav_item_modules
+ - @string/nav_item_download
+ - @string/nav_title_black_list
+ - @string/nav_title_compat_list
+ - @string/nav_item_logs
+ - @string/nav_item_logs_err
+
+
+
+ - -1
+ - 1
+ - 2
+
+
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+
+
+
+ - @string/reltype_stable_summary
+ - @string/reltype_beta_summary
+ - @string/reltype_experimental_summary
+
+
+
+ - stable
+ - beta
+ - experimental
+
+
+
+ - @string/reltype_use_global_summary
+ - @string/reltype_stable_summary
+ - @string/reltype_beta_summary
+ - @string/reltype_experimental_summary
+
+
+
+
+ - stable
+ - beta
+ - experimental
+
+
+
+ - @string/sort_by_name
+ - @string/sort_by_name_reverse
+ - @string/sort_by_package_name
+ - @string/sort_by_package_name_reverse
+ - @string/sort_by_install_time
+ - @string/sort_by_install_time_reverse
+ - @string/sort_by_update_time
+ - @string/sort_by_update_time_reverse
+
+
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+
+
+
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..d191aeb7
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/bool.xml b/app/src/main/res/values/bool.xml
new file mode 100644
index 00000000..8b3c6a3e
--- /dev/null
+++ b/app/src/main/res/values/bool.xml
@@ -0,0 +1,6 @@
+
+
+
+ true
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..bb89acd4
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,12 @@
+
+
+ #2196f3
+ #fff
+ #448aff
+ #F44336
+ @color/red_500
+ #4CAF50
+ #FFC107
+ @color/colorPrimary
+ #eaffffff
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..7abc06d3
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1 @@
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..3014384a
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,350 @@
+
+ EdXposed Manager
+ Settings
+ Activated
+ Inactivate
+ Install
+ Tap to install EdXposed
+ EdXposed is activated
+ Modules
+ %d modules enabled
+ Downloads
+ %s modules can be upgraded
+ All modules are up-to-date
+ Apps
+ Logs
+ Settings
+ About
+
+
+ Send
+ Save to SD card
+
+
+ Device
+ An update for the EdXposed Framework is available (%s)
+
+
+ Framework
+ Modules
+ Download
+ Verbose Logs
+ Modules Logs
+
+
+ Install/Update
+ Uninstall
+ Soft reboot
+ Reboot
+ Are you sure?
+ Reboot failed. Please use the device\'s normal reboot function
+ Be careful!
+ In some cases, your device might no longer boot after installing EdXposed.\n\nIf you never heard about \"soft brick\" and \"bootloop\" before or if you don\'t know how to recover from such a situation, do NOT install or use EdXposed!\n\nIn any case, having a recent backup is highly recommended.
+ Don\'t show this again
+ EdXposed Framework is installed, but not active\nPlease check the logs for details
+ Module update(s) are available
+
+
+ Failed to get root access\n\nMake sure your device is rooted properly and you have not blocked shell commands
+
+
+ EdXposed module is not activated yet
+ %s has been installed, but is not activated yet
+ Activate and reboot
+ Activate only
+ EdXposed module has been activated
+ (no description provided)
+ This module does not provide a user interface
+ EdXposed module list was updated\nChanges will become active on reboot
+ EdXposed module updated
+ Reboot to activate changes
+ This module requires a newer EdXposed version (%d) and thus cannot be activated
+ This module does not specify the EdXposed version it needs.
+ This module was created for EdXposed version %1$d, but due to incompatible changes in version %2$d, it has been disabled
+ Launch UI
+ Download/Updates
+ Support
+ View in App Store
+ App info
+ Uninstall
+
+
+ Search
+ Reload
+ Changelog:
+ Description
+ Versions
+ Settings
+ Stable
+ Stable (low risk of bugs)
+ Beta
+ Beta (some bugs to be expected)
+ Experimental
+ Experimental (high risk of bugs)
+ Use global setting
+ by %s
+ unknown author
+ Installed (version %s)
+ Update available (version %1$s \u2192 %2$s)
+ Added on %1$s \u00b7 Updated on %2$s
+ Sorting order
+ Sort by status
+ Sort by last update
+ Sort by creation date
+ EdXposed Framework
+ Update available
+ Installed
+ Not installed
+ Last 24 hours
+ Last 7 days
+ Last 30 days
+ More than 30 days ago
+ There is no module with package name \"%s\" in the repository
+ The author hasn\'t uploaded anything yet
+ A test version is available, however it isn\'t shown due to your global or module-specific settings
+
+
+ Download and install
+ Versions to be shown
+ Framework
+ Disabled resource hooks
+ Workaround for conflicts with theming engines\nWARNING: Modules which try to change resources will not work when this is enabled
+ Application
+ Theme
+ Light
+ Dark
+ Default view
+
+
+ Support for modules
+ Click any module in the modules list and select \"%1$s\".
+ Framework/Manager support
+ FAQ/known issues
+ QQ Group: 855219808
+ Telegram Group: @Code_of_MeowCat
+ Donate
+ If you would like to say thanks for EdXposed, you can donate to our here
+
+
+ Could not write log to SD card:
+ SD card not found or not writable
+ Clear log now
+ Log successfully cleared.
+ Could not clear the log:
+ Log is empty.
+
+
+ Main developers
+ Translator
+ MlgmXyysd
+ Version
+ Source code
+ Used libraries
+
+
+ Download and Install
+ Install
+ Remove
+ Cancel
+ No download URL available
+ Download is running (%1$,d of %2$,d kB)
+ Download is waiting
+ Download successful
+ Download failed (error %d)
+
+
+ MD5 sum is incorrect (downloaded: %1$s, expected: %2$s)
+ Could not read downloaded file: %s
+ Downloaded file is not a valid APK (or incompatible)
+ Package name is incorrect (downloaded: %1$s, expected: %2$s)
+ Download is running
+ %1$,d / %2$,d kB
+
+
+ Downloading %1$s failed: %2$d (%3$s)
+ Downloading %1$s failed: %2$s
+ Cannot load repository from %1$s:\n%2$s
+
+ This module cannot be loaded because it\'s installed on the SD card, please move it to internal storage
+ Export…
+ Import…
+ Export enabled modules list
+ Import enabled modules list
+ Export installed modules list
+ Import installed modules list
+ There are no installed modules!
+ Enable Heads-Up notification
+ This option enables heads up notification on new/updated module
+ No backup found
+ There are no modules enabled
+
+ Share with…
+ Info
+
+ Name: %1$s
+ \nVersion: %2$s
+
+
+ Name: %1$s
+ \nDate: %2$s
+
+ This feature will not work without permission to write external storage
+ You will download a ZIP file, flash in Magisk Manager or Recovery manually
+ New version of EdXposed Manager is available! Update it!
+ Update
+ EdXposed Framework is not installed
+ Enable module update notification
+
+ Changelog
+ Scroll to top
+ Bookmark added
+ Bookmark removed
+ Color notification icon
+ File is empty
+ Silent installation (root)
+ If checked EdXposed Manager will install modules without a prompt
+ Module saved at %1$s
+ Ignore update
+ Ignore updates for a specific module
+ Reboot to Recovery
+ Reboot to Bootloader
+ Reboot to Download
+ Reboot to Emergency
+ Loading…
+ Chrome Custom Tabs
+ Open links in internal browser
+ Scroll to bottom
+ Confirm reboots
+ Skip status check
+ Skip \"EdXposed Status\" check for modules
+
+ Get support
+ Android %2$s (%1$s, API %3$d)
+ Later
+ Save only
+ Download location
+ Download location for APK files (modules only)
+ This is the EdXposed Framework and modules log\nif you need a Android logcat, you can try our Log Catcher Magisk module
+ Installation error
+ Installation successful
+ %s installed successfully
+ %1$s installation failed (%2$s)
+ Install APK normally
+ Installing…
+ Installing %s
+
+ This is a unstable build\nInstall it at your own risk
+ This is a unofficial build\nInstall it at your own risk
+ Alternative download method
+ Check this option if you get issues when downloading a module
+ Use mirrored modules list
+ Mirrored modules list can help load faster\nBut the newly submitted module display will be delayed (about 24 hours)
+
+ Wrong base directory: %1$s
+ Missing base directory
+
+ Optimize all apps with Dexopt
+ Optimize all apps with Speed mode
+ Please wait, this may take a while…
+ Are you sure to optimize all apps?\n\nThis may take a while, and CANNOT be restored\n\nTip: You can optimize selected app in Application List
+ Done!
+ Verified Boot is deactivated
+ Could not detect Verified Boot state
+ Verified Boot is active
+ Verified Boot (dm-verity) prevents the device from booting if the system partition is modified
+ Ignore Chinese characters
+ Hide Chinese modules from list\nModules are still available in search window\nEnabling this option may result in slow performance
+
+
+ Applications
+ Compat List
+ White List mode
+ White List
+ Black List
+ Failed to edit, one or more settings prevent you from editing.
+ De-optimize
+ Optimize with Speed mode
+ Optimize with Dexopt mode
+ Resetting …
+ Optimizing …
+ Instant module list
+ When turned on, load latest activated module list on each app process started
+ App List mode
+ After opening, you can set the list in \"Application\"\nBlack List mode: Selected app NOT to load EdXposed modules\nNote: using app list mode might break system-wide modules\' functions
+ White List mode: Only selected app load EdXposed modules\nApp list mode is required to enable
+
+ http://edxp.meowcat.org/
+ http://github.com/ElderDrivers/EdXposed/wiki
+ http://edxp.meowcat.org/donate
+ https://github.com/ElderDrivers/EdXposedManager/
+ http://edxp.meowcat.org/faq
+
+ givein2u
+ MlgmXyysd
+ MeowCat Studio
+ Elder Drivers
+
+ GitHub
+ XDA
+ CoolApk
+ Website
+
+ Main creator of EdXposed
+ Main creator of EdXposed Manager
+ Organization of EdXposed
+ Organization of MlgmXyysd
+ https://forum.xda-developers.com/member.php?u=5571846
+ https://github.com/solohsu
+ http://www.coolapk.com/u/537470
+ http://www.coolapk.com/u/814297
+ https://forum.xda-developers.com/member.php?u=8430637
+ https://github.com/MlgmXyysd
+ http://www.meowcat.org/
+ https://github.com/MeowCat-Studio
+ https://github.com/ElderDrivers/
+ http://edxp.meowcat.org/
+ https://jq.qq.com/?k=57IiX8R
+ http://t.me/Code_of_MeowCat
+ http://t.me/EdXposed
+ The EdXposed framework is not installed.
+ De-optimize boot image
+ Turn on to make all framework methods hookable, but the whole system will be slowed down\nMainly used to check if one framework method is inlined when it can\'t be hooked into
+
+ Disable verbose logs
+ Verbose logs is disabled
+ Log is too long, not fully displayed
+ Cannot read log: \n
+ Launch it
+ Stop it
+ Update Changelog
+ You must to uninstall EdXposed Installer to use EdXposed Manager
+ Install time:
+ Update time:
+ List sorting
+ Sort by application name
+ Sort by application name (reverse)
+ Sort by package name
+ Sort by package name (reverse)
+ Sort by install time
+ Sort by install time (reverse)
+ Sort by update time
+ Sort by update time (reverse)
+ Show modules and manager
+ Show modules and manager in application list\nThis option does not remove checked application flag
+ Optimization failed or return value is empty
+ Force hook modules
+ Force add modules to white-list and remove from black list\nClosing this option does not remove modules from white list\nApp list mode is required to enable
+ Not installed
+ Pass SafetyNet
+ Force add GMS and GSF to black list and remove from white list\nClosing this option does not remove modules from black list\nApp list mode is required to enable
+ Telegram Channel: @EdXposed
+ Application list mode is not enabled, you can enable in settings
+ Params empty
+ Reboot to System
+ Verbose log will stop catch before the next boot
+ Modules log will stop catch before the next boot
+ Disable modules logs
+ Exception stack thrown by the module will still be catched normally
+ Use the pure black dark theme
+ Follow system
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..eba7b958
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 00000000..a132791b
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/module_prefs.xml b/app/src/main/res/xml/module_prefs.xml
new file mode 100644
index 00000000..6e0e3a65
--- /dev/null
+++ b/app/src/main/res/xml/module_prefs.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/prefs.xml b/app/src/main/res/xml/prefs.xml
new file mode 100644
index 00000000..9a86c1e5
--- /dev/null
+++ b/app/src/main/res/xml/prefs.xml
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
new file mode 100644
index 00000000..9d1cfcb1
--- /dev/null
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..34cc5f88
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.3'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..199d16ed
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,20 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..f6b961fd
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..11e0d97a
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Feb 03 10:33:02 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 00000000..cccdd3d5
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..f9553162
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..5acc29b0
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name='EdXPManager'