first commit

This commit is contained in:
NekoInverter 2020-02-03 18:57:01 +08:00
commit 5dddd6290b
164 changed files with 14812 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,116 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>

16
.idea/gradle.xml Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<compositeConfiguration>
<compositeBuild compositeDefinitionSource="SCRIPT" />
</compositeConfiguration>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="resolveModulePerSourceSet" value="false" />
<option name="testRunner" value="PLATFORM" />
</GradleProjectSettings>
</option>
</component>
</project>

53
.idea/misc.xml Normal file
View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CMakeSettings">
<configurations>
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations>
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
<option name="myDefaultNotNull" value="androidx.annotation.RecentlyNonNull" />
<option name="myNullables">
<value>
<list size="12">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="7" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="11" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="11">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

41
app/build.gradle Normal file
View File

@ -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'
}

Binary file not shown.

21
app/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.meowcat.edxposed.manager">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<application
android:name=".XposedApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name=".AboutActivity"
android:label="@string/About"
android:parentActivityName=".MainActivity" />
<activity
android:name=".LogsActivity"
android:label="@string/Logs"
android:parentActivityName=".MainActivity" />
<activity
android:name=".EdDownloadActivity"
android:label="@string/Install"
android:parentActivityName=".MainActivity" />
<activity android:name=".BlackListActivity" />
<activity android:name=".DownloadDetailsActivity" />
<activity
android:name=".DownloadActivity"
android:label="@string/Downloads"
android:parentActivityName=".MainActivity" />
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".ModulesActivity"
android:label="@string/Modules"
android:parentActivityName=".MainActivity" />
<activity
android:name=".SettingsActivity"
android:label="@string/Settings"
android:parentActivityName=".MainActivity" />
<receiver
android:name=".receivers.PackageChangeReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_CHANGED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<receiver
android:name=".receivers.DownloadReceiver"
android:exported="true"
android:permission="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
<receiver
android:name=".util.NotificationUtil$RebootReceiver"
android:exported="false" />
<receiver
android:name=".util.NotificationUtil$ApkReceiver"
android:exported="false" />
<receiver
android:name=".receivers.BootReceiver"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedReceiver" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="org.meowcat.edxposed.manager.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@ -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;
}
}
}

View File

@ -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<String> mRequires = new HashSet<>();
private boolean isComplete() {
return mVersion != null
&& mVersionInt > 0;
}
public String getVersion() {
return mVersion;
}
// public int getVersionInt() {
// return mVersionInt;
// }
}
}

View File

@ -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());
}
}

View File

@ -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<String> 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<String> 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);
}
}

View File

@ -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<String> 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<XposedZip> installers() {
XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab");
return Objects.requireNonNull(tab).installers;
}
private List<XposedZip> 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();
}
}

View File

@ -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);
}
}
}

View File

@ -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<String, Void, String> {
WeakReference<CompileDialogFragment> 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();
}
}
}

View File

@ -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<DownloadsAdapter.ViewHolder> 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);
}
}
}
}

View File

@ -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<String> 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];
}
}
}

View File

@ -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<String, String> 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;
}
}

View File

@ -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<String, ?> 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;
});
}
}

View File

@ -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<ModuleVersion> {
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;
}
}
}

View File

@ -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<Void, Void, Boolean> {
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<XposedTab> 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<String> titles = new ArrayList<>();
private final ArrayList<Fragment> 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);
}
}
}

View File

@ -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<File, Integer, String> {
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);
}
}
}

View File

@ -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);
}
}

View File

@ -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<ModuleUtil.InstalledModule> showList;
Collection<ModuleUtil.InstalledModule> 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<String, ModuleUtil.InstalledModule> 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<String> 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<Module> 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<ResolveInfo> 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<ModuleAdapter.ViewHolder> {
Collection<ModuleUtil.InstalledModule> 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<ModuleUtil.InstalledModule> 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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<PackageInfo> 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;
}
}

