first commit
This commit is contained in:
commit
5dddd6290b
|
|
@ -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
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
|
|
@ -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.
|
|
@ -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
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
Loading…
Reference in New Issue