View File

@ -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) {
}
}

View File

@ -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<AppAdapter.ViewHolder> {
protected final Context context;
private final ApplicationInfo.DisplayNameComparator displayNameComparator;
public Callback callback;
private List<ApplicationInfo> fullList, showList;
private DateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
private List<String> checkedList;
private PackageManager pm;
private ApplicationFilter filter;
private Comparator<ApplicationInfo> 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<ApplicationInfo> 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<String> 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();
}
}
}

View File

@ -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<String> FORCE_WHITE_LIST = new ArrayList<>(Collections.singletonList(BuildConfig.APPLICATION_ID));
private static final List<String> SAFETYNET_BLACK_LIST = new ArrayList<>(Arrays.asList("com.google.android.gms", "com.google.android.gsf"));
static List<String> 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<String> getBlackList() {
File file = new File(BASE_PATH + BLACK_LIST_PATH);
File[] files = file.listFiles();
if (files == null) {
return new ArrayList<>();
}
List<String> 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<String> getWhiteList() {
File file = new File(BASE_PATH + WHITE_LIST_PATH);
File[] files = file.listFiles();
if (files == null) {
return FORCE_WHITE_LIST_MODULE;
}
List<String> 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<String> getCompatList() {
File file = new File(BASE_PATH + COMPAT_LIST_PATH);
File[] files = file.listFiles();
if (files == null) {
return new ArrayList<>();
}
List<String> 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);
}
}

View File

@ -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<String> checkedList;
public BlackListAdapter(Context context, boolean isWhiteListMode) {
super(context);
this.isWhiteListMode = isWhiteListMode;
}
// public void setWhiteListMode(boolean isWhiteListMode) {
// this.isWhiteListMode = isWhiteListMode;
// }
@Override
protected List<String> generateCheckedList() {
if (XposedApp.getPreferences().getBoolean("hook_modules", true)) {
Collection<ModuleUtil.InstalledModule> 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);
}
}
}

View File

@ -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<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
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 <em>not</em>
* 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
}
}
}

View File

@ -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<Void, Void, Void> {
@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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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<Pair<String, String>> moreInfo = new LinkedList<>();
public final List<ModuleVersion> versions = new ArrayList<>();
final List<String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<Long, Repository> getRepositories() {
Map<Long, Repository> 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<String, String> 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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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("<li>", "\t\u0095 ");
source = source.replaceAll("</li>", "<br>");
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<Bitmap>() {
@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);
}
}

View File

@ -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() {
}
}

View File

@ -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);
}
}

View File

@ -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<String, DownloadFinishedCallback> 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<File> 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<DownloadInfo> all;
try {
all = getAllForUrl(context, url);
} catch (Throwable throwable) {
return null;
}
return Objects.requireNonNull(all).isEmpty() ? null : all.get(0);
}
private static List<DownloadInfo> 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<DownloadInfo> 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<Long> 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<Long> 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<Long> 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<DownloadInfo> {
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;
}
}
}

View File

@ -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();
}
}

View File

@ -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<Void, Void, Integer> {
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<String> 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);
}
}
}

View File

@ -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());
}
}

View File

@ -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<ModuleListener> mListeners = new CopyOnWriteArrayList<>();
private SharedPreferences mPref;
private InstalledModule mFramework = null;
private Map<String, InstalledModule> 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<String, InstalledModule> 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<String, InstalledModule> 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<InstalledModule> getEnabledModules() {
LinkedList<InstalledModule> 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<InstalledModule> 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<ResolveInfo> 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();
}
}
}

View File

@ -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());
}
}

View File

@ -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<String> 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);
}
}
}

View File

@ -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<String, ?> getAll() {
Map<String, ?> baseResult = mBase.getAll();
Map<String, Object> prefixedResult = new HashMap<String, Object>(baseResult);
for (Entry<String, ?> 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<String> getStringSet(String key, Set<String> 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<String> 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();
}
}
}

View File

@ -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<RepoListener> mListeners = new CopyOnWriteArrayList<>();
private final Map<String, ReleaseType> 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<Long, Repository> 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<String> 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<Long, Repository>(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<String> 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<Long, Repository> 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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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) {
}
}

View File

@ -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<XposedTab> tabs;
public ApkRelease apk;
}
public class ApkRelease {
public String version;
public String changelog;
public String link;
}
}

View File

@ -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<XposedTab> CREATOR = new Creator<XposedTab>() {
@Override
public XposedTab createFromParcel(Parcel in) {
return new XposedTab(in);
}
@Override
public XposedTab[] newArray(int size) {
return new XposedTab[size];
}
};
public List<Integer> 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<XposedZip> installers = new ArrayList<>();
public List<XposedZip> uninstallers = new ArrayList<>();
// private HashMap<String, String> compatibility = new HashMap<>();
// private HashMap<String, String> 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<XposedZip> 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));
}
}

View File

@ -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<XposedZip> {
private final Context context;
List<XposedZip> list;
public MyAdapter(Context context, List<XposedZip> 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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#757575"
android:pathData="M15,5H14V4H15M10,5H9V4H10M15.53,2.16L16.84,0.85C17.03,0.66 17.03,0.34 16.84,0.14C16.64,-0.05 16.32,-0.05 16.13,0.14L14.65,1.62C13.85,1.23 12.95,1 12,1C11.04,1 10.14,1.23 9.34,1.63L7.85,0.14C7.66,-0.05 7.34,-0.05 7.15,0.14C6.95,0.34 6.95,0.66 7.15,0.85L8.46,2.16C6.97,3.26 6,5 6,7H18C18,5 17,3.25 15.53,2.16M20.5,8A1.5,1.5 0 0,0 19,9.5V16.5A1.5,1.5 0 0,0 20.5,18A1.5,1.5 0 0,0 22,16.5V9.5A1.5,1.5 0 0,0 20.5,8M3.5,8A1.5,1.5 0 0,0 2,9.5V16.5A1.5,1.5 0 0,0 3.5,18A1.5,1.5 0 0,0 5,16.5V9.5A1.5,1.5 0 0,0 3.5,8M6,18A1,1 0 0,0 7,19H8V22.5A1.5,1.5 0 0,0 9.5,24A1.5,1.5 0 0,0 11,22.5V19H13V22.5A1.5,1.5 0 0,0 14.5,24A1.5,1.5 0 0,0 16,22.5V19H17A1,1 0 0,0 18,18V8H6V18Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M7,15h7v2L7,17zM7,11h10v2L7,13zM7,7h10v2L7,9zM19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-0.14,0 -0.27,0.01 -0.4,0.04 -0.39,0.08 -0.74,0.28 -1.01,0.55 -0.18,0.18 -0.33,0.4 -0.43,0.64 -0.1,0.23 -0.16,0.49 -0.16,0.77v14c0,0.27 0.06,0.54 0.16,0.78s0.25,0.45 0.43,0.64c0.27,0.27 0.62,0.47 1.01,0.55 0.13,0.02 0.26,0.03 0.4,0.03h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,2.75c0.41,0 0.75,0.34 0.75,0.75s-0.34,0.75 -0.75,0.75 -0.75,-0.34 -0.75,-0.75 0.34,-0.75 0.75,-0.75zM19,19L5,19L5,5h14v14z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#757575"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM16.59,7.58L10,14.17l-2.59,-2.58L6,13l4,4 8,-8z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#757575"
android:pathData="M6,4H18V5H21V7H18V9H21V11H18V13H21V15H18V17H21V19H18V20H6V19H3V17H6V15H3V13H6V11H3V9H6V7H3V5H6V4M11,15V18H12V15H11M13,15V18H14V15H13M15,15V18H16V15H15Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#757575"
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#757575"
android:pathData="M11.8,10.9c-2.27,-0.59 -3,-1.2 -3,-2.15 0,-1.09 1.01,-1.85 2.7,-1.85 1.78,0 2.44,0.85 2.5,2.1h2.21c-0.07,-1.72 -1.12,-3.3 -3.21,-3.81V3h-3v2.16c-1.94,0.42 -3.5,1.68 -3.5,3.61 0,2.31 1.91,3.46 4.7,4.13 2.5,0.6 3,1.48 3,2.41 0,0.69 -0.49,1.79 -2.7,1.79 -2.06,0 -2.87,-0.92 -2.98,-2.1h-2.2c0.12,2.19 1.76,3.42 3.68,3.83V21h3v-2.15c1.95,-0.37 3.5,-1.5 3.5,-3.55 0,-2.84 -2.43,-3.81 -4.7,-4.4z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#757575"
android:pathData="M10.82,12.49c0.02,-0.16 0.04,-0.32 0.04,-0.49 0,-0.17 -0.02,-0.33 -0.04,-0.49l1.08,-0.82c0.1,-0.07 0.12,-0.21 0.06,-0.32l-1.03,-1.73c-0.06,-0.11 -0.2,-0.15 -0.31,-0.11l-1.28,0.5c-0.27,-0.2 -0.56,-0.36 -0.87,-0.49l-0.2,-1.33c0,-0.12 -0.11,-0.21 -0.24,-0.21H5.98c-0.13,0 -0.24,0.09 -0.26,0.21l-0.2,1.32c-0.31,0.12 -0.6,0.3 -0.87,0.49l-1.28,-0.5c-0.12,-0.05 -0.25,0 -0.31,0.11l-1.03,1.73c-0.06,0.12 -0.03,0.25 0.07,0.33l1.08,0.82c-0.02,0.16 -0.03,0.33 -0.03,0.49 0,0.17 0.02,0.33 0.04,0.49l-1.09,0.83c-0.1,0.07 -0.12,0.21 -0.06,0.32l1.03,1.73c0.06,0.11 0.2,0.15 0.31,0.11l1.28,-0.5c0.27,0.2 0.56,0.36 0.87,0.49l0.2,1.32c0.01,0.12 0.12,0.21 0.25,0.21h2.06c0.13,0 0.24,-0.09 0.25,-0.21l0.2,-1.32c0.31,-0.12 0.6,-0.3 0.87,-0.49l1.28,0.5c0.12,0.05 0.25,0 0.31,-0.11l1.03,-1.73c0.06,-0.11 0.04,-0.24 -0.06,-0.32l-1.1,-0.83zM7,13.75c-0.99,0 -1.8,-0.78 -1.8,-1.75s0.81,-1.75 1.8,-1.75 1.8,0.78 1.8,1.75S8,13.75 7,13.75zM18,1.01L8,1c-1.1,0 -2,0.9 -2,2v3h2V5h10v14H8v-1H6v3c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99z"
tools:ignore="VectorPath" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M13,5v6h1.17L12,13.17 9.83,11L11,11L11,5h2m2,-2L9,3v6L5,9l7,7 7,-7h-4L15,3zM19,18L5,18v2h14v-2z" />
</vector>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#757575"
android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#757575"
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#757575"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#757575"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z" />
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#757575"
android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" />
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#757575"
android:pathData="M20.5,11H19V7c0,-1.1 -0.9,-2 -2,-2h-4V3.5C13,2.12 11.88,1 10.5,1S8,2.12 8,3.5V5H4c-1.1,0 -1.99,0.9 -1.99,2v3.8H3.5c1.49,0 2.7,1.21 2.7,2.7s-1.21,2.7 -2.7,2.7H2V20c0,1.1 0.9,2 2,2h3.8v-1.5c0,-1.49 1.21,-2.7 2.7,-2.7 1.49,0 2.7,1.21 2.7,2.7V22H17c1.1,0 2,-0.9 2,-2v-4h1.5c1.38,0 2.5,-1.12 2.5,-2.5S21.88,11 20.5,11z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M20.5,11H19V7c0,-1.1 -0.9,-2 -2,-2h-4V3.5C13,2.12 11.88,1 10.5,1S8,2.12 8,3.5V5H4c-1.1,0 -1.99,0.9 -1.99,2v3.8H3.5c1.49,0 2.7,1.21 2.7,2.7s-1.21,2.7 -2.7,2.7H2V20c0,1.1 0.9,2 2,2h3.8v-1.5c0,-1.49 1.21,-2.7 2.7,-2.7 1.49,0 2.7,1.21 2.7,2.7V22H17c1.1,0 2,-0.9 2,-2v-4h1.5c1.38,0 2.5,-1.12 2.5,-2.5S21.88,11 20.5,11z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#757575"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#757575"
android:pathData="M16,1L8,1C6.34,1 5,2.34 5,4v16c0,1.66 1.34,3 3,3h8c1.66,0 3,-1.34 3,-3L19,4c0,-1.66 -1.34,-3 -3,-3zM14,21h-4v-1h4v1zM17.25,18L6.75,18L6.75,4h10.5v14z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4.01,6.03l7.51,3.22 -7.52,-1 0.01,-2.22m7.5,8.72L4,17.97v-2.22l7.51,-1M2.01,3L2,10l15,2 -15,2 0.01,7L23,12 2.01,3z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98 0,-0.34 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.09,-0.16 -0.26,-0.25 -0.44,-0.25 -0.06,0 -0.12,0.01 -0.17,0.03l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.06,-0.02 -0.12,-0.03 -0.18,-0.03 -0.17,0 -0.34,0.09 -0.43,0.25l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98 0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.09,0.16 0.26,0.25 0.44,0.25 0.06,0 0.12,-0.01 0.17,-0.03l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.06,0.02 0.12,0.03 0.18,0.03 0.17,0 0.34,-0.09 0.43,-0.25l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM17.45,11.27c0.04,0.31 0.05,0.52 0.05,0.73 0,0.21 -0.02,0.43 -0.05,0.73l-0.14,1.13 0.89,0.7 1.08,0.84 -0.7,1.21 -1.27,-0.51 -1.04,-0.42 -0.9,0.68c-0.43,0.32 -0.84,0.56 -1.25,0.73l-1.06,0.43 -0.16,1.13 -0.2,1.35h-1.4l-0.19,-1.35 -0.16,-1.13 -1.06,-0.43c-0.43,-0.18 -0.83,-0.41 -1.23,-0.71l-0.91,-0.7 -1.06,0.43 -1.27,0.51 -0.7,-1.21 1.08,-0.84 0.89,-0.7 -0.14,-1.13c-0.03,-0.31 -0.05,-0.54 -0.05,-0.74s0.02,-0.43 0.05,-0.73l0.14,-1.13 -0.89,-0.7 -1.08,-0.84 0.7,-1.21 1.27,0.51 1.04,0.42 0.9,-0.68c0.43,-0.32 0.84,-0.56 1.25,-0.73l1.06,-0.43 0.16,-1.13 0.2,-1.35h1.39l0.19,1.35 0.16,1.13 1.06,0.43c0.43,0.18 0.83,0.41 1.23,0.71l0.91,0.7 1.06,-0.43 1.27,-0.51 0.7,1.21 -1.07,0.85 -0.89,0.7 0.14,1.13zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#4CAF50"
android:pathData="M12,16.5l4,-4h-3v-9h-2v9L8,12.5l4,4zM21,3.5h-6v1.99h6v14.03L3,19.52L3,5.49h6L9,3.5L3,3.5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2v-14c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#757575"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM10,17l-4,-4 1.41,-1.41L10,14.17l6.59,-6.59L18,9l-8,8z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,13h2v-2L3,11v2zM3,17h2v-2L3,15v2zM3,9h2L5,7L3,7v2zM7,13h14v-2L7,11v2zM7,17h14v-2L7,15v2zM7,7v2h14L21,7L7,7zM3,13h2v-2L3,11v2zM3,17h2v-2L3,15v2zM3,9h2L5,7L3,7v2zM7,13h14v-2L7,11v2zM7,17h14v-2L7,15v2zM7,7v2h14L21,7L7,7z" />
</vector>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,796 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/appbar_layout" />
<ScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:ignore="UseCompoundDrawables,ContentDescription">
<LinearLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="72dp"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:src="@mipmap/ic_launcher" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_info" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/about_version_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:id="@+id/app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/changelogView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_history" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/changelog"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
tools:ignore="RtlHardcoded" />
</LinearLayout>
<LinearLayout
android:id="@+id/licensesView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_description" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/about_libraries_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
tools:ignore="RtlHardcoded" />
</LinearLayout>
<LinearLayout
android:id="@+id/translatorsView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_language" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/about_translator_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/translator"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/tgChannelView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_modules" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_group_telegram_channel"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/qqGroupView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_help" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_group_qq"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/tgGroupView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_help" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_group_telegram"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/sourceCodeView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_github" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/about_source_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/donateView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_donate" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_donate_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_donate_description"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/faqView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_bug" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_faq_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_info" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_modules_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:id="@+id/tab_support_module_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_modules_description"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/installerSupportView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_help" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_framework_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="16dp"
android:paddingTop="24dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:text="@string/about_developers_label"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="?android:textColorSecondary" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/windowBackground" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/mlgmxyysd"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/mlgmxyysd_summary"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<androidx.appcompat.widget.AppCompatButton
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openLink"
android:tag="@string/mlgmxyysd_xda"
android:text="@string/xda"
android:textColor="#F57C00" />
<androidx.appcompat.widget.AppCompatButton
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openLink"
android:tag="@string/mlgmxyysd_github"
android:text="@string/github"
android:textColor="#4183c4" />
<androidx.appcompat.widget.AppCompatButton
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openLink"
android:tag="@string/mlgmxyysd_coolapk"
android:text="@string/coolapk"
android:textColor="#0F9D58" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/windowBackground" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/solohsu"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/solohsu_summary"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<androidx.appcompat.widget.AppCompatButton
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openLink"
android:tag="@string/solohsu_xda"
android:text="@string/xda"
android:textColor="#F57C00" />
<androidx.appcompat.widget.AppCompatButton
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openLink"
android:tag="@string/solohsu_github"
android:text="@string/github"
android:textColor="#4183c4" />
<androidx.appcompat.widget.AppCompatButton
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openLink"
android:tag="@string/solohsu_coolapk"
android:text="@string/coolapk"
android:textColor="#0F9D58" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/windowBackground" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/meowcat"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/meowcat_summary"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<androidx.appcompat.widget.AppCompatButton
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openLink"
android:tag="@string/meowcat_website"
android:text="@string/website"
android:textColor="#F57C00" />
<androidx.appcompat.widget.AppCompatButton
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openLink"
android:tag="@string/meowcat_github"
android:text="@string/github"
android:textColor="#4183c4" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/windowBackground" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/elderdrivers"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/elderdrivers_summary"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<androidx.appcompat.widget.AppCompatButton
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openLink"
android:tag="@string/elderdrivers_website"
android:text="@string/website"
android:textColor="#F57C00" />
<androidx.appcompat.widget.AppCompatButton
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openLink"
android:tag="@string/elderdrivers_github"
android:text="@string/github"
android:textColor="#4183c4" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/appbar_layout" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_marginTop="?attr/actionBarSize"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/appbar_layout" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_marginTop="?attr/actionBarSize"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Some files were not shown because too many files have changed in this diff Show More