Merge remote-tracking branch 'app/master'
This commit is contained in:
commit
ffb83c0ba8
|
|
@ -18,7 +18,11 @@ jobs:
|
||||||
- name: Get version code
|
- name: Get version code
|
||||||
run: echo APPVEYOR_BUILD_NUMBER=$(expr $GITHUB_RUN_NUMBER + 4999) >> $GITHUB_ENV
|
run: echo APPVEYOR_BUILD_NUMBER=$(expr $GITHUB_RUN_NUMBER + 4999) >> $GITHUB_ENV
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: bash ./gradlew zipRelease zipDebug
|
env:
|
||||||
|
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
||||||
|
ALIAS_NAME: ${{ secrets.ALIAS_NAME }}
|
||||||
|
ALIAS_PASS: ${{ secrets.ALIAS_PASS }}
|
||||||
|
run: bash ./gradlew zipRelease zipDebug :app:assembleRelease
|
||||||
- name: Prepare artifact
|
- name: Prepare artifact
|
||||||
if: success()
|
if: success()
|
||||||
run: unzip edxp-core/release/LSPosed-v*-release.zip -d LSPosed-release;
|
run: unzip edxp-core/release/LSPosed-v*-release.zip -d LSPosed-release;
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,4 @@ elf-cleaner.sh
|
||||||
.settings/
|
.settings/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
dalvikdx/bin/
|
.cxx
|
||||||
dexmaker/bin/
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
|
||||||
|
|
||||||
|
android {
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
signingConfigs {
|
||||||
|
def keystorePwd = null
|
||||||
|
def alias = null
|
||||||
|
def pwd = null
|
||||||
|
if (project.rootProject.file('local.properties').exists()) {
|
||||||
|
Properties properties = new Properties()
|
||||||
|
properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||||
|
keystorePwd = properties.getProperty("RELEASE_STORE_PASSWORD")
|
||||||
|
alias = properties.getProperty("RELEASE_KEY_ALIAS")
|
||||||
|
pwd = properties.getProperty("RELEASE_KEY_PASSWORD")
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
storeFile file("edxpmanager.jks")
|
||||||
|
storePassword keystorePwd != null ? keystorePwd : System.getenv("KEYSTORE_PASS")
|
||||||
|
keyAlias alias != null ? alias : System.getenv("ALIAS_NAME")
|
||||||
|
keyPassword pwd != null ? pwd : System.getenv("ALIAS_PASS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lintOptions {
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
disable 'ExtraTranslation'
|
||||||
|
}
|
||||||
|
compileSdkVersion 30
|
||||||
|
buildToolsVersion "30.0.2"
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "org.meowcat.edxposed.manager"
|
||||||
|
minSdkVersion 26
|
||||||
|
targetSdkVersion 30
|
||||||
|
versionCode 458010
|
||||||
|
versionName "4.5.8.1"
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
minifyEnabled false
|
||||||
|
shrinkResources false
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
targetCompatibility = 1.8
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all { variant ->
|
||||||
|
variant.outputs.all { output ->
|
||||||
|
outputFileName = "EdXposedManagerR-${defaultConfig.versionName}-${defaultConfig.versionCode}-${buildType.name}.apk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
|
implementation 'androidx.browser:browser:1.3.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
|
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||||
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
|
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||||
|
implementation "com.github.topjohnwu.libsu:core:2.5.1"
|
||||||
|
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
|
||||||
|
implementation 'com.google.android.material:material:1.2.1'
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
|
implementation 'com.takisoft.preferencex:preferencex:1.1.0'
|
||||||
|
implementation 'com.takisoft.preferencex:preferencex-colorpicker:1.1.0'
|
||||||
|
implementation 'com.timehop.stickyheadersrecyclerview:library:0.4.3@aar'
|
||||||
|
implementation 'tech.rectifier.preferencex-android:preferencex-simplemenu:88f93154b2'
|
||||||
|
implementation 'me.zhanghai.android.appiconloader:appiconloader-glide:1.2.0'
|
||||||
|
implementation 'me.zhanghai.android.fastscroll:library:1.1.5'
|
||||||
|
compileOnly 'de.robv.android.xposed:api:82'
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,23 @@
|
||||||
|
# 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
|
||||||
|
-keep class org.meowcat.edxposed.manager.util.json.** {public *; }
|
||||||
|
-keep class org.meowcat.edxposed.manager.Constants { *; }
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?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.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" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".App"
|
||||||
|
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="AllowBackup,GoogleAppIndexingWarning">
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.CrashReportActivity"
|
||||||
|
android:process=":error_activity" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.AboutActivity"
|
||||||
|
android:label="@string/About" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.LogsActivity"
|
||||||
|
android:label="@string/Logs" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.EdDownloadActivity"
|
||||||
|
android:label="@string/Install" />
|
||||||
|
<activity android:name=".ui.activity.BlackListActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.DownloadDetailsActivity"
|
||||||
|
android:label="@string/nav_item_download" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.DownloadActivity"
|
||||||
|
android:label="@string/Downloads" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.ModuleScopeActivity"
|
||||||
|
android:label="@string/menu_scope" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.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=".ui.activity.ModulesActivity"
|
||||||
|
android:label="@string/Modules" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.SettingsActivity"
|
||||||
|
android:label="@string/Settings">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<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=".util.NotificationUtil$RebootReceiver"
|
||||||
|
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="${applicationId}.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,210 @@
|
||||||
|
package org.meowcat.edxposed.manager;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Application;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.adapters.AppHelper;
|
||||||
|
import org.meowcat.edxposed.manager.receivers.PackageChangeReceiver;
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.CrashReportActivity;
|
||||||
|
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.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class App extends Application implements Application.ActivityLifecycleCallbacks {
|
||||||
|
public static final String TAG = "EdXposedManager";
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private static App instance = null;
|
||||||
|
private static Thread uiThread;
|
||||||
|
private static Handler mainHandler;
|
||||||
|
private SharedPreferences pref;
|
||||||
|
private AppCompatActivity currentActivity = null;
|
||||||
|
private boolean isUiLoaded = false;
|
||||||
|
|
||||||
|
public static App getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void runOnUiThread(Runnable action) {
|
||||||
|
if (Thread.currentThread() != uiThread) {
|
||||||
|
mainHandler.post(action);
|
||||||
|
} else {
|
||||||
|
action.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SharedPreferences getPreferences() {
|
||||||
|
return instance.pref;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void mkdir(String dir) {
|
||||||
|
dir = Constants.getBaseDir() + dir;
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
new File(dir).mkdir();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean supportScope() {
|
||||||
|
return Constants.getXposedApiVersion() >= 92;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
if (!BuildConfig.DEBUG) {
|
||||||
|
try {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
|
|
||||||
|
StringWriter sw = new StringWriter();
|
||||||
|
PrintWriter pw = new PrintWriter(sw);
|
||||||
|
throwable.printStackTrace(pw);
|
||||||
|
String stackTraceString = sw.toString();
|
||||||
|
|
||||||
|
//Reduce data to 128KB so we don't get a TransactionTooLargeException when sending the intent.
|
||||||
|
//The limit is 1MB on Android but some devices seem to have it lower.
|
||||||
|
//See: http://developer.android.com/reference/android/os/TransactionTooLargeException.html
|
||||||
|
//And: http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception#comment46697371_12809171
|
||||||
|
if (stackTraceString.length() > 131071) {
|
||||||
|
String disclaimer = " [stack trace too large]";
|
||||||
|
stackTraceString = stackTraceString.substring(0, 131071 - disclaimer.length()) + disclaimer;
|
||||||
|
}
|
||||||
|
Intent intent = new Intent(App.this, CrashReportActivity.class);
|
||||||
|
intent.putExtra(BuildConfig.APPLICATION_ID + ".EXTRA_STACK_TRACE", stackTraceString);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
|
App.this.startActivity(intent);
|
||||||
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
|
System.exit(10);
|
||||||
|
});
|
||||||
|
} catch (Throwable t) {
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
uiThread = Thread.currentThread();
|
||||||
|
mainHandler = new Handler();
|
||||||
|
|
||||||
|
pref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
|
||||||
|
createDirectories();
|
||||||
|
NotificationUtil.init();
|
||||||
|
registerReceivers();
|
||||||
|
|
||||||
|
registerActivityLifecycleCallbacks(this);
|
||||||
|
|
||||||
|
@SuppressLint("SimpleDateFormat") DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
|
||||||
|
Date date = new Date();
|
||||||
|
|
||||||
|
if (!Objects.requireNonNull(pref.getString("date", "")).equals(dateFormat.format(date))) {
|
||||||
|
pref.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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint({"PrivateApi", "NewApi"})
|
||||||
|
private void createDirectories() {
|
||||||
|
mkdir("conf");
|
||||||
|
mkdir("log");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateProgressIndicator(final SwipeRefreshLayout refreshLayout) {
|
||||||
|
final boolean isLoading = RepoLoader.getInstance().isLoading() || ModuleUtil.getInstance().isLoading();
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
synchronized (App.this) {
|
||||||
|
if (currentActivity != null) {
|
||||||
|
if (refreshLayout != null)
|
||||||
|
refreshLayout.setRefreshing(isLoading);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {
|
||||||
|
if (isUiLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RepoLoader.getInstance().triggerFirstLoadIfNecessary();
|
||||||
|
isUiLoaded = true;
|
||||||
|
|
||||||
|
if (pref.getBoolean("hook_modules", true)) {
|
||||||
|
Collection<ModuleUtil.InstalledModule> installedModules = ModuleUtil.getInstance().getModules().values();
|
||||||
|
for (ModuleUtil.InstalledModule info : installedModules) {
|
||||||
|
if (!AppHelper.FORCE_WHITE_LIST_MODULE.contains(info.packageName)) {
|
||||||
|
AppHelper.FORCE_WHITE_LIST_MODULE.add(info.packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(TAG, "ApplicationList: Force add modules to list");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityStarted(@NonNull Activity activity) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onActivityResumed(@NonNull Activity activity) {
|
||||||
|
currentActivity = (AppCompatActivity) activity;
|
||||||
|
updateProgressIndicator(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onActivityPaused(@NonNull Activity activity) {
|
||||||
|
currentActivity = 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,37 @@
|
||||||
|
package org.meowcat.edxposed.manager;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public class Constants {
|
||||||
|
public static int getXposedApiVersion() {
|
||||||
|
Log.e(App.TAG, "getXposedApiVersion: Xposed is not active");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getXposedVersion() {
|
||||||
|
Log.e(App.TAG, "getXposedVersion: Xposed is not active");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getXposedVersionCode() {
|
||||||
|
Log.e(App.TAG, "getXposedVersionCode: Xposed is not active");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getXposedVariant() {
|
||||||
|
Log.e(App.TAG, "getXposedVariant: Xposed is not active");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getEnabledModulesListFile() {
|
||||||
|
return getBaseDir() + "conf/enabled_modules.list";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getModulesListFile() {
|
||||||
|
return getBaseDir() + "conf/modules.list";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getBaseDir() {
|
||||||
|
return App.getInstance().getApplicationInfo().deviceProtectedDataDir + "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,322 @@
|
||||||
|
package org.meowcat.edxposed.manager.adapters;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
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.Filterable;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.widget.SwitchCompat;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.bumptech.glide.request.target.CustomTarget;
|
||||||
|
import com.bumptech.glide.request.transition.Transition;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.util.GlideApp;
|
||||||
|
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> implements Filterable {
|
||||||
|
|
||||||
|
protected Context context;
|
||||||
|
private final ApplicationInfo.DisplayNameComparator displayNameComparator;
|
||||||
|
private Callback callback;
|
||||||
|
protected List<ApplicationInfo> fullList, showList;
|
||||||
|
private final DateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||||
|
private List<String> checkedList;
|
||||||
|
private final PackageManager pm;
|
||||||
|
private final 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_module, parent, false);
|
||||||
|
return new ViewHolder(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadApps() {
|
||||||
|
fullList = pm.getInstalledApplications(PackageManager.GET_META_DATA);
|
||||||
|
List<ApplicationInfo> rmList = new ArrayList<>();
|
||||||
|
for (ApplicationInfo info : fullList) {
|
||||||
|
if (this instanceof ScopeAdapter) {
|
||||||
|
if (AppHelper.isBlackListMode()) {
|
||||||
|
if (AppHelper.isWhiteListMode()) {
|
||||||
|
List<String> whiteList = AppHelper.getWhiteList();
|
||||||
|
if (!whiteList.contains(info.packageName)) {
|
||||||
|
rmList.add(info);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List<String> blackList = AppHelper.getBlackList();
|
||||||
|
if (blackList.contains(info.packageName)) {
|
||||||
|
rmList.add(info);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.packageName.equals(((ScopeAdapter) this).modulePackageName)) {
|
||||||
|
rmList.add(info);
|
||||||
|
}
|
||||||
|
} else if (!App.getPreferences().getBoolean("show_modules", true)) {
|
||||||
|
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();
|
||||||
|
showList = fullList;
|
||||||
|
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 (App.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;
|
||||||
|
}
|
||||||
|
fullList.sort((a, b) -> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
ApplicationInfo info = showList.get(position);
|
||||||
|
holder.appName.setText(InstallApkUtil.getAppLabel(info, pm));
|
||||||
|
try {
|
||||||
|
PackageInfo packageInfo = pm.getPackageInfo(info.packageName, 0);
|
||||||
|
GlideApp.with(holder.appIcon)
|
||||||
|
.load(packageInfo)
|
||||||
|
.into(new CustomTarget<Drawable>() {
|
||||||
|
@Override
|
||||||
|
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
|
||||||
|
holder.appIcon.setImageDrawable(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
holder.appVersion.setText(packageInfo.versionName);
|
||||||
|
holder.appVersion.setSelected(true);
|
||||||
|
String creationDate = dateformat.format(new Date(packageInfo.firstInstallTime));
|
||||||
|
String updateDate = dateformat.format(new Date(packageInfo.lastUpdateTime));
|
||||||
|
holder.timestamps.setText(holder.itemView.getContext().getString(R.string.install_timestamps, creationDate, updateDate));
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
holder.appPackage.setText(info.packageName);
|
||||||
|
|
||||||
|
holder.mSwitch.setOnCheckedChangeListener(null);
|
||||||
|
holder.mSwitch.setChecked(checkedList.contains(info.packageName));
|
||||||
|
if (this instanceof ScopeAdapter) {
|
||||||
|
holder.mSwitch.setEnabled(((ScopeAdapter) this).enabled);
|
||||||
|
} else {
|
||||||
|
holder.mSwitch.setEnabled(true);
|
||||||
|
}
|
||||||
|
holder.mSwitch.setOnCheckedChangeListener((v, isChecked) ->
|
||||||
|
onCheckedChange(v, isChecked, info));
|
||||||
|
holder.itemView.setOnClickListener(v -> {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onItemClick(v, info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return showList.get(position).packageName.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Filter getFilter() {
|
||||||
|
return new ApplicationFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
|
||||||
|
ImageView appIcon;
|
||||||
|
TextView appName;
|
||||||
|
TextView appPackage;
|
||||||
|
TextView appVersion;
|
||||||
|
TextView timestamps;
|
||||||
|
SwitchCompat mSwitch;
|
||||||
|
|
||||||
|
ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
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);
|
||||||
|
timestamps = itemView.findViewById(R.id.timestamps);
|
||||||
|
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.toString().isEmpty()) {
|
||||||
|
showList = fullList;
|
||||||
|
} else {
|
||||||
|
ArrayList<ApplicationInfo> filtered = new ArrayList<>();
|
||||||
|
String filter = constraint.toString().toLowerCase();
|
||||||
|
for (ApplicationInfo info : fullList) {
|
||||||
|
if (lowercaseContains(InstallApkUtil.getAppLabel(info, pm), filter)
|
||||||
|
|| lowercaseContains(info.packageName, filter)) {
|
||||||
|
filtered.add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showList = filtered;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,351 @@
|
||||||
|
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.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.App;
|
||||||
|
import org.meowcat.edxposed.manager.BuildConfig;
|
||||||
|
import org.meowcat.edxposed.manager.Constants;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.util.CompileUtil;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
|
||||||
|
|
||||||
|
public class AppHelper {
|
||||||
|
|
||||||
|
private static final String BASE_PATH = Constants.getBaseDir();
|
||||||
|
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 SCOPE_LIST_PATH = "conf/%s.conf";
|
||||||
|
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));
|
||||||
|
public static List<String> FORCE_WHITE_LIST_MODULE = new ArrayList<>(FORCE_WHITE_LIST);
|
||||||
|
|
||||||
|
private static final HashMap<String, List<String>> scopeList = new HashMap<>();
|
||||||
|
|
||||||
|
static void makeSurePath() {
|
||||||
|
App.mkdir(WHITE_LIST_PATH);
|
||||||
|
App.mkdir(BLACK_LIST_PATH);
|
||||||
|
App.mkdir(COMPAT_LIST_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
} 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 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());
|
||||||
|
} 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());
|
||||||
|
} 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 -> {
|
||||||
|
int itemId = menuItem.getItemId();
|
||||||
|
if (itemId == 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();
|
||||||
|
}
|
||||||
|
} else if (itemId == 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();
|
||||||
|
}
|
||||||
|
} else if (itemId == R.id.app_menu_compile_speed) {
|
||||||
|
CompileUtil.compileSpeed(context, fragmentManager, info);
|
||||||
|
} else if (itemId == R.id.app_menu_compile_dexopt) {
|
||||||
|
CompileUtil.compileDexopt(context, fragmentManager, info);
|
||||||
|
} else if (itemId == R.id.app_menu_compile_reset) {
|
||||||
|
CompileUtil.reset(context, fragmentManager, info);
|
||||||
|
} else if (itemId == 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();
|
||||||
|
}
|
||||||
|
} else if (itemId == R.id.app_menu_info) {
|
||||||
|
context.startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", info.packageName, null)));
|
||||||
|
} else if (itemId == R.id.app_menu_uninstall) {
|
||||||
|
context.startActivity(new Intent(Intent.ACTION_UNINSTALL_PACKAGE, Uri.fromParts("package", info.packageName, null)));
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> getScopeList(String modulePackageName) {
|
||||||
|
if (scopeList.containsKey(modulePackageName)) {
|
||||||
|
return scopeList.get(modulePackageName);
|
||||||
|
}
|
||||||
|
File file = new File(BASE_PATH + String.format(SCOPE_LIST_PATH, modulePackageName));
|
||||||
|
List<String> s = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
|
||||||
|
for (String line; (line = bufferedReader.readLine()) != null; ) {
|
||||||
|
s.add(line);
|
||||||
|
}
|
||||||
|
scopeList.put(modulePackageName, s);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("WorldReadableFiles")
|
||||||
|
static boolean saveScopeList(String modulePackageName, List<String> list) {
|
||||||
|
File file = new File(BASE_PATH + String.format(SCOPE_LIST_PATH, modulePackageName));
|
||||||
|
if (list.size() == 0) {
|
||||||
|
scopeList.put(modulePackageName, list);
|
||||||
|
return file.delete();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
PrintWriter pr = new PrintWriter(new FileWriter(file));
|
||||||
|
for (String line : list) {
|
||||||
|
pr.println(line);
|
||||||
|
}
|
||||||
|
pr.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
scopeList.put(modulePackageName, list);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package org.meowcat.edxposed.manager.adapters;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
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 final boolean isWhiteListMode;
|
||||||
|
private List<String> checkedList;
|
||||||
|
|
||||||
|
public BlackListAdapter(Context context, boolean isWhiteListMode) {
|
||||||
|
super(context);
|
||||||
|
this.isWhiteListMode = isWhiteListMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> generateCheckedList() {
|
||||||
|
if (App.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,41 @@
|
||||||
|
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.util.ToastUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CompatListAdapter extends AppAdapter {
|
||||||
|
|
||||||
|
private List<String> checkedList;
|
||||||
|
|
||||||
|
public CompatListAdapter(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> generateCheckedList() {
|
||||||
|
AppHelper.makeSurePath();
|
||||||
|
return checkedList = AppHelper.getCompatList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCheckedChange(CompoundButton view, boolean isChecked, ApplicationInfo info) {
|
||||||
|
boolean success = isChecked ?
|
||||||
|
AppHelper.addCompatList(info.packageName) : AppHelper.removeCompatList(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,123 @@
|
||||||
|
package org.meowcat.edxposed.manager.adapters;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.DataSetObserver;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
|
||||||
|
|
||||||
|
private Cursor cursor;
|
||||||
|
|
||||||
|
private boolean dataValid;
|
||||||
|
|
||||||
|
private int rowIdColumn;
|
||||||
|
|
||||||
|
private final DataSetObserver dataSetObserver;
|
||||||
|
|
||||||
|
public CursorRecyclerViewAdapter(Cursor cursor) {
|
||||||
|
this.cursor = cursor;
|
||||||
|
dataValid = cursor != null;
|
||||||
|
rowIdColumn = dataValid ? cursor.getColumnIndex("_id") : -1;
|
||||||
|
dataSetObserver = new NotifyingDataSetObserver();
|
||||||
|
if (this.cursor != null) {
|
||||||
|
this.cursor.registerDataSetObserver(dataSetObserver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Cursor getCursor() {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
if (dataValid && cursor != null) {
|
||||||
|
return cursor.getCount();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
if (dataValid && cursor != null && cursor.moveToPosition(position)) {
|
||||||
|
return cursor.getLong(rowIdColumn);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHasStableIds(boolean hasStableIds) {
|
||||||
|
super.setHasStableIds(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void onBindViewHolder(VH viewHolder, Cursor cursor);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull VH viewHolder, int position) {
|
||||||
|
if (!dataValid) {
|
||||||
|
throw new IllegalStateException("this should only be called when the cursor is valid");
|
||||||
|
}
|
||||||
|
if (!cursor.moveToPosition(position)) {
|
||||||
|
throw new IllegalStateException("couldn't move cursor to position " + position);
|
||||||
|
}
|
||||||
|
onBindViewHolder(viewHolder, cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
private Cursor swapCursor(Cursor newCursor) {
|
||||||
|
if (newCursor == cursor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Cursor oldCursor = cursor;
|
||||||
|
if (oldCursor != null && dataSetObserver != null) {
|
||||||
|
oldCursor.unregisterDataSetObserver(dataSetObserver);
|
||||||
|
}
|
||||||
|
cursor = newCursor;
|
||||||
|
if (cursor != null) {
|
||||||
|
if (dataSetObserver != null) {
|
||||||
|
cursor.registerDataSetObserver(dataSetObserver);
|
||||||
|
}
|
||||||
|
rowIdColumn = newCursor.getColumnIndexOrThrow("_id");
|
||||||
|
dataValid = true;
|
||||||
|
} else {
|
||||||
|
rowIdColumn = -1;
|
||||||
|
dataValid = false;
|
||||||
|
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
|
||||||
|
}
|
||||||
|
notifyDataSetChanged();
|
||||||
|
return oldCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NotifyingDataSetObserver extends DataSetObserver {
|
||||||
|
@Override
|
||||||
|
public void onChanged() {
|
||||||
|
super.onChanged();
|
||||||
|
dataValid = true;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInvalidated() {
|
||||||
|
super.onInvalidated();
|
||||||
|
dataValid = false;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package org.meowcat.edxposed.manager.adapters;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.ui.widget.MasterSwitch;
|
||||||
|
import org.meowcat.edxposed.manager.util.ToastUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ScopeAdapter extends AppAdapter {
|
||||||
|
|
||||||
|
protected final String modulePackageName;
|
||||||
|
protected boolean enabled = true;
|
||||||
|
private List<String> checkedList;
|
||||||
|
private final MasterSwitch masterSwitch;
|
||||||
|
|
||||||
|
public ScopeAdapter(Context context, String modulePackageName, MasterSwitch masterSwitch) {
|
||||||
|
super(context);
|
||||||
|
this.modulePackageName = modulePackageName;
|
||||||
|
this.masterSwitch = masterSwitch;
|
||||||
|
masterSwitch.setTitle(context.getString(R.string.enable_scope));
|
||||||
|
masterSwitch.setOnCheckedChangedListener(new MasterSwitch.OnCheckedChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(boolean checked) {
|
||||||
|
enabled = checked;
|
||||||
|
AppHelper.saveScopeList(modulePackageName, enabled ? checkedList : new ArrayList<>());
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> generateCheckedList() {
|
||||||
|
AppHelper.makeSurePath();
|
||||||
|
List<String> scopeList = AppHelper.getScopeList(modulePackageName);
|
||||||
|
List<String> list = new ArrayList<>();
|
||||||
|
for (ApplicationInfo info : fullList) {
|
||||||
|
list.add(info.packageName);
|
||||||
|
}
|
||||||
|
scopeList.retainAll(list);
|
||||||
|
checkedList = scopeList;
|
||||||
|
enabled = checkedList.size() != 0;
|
||||||
|
((Activity) context).runOnUiThread(() -> masterSwitch.setChecked(enabled));
|
||||||
|
return checkedList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCheckedChange(CompoundButton view, boolean isChecked, ApplicationInfo info) {
|
||||||
|
if (isChecked) {
|
||||||
|
checkedList.add(info.packageName);
|
||||||
|
} else {
|
||||||
|
checkedList.remove(info.packageName);
|
||||||
|
}
|
||||||
|
if (!AppHelper.saveScopeList(modulePackageName, checkedList)) {
|
||||||
|
ToastUtil.showShortToast(context, R.string.add_package_failed);
|
||||||
|
if (!isChecked) {
|
||||||
|
checkedList.add(info.packageName);
|
||||||
|
} else {
|
||||||
|
checkedList.remove(info.packageName);
|
||||||
|
}
|
||||||
|
view.setChecked(!isChecked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package org.meowcat.edxposed.manager.receivers;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.meowcat.edxposed.manager.BuildConfig;
|
||||||
|
import org.meowcat.edxposed.manager.util.NotificationUtil;
|
||||||
|
import org.meowcat.edxposed.manager.util.TaskRunner;
|
||||||
|
import org.meowcat.edxposed.manager.util.json.JSONUtils;
|
||||||
|
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
public class BootReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(final Context context, Intent intent) {
|
||||||
|
new TaskRunner().executeAsync(new LongRunningTask());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class LongRunningTask implements Callable<Void> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void call() {
|
||||||
|
try {
|
||||||
|
Thread.sleep(60 * 60 * 1000);
|
||||||
|
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) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
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 moduleUtil = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleUtil = getModuleUtilInstance();
|
||||||
|
|
||||||
|
moduleUtil.updateModulesList(false);
|
||||||
|
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 (moduleUtil.isModuleEnabled(packageName)) {
|
||||||
|
moduleUtil.setModuleEnabled(packageName, false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleUtil.isModuleEnabled(packageName)) {
|
||||||
|
NotificationUtil.showModulesUpdatedNotification();
|
||||||
|
} else {
|
||||||
|
NotificationUtil.showNotActivatedNotification(packageName, module.getAppName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleUtil getModuleUtilInstance() {
|
||||||
|
if (moduleUtil == null) {
|
||||||
|
moduleUtil = ModuleUtil.getInstance();
|
||||||
|
}
|
||||||
|
return moduleUtil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.App;
|
||||||
|
import org.meowcat.edxposed.manager.BuildConfig;
|
||||||
|
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 final SQLiteDatabase db;
|
||||||
|
|
||||||
|
static {
|
||||||
|
RepoDb instance = new RepoDb(App.getInstance());
|
||||||
|
db = instance.getWritableDatabase();
|
||||||
|
db.execSQL("PRAGMA foreign_keys=ON");
|
||||||
|
instance.createTempTables(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
db.beginTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setTransactionSuccessful() {
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void endTransation() {
|
||||||
|
db.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 = db.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 db.insertOrThrow(RepositoriesColumns.TABLE_NAME, null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteRepositories() {
|
||||||
|
if (db != null)
|
||||||
|
db.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 = db.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);
|
||||||
|
db.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);
|
||||||
|
db.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);
|
||||||
|
|
||||||
|
db.beginTransaction();
|
||||||
|
try {
|
||||||
|
long moduleId = db.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);
|
||||||
|
db.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
|
||||||
|
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
return moduleId;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
db.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 db.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 db.insertOrThrow(MoreInfoColumns.TABLE_NAME, null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteAllModules(long repoId) {
|
||||||
|
db.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ?", new String[]{Long.toString(repoId)});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteModule(long repoId, String packageName) {
|
||||||
|
db.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 = db.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 = db.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 = db.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();
|
||||||
|
db.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() {
|
||||||
|
db.beginTransaction();
|
||||||
|
try {
|
||||||
|
String[] projection = new String[]{ModulesColumns.PKGNAME};
|
||||||
|
Cursor c = db.query(true, ModulesColumns.TABLE_NAME, projection, null, null, null, null, null, null);
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
updateModuleLatestVersion(c.getString(0));
|
||||||
|
}
|
||||||
|
c.close();
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
db.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 db.insertOrThrow(InstalledModulesColumns.TABLE_NAME, null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteInstalledModule(String packageName) {
|
||||||
|
db.delete(InstalledModulesColumns.TABLE_NAME, InstalledModulesColumns.PKGNAME + " = ?", new String[]{packageName});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteAllInstalledModules() {
|
||||||
|
db.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 = db.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 = db.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,320 @@
|
||||||
|
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.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 androidx.core.text.HtmlCompat;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.request.target.CustomTarget;
|
||||||
|
import com.bumptech.glide.request.transition.Transition;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
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 = App.TAG;
|
||||||
|
private final static String NS = null;
|
||||||
|
private final XmlPullParser parser;
|
||||||
|
private final RepoParserCallback callback;
|
||||||
|
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();
|
||||||
|
this.callback = 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 = HtmlCompat.fromHtml(source, HtmlCompat.FROM_HTML_MODE_LEGACY, source1 -> {
|
||||||
|
|
||||||
|
LevelListDrawable levelListDrawable = new LevelListDrawable();
|
||||||
|
final Drawable[] drawable = new Drawable[1];
|
||||||
|
Glide.with(context).asBitmap().load(source1).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)
|
||||||
|
callback.onNewModule(module);
|
||||||
|
break;
|
||||||
|
case "remove-module":
|
||||||
|
triggerRepoEvent(repository);
|
||||||
|
String packageName = readRemoveModule();
|
||||||
|
if (packageName != null)
|
||||||
|
callback.onRemoveModule(packageName);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
//skip(true);
|
||||||
|
skip(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.onCompleted(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void triggerRepoEvent(Repository repository) {
|
||||||
|
if (mRepoEventTriggered)
|
||||||
|
return;
|
||||||
|
|
||||||
|
callback.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,83 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.core.text.HtmlCompat;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity;
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.BuildConfig;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityAboutBinding;
|
||||||
|
import org.meowcat.edxposed.manager.util.GlideHelper;
|
||||||
|
import org.meowcat.edxposed.manager.util.NavUtil;
|
||||||
|
|
||||||
|
public class AboutActivity extends BaseActivity {
|
||||||
|
ActivityAboutBinding binding;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
binding = ActivityAboutBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
setSupportActionBar(binding.toolbar);
|
||||||
|
binding.toolbar.setNavigationOnClickListener(view -> finish());
|
||||||
|
ActionBar bar = getSupportActionBar();
|
||||||
|
if (bar != null) {
|
||||||
|
bar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
setupWindowInsets(binding.snackbar, null);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
binding.changelogView.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
binding.changelogView.setOnClickListener(v1 -> new MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(R.string.changes)
|
||||||
|
.setMessage(HtmlCompat.fromHtml(changes, HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
|
.setPositiveButton(android.R.string.ok, null).show());
|
||||||
|
}
|
||||||
|
binding.appVersion.setText(BuildConfig.VERSION_NAME);
|
||||||
|
|
||||||
|
binding.licensesView.setOnClickListener(v12 -> startActivity(new Intent(this, OssLicensesMenuActivity.class)));
|
||||||
|
|
||||||
|
binding.tabSupportModuleDescription.setText(getString(R.string.support_modules_description,
|
||||||
|
getString(R.string.module_support)));
|
||||||
|
|
||||||
|
setupView(binding.installerSupportView, R.string.support_material_xda);
|
||||||
|
setupView(binding.faqView, R.string.support_faq_url);
|
||||||
|
setupView(binding.tgGroupView, R.string.group_telegram_link);
|
||||||
|
setupView(binding.qqGroupView, R.string.group_qq_link);
|
||||||
|
setupView(binding.donateView, R.string.support_donate_url);
|
||||||
|
setupView(binding.sourceCodeView, R.string.about_source);
|
||||||
|
setupView(binding.tgChannelView, R.string.group_telegram_channel_link);
|
||||||
|
|
||||||
|
if (translator.isEmpty()) {
|
||||||
|
binding.translatorsView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Glide.with(binding.appIcon)
|
||||||
|
.load(GlideHelper.wrapApplicationInfoForIconLoader(getApplicationInfo()))
|
||||||
|
.into(binding.appIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupView(View v, final int url) {
|
||||||
|
v.setOnClickListener(v1 -> NavUtil.startURL(this, getString(url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openLink(View view) {
|
||||||
|
NavUtil.startURL(this, view.getTag().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StyleRes;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.topjohnwu.superuser.Shell;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.util.CustomThemeColor;
|
||||||
|
import org.meowcat.edxposed.manager.util.CustomThemeColors;
|
||||||
|
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 theme;
|
||||||
|
|
||||||
|
public static boolean isBlackNightTheme() {
|
||||||
|
return App.getPreferences().getBoolean("black_dark_theme", false) || App.getPreferences().getBoolean("md2", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTheme(Context context) {
|
||||||
|
if (isBlackNightTheme()
|
||||||
|
&& isNightMode(context.getResources().getConfiguration()))
|
||||||
|
return THEME_BLACK;
|
||||||
|
|
||||||
|
return THEME_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isNightMode(Configuration configuration) {
|
||||||
|
return (configuration.uiMode & Configuration.UI_MODE_NIGHT_YES) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContentView(View view) {
|
||||||
|
FrameLayout frameLayout = new FrameLayout(this);
|
||||||
|
frameLayout.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||||
|
if (displayMetrics.widthPixels > displayMetrics.heightPixels) {
|
||||||
|
int padding = (displayMetrics.widthPixels - displayMetrics.heightPixels) / 2;
|
||||||
|
frameLayout.setPadding(padding, 0, padding, 0);
|
||||||
|
}
|
||||||
|
frameLayout.addView(view);
|
||||||
|
super.setContentView(frameLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@StyleRes
|
||||||
|
public int getThemeStyleRes(Context context) {
|
||||||
|
switch (getTheme(context)) {
|
||||||
|
case THEME_BLACK:
|
||||||
|
return R.style.ThemeOverlay_Black;
|
||||||
|
case THEME_DEFAULT:
|
||||||
|
default:
|
||||||
|
return R.style.ThemeOverlay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StyleRes
|
||||||
|
private int getCustomTheme() {
|
||||||
|
String baseThemeName = App.getPreferences().getBoolean("colorized_action_bar", false) && !App.getPreferences().getBoolean("md2", false) ?
|
||||||
|
"ThemeOverlay.ActionBarPrimaryColor" : "ThemeOverlay";
|
||||||
|
String customThemeName;
|
||||||
|
String primaryColorEntryName = "colorPrimary";
|
||||||
|
for (CustomThemeColor color : CustomThemeColors.Primary.values()) {
|
||||||
|
if (App.getPreferences().getInt("primary_color", ContextCompat.getColor(this, R.color.colorPrimary))
|
||||||
|
== ContextCompat.getColor(this, color.getResourceId())) {
|
||||||
|
primaryColorEntryName = color.getResourceEntryName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String accentColorEntryName = "colorAccent";
|
||||||
|
for (CustomThemeColor color : CustomThemeColors.Accent.values()) {
|
||||||
|
if (App.getPreferences().getInt("accent_color", ContextCompat.getColor(this, R.color.colorAccent))
|
||||||
|
== ContextCompat.getColor(this, color.getResourceId())) {
|
||||||
|
accentColorEntryName = color.getResourceEntryName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customThemeName = baseThemeName + "." + primaryColorEntryName + "." + accentColorEntryName;
|
||||||
|
return getResources().getIdentifier(customThemeName, "style", getPackageName());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setupWindowInsets(View rootView, View secondView) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
AppCompatDelegate.setDefaultNightMode(App.getPreferences().getInt("theme", -1));
|
||||||
|
theme = getTheme(this) + getCustomTheme() + App.getPreferences().getBoolean("md2", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getThemedColor(int id) {
|
||||||
|
TypedArray typedArray = getTheme().obtainStyledAttributes(new int[]{id});
|
||||||
|
int color = typedArray.getColor(0, 0);
|
||||||
|
typedArray.recycle();
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (!(this instanceof MainActivity)) {
|
||||||
|
if (App.getPreferences().getBoolean("transparent_status_bar", false)) {
|
||||||
|
getWindow().setStatusBarColor(getThemedColor(R.attr.colorActionBar));
|
||||||
|
} else {
|
||||||
|
getWindow().setStatusBarColor(getThemedColor(R.attr.colorPrimaryDark));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Objects.equals(theme, getTheme(this) + getCustomTheme() + App.getPreferences().getBoolean("md2", false))) {
|
||||||
|
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(getCustomTheme(), true);
|
||||||
|
if (App.getPreferences().getBoolean("md2", false) && !(this instanceof MainActivity)) {
|
||||||
|
theme.applyStyle(R.style.ThemeOverlay_Md2, true);
|
||||||
|
}
|
||||||
|
if (this instanceof MainActivity) {
|
||||||
|
theme.applyStyle(R.style.ThemeOverlay_ActivityMain, true);
|
||||||
|
}
|
||||||
|
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 (!Shell.rootAccess()) {
|
||||||
|
showAlert(getString(R.string.root_failed));
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showAlert(final String result) {
|
||||||
|
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||||
|
runOnUiThread(() -> showAlert(result));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(this)
|
||||||
|
.setMessage(result)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reboot(String mode) {
|
||||||
|
if (!Shell.rootAccess()) {
|
||||||
|
showAlert(getString(R.string.root_failed));
|
||||||
|
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) {
|
||||||
|
int itemId = item.getItemId();
|
||||||
|
if (itemId == 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();
|
||||||
|
App.runOnUiThread(() -> Toast.makeText(BaseActivity.this, R.string.done, Toast.LENGTH_LONG).show());
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
});
|
||||||
|
} else if (itemId == R.id.speed_all) {
|
||||||
|
areYouSure(R.string.take_while_cannot_resore, (dialog, which) -> {
|
||||||
|
new MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(R.string.speed_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();
|
||||||
|
App.runOnUiThread(() -> Toast.makeText(BaseActivity.this, R.string.done, Toast.LENGTH_LONG).show());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (itemId == R.id.reboot) {
|
||||||
|
areYouSure(R.string.reboot, (dialog, which) -> reboot(null));
|
||||||
|
} else if (itemId == R.id.soft_reboot) {
|
||||||
|
areYouSure(R.string.soft_reboot, (dialog, which) -> softReboot());
|
||||||
|
} else if (itemId == R.id.reboot_recovery) {
|
||||||
|
areYouSure(R.string.reboot_recovery, (dialog, which) -> reboot("recovery"));
|
||||||
|
} else if (itemId == R.id.reboot_bootloader) {
|
||||||
|
areYouSure(R.string.reboot_bootloader, (dialog, which) -> reboot("bootloader"));
|
||||||
|
} else if (itemId == R.id.reboot_download) {
|
||||||
|
areYouSure(R.string.reboot_download, (dialog, which) -> reboot("download"));
|
||||||
|
} else if (itemId == R.id.reboot_edl) {
|
||||||
|
areYouSure(R.string.reboot_edl, (dialog, which) -> reboot("edl"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
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 com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.adapters.AppAdapter;
|
||||||
|
import org.meowcat.edxposed.manager.adapters.AppHelper;
|
||||||
|
import org.meowcat.edxposed.manager.adapters.BlackListAdapter;
|
||||||
|
import org.meowcat.edxposed.manager.adapters.CompatListAdapter;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityBlackListBinding;
|
||||||
|
import org.meowcat.edxposed.manager.util.LinearLayoutManagerFix;
|
||||||
|
|
||||||
|
import me.zhanghai.android.fastscroll.FastScrollerBuilder;
|
||||||
|
|
||||||
|
public class BlackListActivity extends BaseActivity implements AppAdapter.Callback {
|
||||||
|
private SearchView searchView;
|
||||||
|
private AppAdapter appAdapter;
|
||||||
|
|
||||||
|
private SearchView.OnQueryTextListener searchListener;
|
||||||
|
private ActivityBlackListBinding binding;
|
||||||
|
private final Runnable runnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
binding.swipeRefreshLayout.setRefreshing(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private final Handler handler = new Handler();
|
||||||
|
private boolean isCompat;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
isCompat = getIntent().getBooleanExtra("compat_list", false);
|
||||||
|
binding = ActivityBlackListBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
setSupportActionBar(binding.toolbar);
|
||||||
|
binding.toolbar.setNavigationOnClickListener(view -> finish());
|
||||||
|
ActionBar bar = getSupportActionBar();
|
||||||
|
if (bar != null) {
|
||||||
|
bar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
setupWindowInsets(binding.snackbar, binding.recyclerView);
|
||||||
|
final boolean isWhiteListMode = isWhiteListMode();
|
||||||
|
appAdapter = isCompat ? new CompatListAdapter(this) : new BlackListAdapter(this, isWhiteListMode);
|
||||||
|
appAdapter.setHasStableIds(true);
|
||||||
|
binding.recyclerView.setAdapter(appAdapter);
|
||||||
|
binding.recyclerView.setLayoutManager(new LinearLayoutManagerFix(this));
|
||||||
|
FastScrollerBuilder fastScrollerBuilder = new FastScrollerBuilder(binding.recyclerView);
|
||||||
|
if (!App.getPreferences().getBoolean("md2", false)) {
|
||||||
|
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this,
|
||||||
|
DividerItemDecoration.VERTICAL);
|
||||||
|
binding.recyclerView.addItemDecoration(dividerItemDecoration);
|
||||||
|
} else {
|
||||||
|
fastScrollerBuilder.useMd2Style();
|
||||||
|
}
|
||||||
|
fastScrollerBuilder.build();
|
||||||
|
appAdapter.setCallback(this);
|
||||||
|
handler.postDelayed(runnable, 300);
|
||||||
|
binding.swipeRefreshLayout.setOnRefreshListener(appAdapter::refresh);
|
||||||
|
|
||||||
|
searchListener = new SearchView.OnQueryTextListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextSubmit(String query) {
|
||||||
|
appAdapter.filter(query);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextChange(String newText) {
|
||||||
|
appAdapter.filter(newText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.menu_app_list, menu);
|
||||||
|
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
|
||||||
|
searchView.setOnQueryTextListener(searchListener);
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (!isCompat && !AppHelper.isBlackListMode()) {
|
||||||
|
new MaterialAlertDialogBuilder(this)
|
||||||
|
.setMessage(R.string.warning_list_not_enabled)
|
||||||
|
.setPositiveButton(R.string.Settings, (dialog, which) -> {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(BlackListActivity.this, SettingsActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
|
||||||
|
.setCancelable(false)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
changeTitle(isBlackListMode(), isWhiteListMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void changeTitle(boolean isBlackListMode, boolean isWhiteListMode) {
|
||||||
|
if (isCompat) {
|
||||||
|
setTitle(R.string.nav_title_compat_list);
|
||||||
|
} else 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() {
|
||||||
|
handler.removeCallbacks(runnable);
|
||||||
|
binding.swipeRefreshLayout.setRefreshing(false);
|
||||||
|
String queryStr = searchView != null ? searchView.getQuery().toString() : "";
|
||||||
|
runOnUiThread(() -> appAdapter.getFilter().filter(queryStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemClick(View v, ApplicationInfo info) {
|
||||||
|
AppHelper.showMenu(this, getSupportFragmentManager(), v, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (searchView.isIconified()) {
|
||||||
|
super.onBackPressed();
|
||||||
|
} else {
|
||||||
|
searchView.setIconified(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.BuildConfig;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityCrashReportBinding;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
public class CrashReportActivity extends AppCompatActivity {
|
||||||
|
ActivityCrashReportBinding binding;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
binding = ActivityCrashReportBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
binding.copyLogs.setOnClickListener(v -> {
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||||
|
//Are there any devices without clipboard...?
|
||||||
|
if (clipboard != null) {
|
||||||
|
ClipData clip = ClipData.newPlainText("edcrash", getAllErrorDetailsFromIntent(getIntent()));
|
||||||
|
clipboard.setPrimaryClip(clip);
|
||||||
|
Snackbar.make(binding.snackbar, R.string.copy_toast_msg, Snackbar.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAllErrorDetailsFromIntent(@NonNull Intent intent) {
|
||||||
|
Date currentDate = new Date();
|
||||||
|
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
|
||||||
|
|
||||||
|
String buildDateAsString = getBuildDateAsString(dateFormat);
|
||||||
|
|
||||||
|
String versionName = getVersionName();
|
||||||
|
|
||||||
|
String errorDetails = "";
|
||||||
|
|
||||||
|
errorDetails += "Build version: " + versionName + " \n";
|
||||||
|
if (buildDateAsString != null) {
|
||||||
|
errorDetails += "Build date: " + buildDateAsString + " \n";
|
||||||
|
}
|
||||||
|
errorDetails += "Current date: " + dateFormat.format(currentDate) + " \n";
|
||||||
|
errorDetails += "Device: " + getDeviceModelName() + " \n \n";
|
||||||
|
errorDetails += "Stack trace: \n";
|
||||||
|
errorDetails += getStackTraceFromIntent(intent);
|
||||||
|
return errorDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getBuildDateAsString(@NonNull DateFormat dateFormat) {
|
||||||
|
long buildDate;
|
||||||
|
try {
|
||||||
|
ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), 0);
|
||||||
|
ZipFile zf = new ZipFile(ai.sourceDir);
|
||||||
|
|
||||||
|
ZipEntry ze = zf.getEntry("classes.dex");
|
||||||
|
buildDate = ze.getTime();
|
||||||
|
|
||||||
|
|
||||||
|
zf.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
buildDate = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildDate > 312764400000L) {
|
||||||
|
return dateFormat.format(new Date(buildDate));
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getVersionName() {
|
||||||
|
try {
|
||||||
|
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
|
||||||
|
return packageInfo.versionName;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDeviceModelName() {
|
||||||
|
String manufacturer = Build.MANUFACTURER;
|
||||||
|
String model = Build.MODEL;
|
||||||
|
if (model.startsWith(manufacturer)) {
|
||||||
|
return capitalize(model);
|
||||||
|
} else {
|
||||||
|
return capitalize(manufacturer) + " " + model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String capitalize(@Nullable String s) {
|
||||||
|
if (s == null || s.length() == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
char first = s.charAt(0);
|
||||||
|
if (Character.isUpperCase(first)) {
|
||||||
|
return s;
|
||||||
|
} else {
|
||||||
|
return Character.toUpperCase(first) + s.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStackTraceFromIntent(@NonNull Intent intent) {
|
||||||
|
return intent.getStringExtra(BuildConfig.APPLICATION_ID + ".EXTRA_STACK_TRACE");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,393 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
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.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
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.content.ContextCompat;
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.transition.TransitionManager;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersAdapter;
|
||||||
|
import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersDecoration;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.adapters.CursorRecyclerViewAdapter;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityDownloadBinding;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ItemDownloadBinding;
|
||||||
|
import org.meowcat.edxposed.manager.repo.RepoDb;
|
||||||
|
import org.meowcat.edxposed.manager.repo.RepoDbDefinitions;
|
||||||
|
import org.meowcat.edxposed.manager.util.LinearLayoutManagerFix;
|
||||||
|
import org.meowcat.edxposed.manager.util.ModuleUtil;
|
||||||
|
import org.meowcat.edxposed.manager.util.RepoLoader;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import me.zhanghai.android.fastscroll.FastScrollerBuilder;
|
||||||
|
|
||||||
|
public class DownloadActivity extends BaseActivity implements RepoLoader.RepoListener, ModuleUtil.ModuleListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
private DownloadsAdapter adapter;
|
||||||
|
private String filterText;
|
||||||
|
private RepoLoader repoLoader;
|
||||||
|
private ModuleUtil moduleUtil;
|
||||||
|
private int sortingOrder;
|
||||||
|
private SearchView searchView;
|
||||||
|
private SharedPreferences ignoredUpdatesPref;
|
||||||
|
private boolean changed = false;
|
||||||
|
private final BroadcastReceiver connectionListener = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (repoLoader != null) {
|
||||||
|
repoLoader.triggerReload(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private ActivityDownloadBinding binding;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
binding = ActivityDownloadBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
setSupportActionBar(binding.toolbar);
|
||||||
|
binding.toolbar.setNavigationOnClickListener(view -> finish());
|
||||||
|
ActionBar bar = getSupportActionBar();
|
||||||
|
if (bar != null) {
|
||||||
|
bar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupWindowInsets(binding.snackbar, binding.recyclerView);
|
||||||
|
repoLoader = RepoLoader.getInstance();
|
||||||
|
moduleUtil = ModuleUtil.getInstance();
|
||||||
|
adapter = new DownloadsAdapter(this, RepoDb.queryModuleOverview(sortingOrder, filterText));
|
||||||
|
/*adapter.setFilterQueryProvider(new FilterQueryProvider() {
|
||||||
|
@Override
|
||||||
|
public Cursor runQuery(CharSequence constraint) {
|
||||||
|
return RepoDb.queryModuleOverview(sortingOrder, constraint);
|
||||||
|
}
|
||||||
|
});*/
|
||||||
|
|
||||||
|
sortingOrder = App.getPreferences().getInt("download_sorting_order", RepoDb.SORT_STATUS);
|
||||||
|
|
||||||
|
ignoredUpdatesPref = getSharedPreferences("update_ignored", MODE_PRIVATE);
|
||||||
|
binding.recyclerView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
|
||||||
|
binding.swipeRefreshLayout.setOnRefreshListener(() -> {
|
||||||
|
repoLoader.setSwipeRefreshLayout(binding.swipeRefreshLayout);
|
||||||
|
repoLoader.triggerReload(true);
|
||||||
|
});
|
||||||
|
repoLoader.addListener(this, true);
|
||||||
|
moduleUtil.addListener(this);
|
||||||
|
binding.recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerView.setLayoutManager(new LinearLayoutManagerFix(this));
|
||||||
|
StickyRecyclerHeadersDecoration headersDecor = new StickyRecyclerHeadersDecoration(adapter);
|
||||||
|
binding.recyclerView.addItemDecoration(headersDecor);
|
||||||
|
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||||
|
@Override
|
||||||
|
public void onChanged() {
|
||||||
|
headersDecor.invalidateHeaders();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
FastScrollerBuilder fastScrollerBuilder = new FastScrollerBuilder(binding.recyclerView);
|
||||||
|
if (!App.getPreferences().getBoolean("md2", false)) {
|
||||||
|
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this,
|
||||||
|
DividerItemDecoration.VERTICAL);
|
||||||
|
binding.recyclerView.addItemDecoration(dividerItemDecoration);
|
||||||
|
} else {
|
||||||
|
fastScrollerBuilder.useMd2Style();
|
||||||
|
}
|
||||||
|
fastScrollerBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
ignoredUpdatesPref.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();
|
||||||
|
|
||||||
|
repoLoader.removeListener(this);
|
||||||
|
moduleUtil.removeListener(this);
|
||||||
|
ignoredUpdatesPref.unregisterOnSharedPreferenceChangeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.menu_download, menu);
|
||||||
|
|
||||||
|
// Setup search button
|
||||||
|
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
|
||||||
|
searchView.setIconifiedByDefault(true);
|
||||||
|
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextSubmit(String query) {
|
||||||
|
setFilter(query);
|
||||||
|
searchView.clearFocus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextChange(String newText) {
|
||||||
|
setFilter(newText);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFilter(String filterText) {
|
||||||
|
this.filterText = filterText;
|
||||||
|
reloadItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadItems() {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
adapter.changeCursor(RepoDb.queryModuleOverview(sortingOrder, filterText));
|
||||||
|
TransitionManager.beginDelayedTransition(binding.recyclerView);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
|
if (item.getItemId() == R.id.menu_sort) {
|
||||||
|
new MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(R.string.download_sorting_title)
|
||||||
|
.setSingleChoiceItems(R.array.download_sort_order, sortingOrder, (dialog, which) -> {
|
||||||
|
sortingOrder = which;
|
||||||
|
App.getPreferences().edit().putInt("download_sorting_order", sortingOrder).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 (searchView.isIconified()) {
|
||||||
|
super.onBackPressed();
|
||||||
|
} else {
|
||||||
|
searchView.setIconified(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DownloadsAdapter extends CursorRecyclerViewAdapter<DownloadsAdapter.ViewHolder> implements StickyRecyclerHeadersAdapter<DownloadsAdapter.HeaderViewHolder> {
|
||||||
|
private final Context context;
|
||||||
|
private final DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.SHORT);
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
private final String[] sectionHeaders;
|
||||||
|
|
||||||
|
DownloadsAdapter(Context context, Cursor cursor) {
|
||||||
|
super(cursor);
|
||||||
|
this.context = context;
|
||||||
|
prefs = context.getSharedPreferences("update_ignored", MODE_PRIVATE);
|
||||||
|
|
||||||
|
Resources res = context.getResources();
|
||||||
|
sectionHeaders = 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),
|
||||||
|
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 = prefs.getBoolean(cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.PKGNAME), false);
|
||||||
|
boolean updateIgnorePreference = App.getPreferences().getBoolean("ignore_updates", false);
|
||||||
|
boolean hasUpdate = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.HAS_UPDATE) > 0;
|
||||||
|
|
||||||
|
if (hasUpdate && updateIgnored && updateIgnorePreference) {
|
||||||
|
hasUpdate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortingOrder != RepoDb.SORT_STATUS) {
|
||||||
|
long timestamp = (sortingOrder == RepoDb.SORT_UPDATED) ? updated : created;
|
||||||
|
long age = System.currentTimeMillis() - timestamp;
|
||||||
|
final long mSecsPerDay = 24 * 60 * 60 * 1000L;
|
||||||
|
if (age < mSecsPerDay)
|
||||||
|
return 4;
|
||||||
|
if (age < 7 * mSecsPerDay)
|
||||||
|
return 5;
|
||||||
|
if (age < 30 * mSecsPerDay)
|
||||||
|
return 6;
|
||||||
|
return 7;
|
||||||
|
} else {
|
||||||
|
if (isFramework)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (hasUpdate)
|
||||||
|
return 1;
|
||||||
|
else if (isInstalled)
|
||||||
|
return 2;
|
||||||
|
else
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.sticky_header_download, parent, false);
|
||||||
|
return new HeaderViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
|
||||||
|
long section = getHeaderId(position);
|
||||||
|
viewHolder.title.setText(sectionHeaders[(int) section]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
ItemDownloadBinding binding = ItemDownloadBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
return new ViewHolder(binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 = prefs.getBoolean(cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.PKGNAME), false);
|
||||||
|
boolean updateIgnorePreference = App.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(context.getString(
|
||||||
|
R.string.download_status_update_available,
|
||||||
|
installedVersion, latestVersion));
|
||||||
|
txtStatus.setTextColor(ContextCompat.getColor(DownloadActivity.this, R.color.download_status_update_available));
|
||||||
|
txtStatus.setVisibility(View.VISIBLE);
|
||||||
|
} else if (isInstalled) {
|
||||||
|
txtStatus.setText(context.getString(
|
||||||
|
R.string.download_status_installed, installedVersion));
|
||||||
|
txtStatus.setTextColor(ContextCompat.getColor(DownloadActivity.this, R.color.warning));
|
||||||
|
txtStatus.setTextColor(getThemedColor(android.R.attr.textColorHighlight));
|
||||||
|
txtStatus.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
txtStatus.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
String creationDate = dateFormatter.format(new Date(created));
|
||||||
|
String updateDate = dateFormatter.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(ItemDownloadBinding binding) {
|
||||||
|
super(binding.getRoot());
|
||||||
|
appName = binding.title;
|
||||||
|
appDescription = binding.description;
|
||||||
|
downloadStatus = binding.downloadStatus;
|
||||||
|
timestamps = binding.timestamps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView title;
|
||||||
|
|
||||||
|
HeaderViewHolder(View view) {
|
||||||
|
super(view);
|
||||||
|
title = view.findViewById(android.R.id.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,274 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
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 androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityDownloadDetailsBinding;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityDownloadDetailsNotFoundBinding;
|
||||||
|
import org.meowcat.edxposed.manager.repo.Module;
|
||||||
|
import org.meowcat.edxposed.manager.ui.fragment.DownloadDetailsFragment;
|
||||||
|
import org.meowcat.edxposed.manager.ui.fragment.DownloadDetailsSettingsFragment;
|
||||||
|
import org.meowcat.edxposed.manager.ui.fragment.DownloadDetailsVersionsFragment;
|
||||||
|
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 RepoLoader repoLoader = RepoLoader.getInstance();
|
||||||
|
private static final ModuleUtil moduleUtil = ModuleUtil.getInstance();
|
||||||
|
private String packageName;
|
||||||
|
private Module module;
|
||||||
|
private ModuleUtil.InstalledModule installedModule;
|
||||||
|
private ActivityDownloadDetailsBinding binding;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
||||||
|
packageName = getModulePackageName();
|
||||||
|
try {
|
||||||
|
module = repoLoader.getModule(packageName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.i(App.TAG, "DownloadDetailsActivity -> " + e.getMessage());
|
||||||
|
|
||||||
|
module = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
installedModule = ModuleUtil.getInstance().getModule(packageName);
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
repoLoader.addListener(this, false);
|
||||||
|
moduleUtil.addListener(this);
|
||||||
|
|
||||||
|
if (module != null) {
|
||||||
|
binding = ActivityDownloadDetailsBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar);
|
||||||
|
binding.toolbar.setNavigationOnClickListener(view -> finish());
|
||||||
|
|
||||||
|
ActionBar ab = getSupportActionBar();
|
||||||
|
|
||||||
|
if (ab != null) {
|
||||||
|
ab.setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTabs();
|
||||||
|
|
||||||
|
boolean directDownload = getIntent().getBooleanExtra("direct_download", false);
|
||||||
|
// Updates available => start on the versions page
|
||||||
|
if (installedModule != null && installedModule.isUpdate(repoLoader.getLatestVersion(module)) || directDownload)
|
||||||
|
binding.downloadPager.setCurrentItem(DOWNLOAD_VERSIONS);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ActivityDownloadDetailsNotFoundBinding binding = ActivityDownloadDetailsNotFoundBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
binding.message.setText(getResources().getString(R.string.download_details_not_found, packageName));
|
||||||
|
|
||||||
|
binding.reload.setOnClickListener(v -> {
|
||||||
|
v.setEnabled(false);
|
||||||
|
repoLoader.triggerReload(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setupWindowInsets(binding.snackbar, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupTabs() {
|
||||||
|
binding.downloadPager.setAdapter(new SwipeFragmentPagerAdapter(getSupportFragmentManager()));
|
||||||
|
binding.slidingTabs.setupWithViewPager(binding.downloadPager);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
repoLoader.removeListener(this);
|
||||||
|
moduleUtil.removeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Module getModule() {
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModuleUtil.InstalledModule getInstalledModule() {
|
||||||
|
return installedModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void gotoPage(int page) {
|
||||||
|
binding.downloadPager.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 (this.packageName.equals(packageName))
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
MenuInflater inflater = getMenuInflater();
|
||||||
|
inflater.inflate(R.menu.menu_download_details, menu);
|
||||||
|
|
||||||
|
boolean updateIgnorePreference = App.getPreferences().getBoolean("ignore_updates", false);
|
||||||
|
if (updateIgnorePreference) {
|
||||||
|
SharedPreferences prefs = getSharedPreferences("update_ignored", MODE_PRIVATE);
|
||||||
|
|
||||||
|
boolean ignored = prefs.getBoolean(module.packageName, false);
|
||||||
|
menu.findItem(R.id.ignoreUpdate).setChecked(ignored);
|
||||||
|
} else {
|
||||||
|
menu.removeItem(R.id.ignoreUpdate);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
|
int itemId = item.getItemId();
|
||||||
|
if (itemId == R.id.menu_refresh) {
|
||||||
|
RepoLoader.getInstance().triggerReload(true);
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_share) {
|
||||||
|
String text = module.name + " - ";
|
||||||
|
|
||||||
|
if (isPackageInstalled(packageName, this)) {
|
||||||
|
String s = getPackageManager().getInstallerPackageName(packageName);
|
||||||
|
boolean playStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
playStore = PLAY_STORE_PACKAGE.equals(s);
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
playStore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playStore) {
|
||||||
|
text += String.format(PLAY_STORE_LINK, packageName);
|
||||||
|
} else {
|
||||||
|
text += String.format(XPOSED_REPO_LINK, packageName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text += String.format(XPOSED_REPO_LINK,
|
||||||
|
packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
} else if (itemId == R.id.ignoreUpdate) {
|
||||||
|
SharedPreferences prefs = getSharedPreferences("update_ignored", MODE_PRIVATE);
|
||||||
|
|
||||||
|
boolean ignored = prefs.getBoolean(module.packageName, false);
|
||||||
|
prefs.edit().putBoolean(module.packageName, !ignored).apply();
|
||||||
|
item.setChecked(!ignored);
|
||||||
|
}
|
||||||
|
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 final 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, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,170 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
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 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.transition.TransitionManager;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.BuildConfig;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityEdDownloadBinding;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.DialogInstallWarningBinding;
|
||||||
|
import org.meowcat.edxposed.manager.ui.fragment.BaseAdvancedInstaller;
|
||||||
|
import org.meowcat.edxposed.manager.ui.fragment.StatusInstallerFragment;
|
||||||
|
import org.meowcat.edxposed.manager.util.json.JSONUtils;
|
||||||
|
import org.meowcat.edxposed.manager.util.json.XposedTab;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class EdDownloadActivity extends BaseActivity {
|
||||||
|
ActivityEdDownloadBinding binding;
|
||||||
|
private TabsAdapter tabsAdapter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
binding = ActivityEdDownloadBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
setSupportActionBar(binding.toolbar);
|
||||||
|
binding.toolbar.setNavigationOnClickListener(view -> finish());
|
||||||
|
ActionBar bar = getSupportActionBar();
|
||||||
|
if (bar != null) {
|
||||||
|
bar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
tabsAdapter = new TabsAdapter(getSupportFragmentManager());
|
||||||
|
tabsAdapter.notifyDataSetChanged();
|
||||||
|
binding.pager.setAdapter(tabsAdapter);
|
||||||
|
binding.tabLayout.setupWithViewPager(binding.pager);
|
||||||
|
new JSONParser().execute();
|
||||||
|
|
||||||
|
if (!App.getPreferences().getBoolean("hide_install_warning", false)) {
|
||||||
|
DialogInstallWarningBinding binding = DialogInstallWarningBinding.inflate(getLayoutInflater());
|
||||||
|
new MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(R.string.install_warning_title)
|
||||||
|
.setMessage(R.string.install_warning)
|
||||||
|
.setView(binding.getRoot())
|
||||||
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||||
|
if (binding.checkbox.isChecked())
|
||||||
|
App.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);
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private class JSONParser extends AsyncTask<Void, Void, String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String doInBackground(Void... params) {
|
||||||
|
try {
|
||||||
|
return JSONUtils.getFileContent(JSONUtils.JSON_LINK);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Log.e(App.TAG, "AdvancedInstallerFragment -> " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String result) {
|
||||||
|
super.onPostExecute(result);
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final JSONUtils.XposedJson xposedJson = new Gson().fromJson(result, JSONUtils.XposedJson.class);
|
||||||
|
|
||||||
|
TransitionManager.beginDelayedTransition(binding.tabLayout);
|
||||||
|
for (XposedTab tab : xposedJson.tabs) {
|
||||||
|
if (tab.installers.size() > 0 && tab.sdks.contains(Build.VERSION.SDK_INT)) {
|
||||||
|
tabsAdapter.addFragment(tab.name, BaseAdvancedInstaller.newInstance(tab));
|
||||||
|
tabsAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String newApkVersion = xposedJson.apk.version;
|
||||||
|
String newApkLink = xposedJson.apk.link;
|
||||||
|
String newApkChangelog = xposedJson.apk.changelog;
|
||||||
|
|
||||||
|
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,336 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
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.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
import com.google.android.material.tabs.TabLayout;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.BuildConfig;
|
||||||
|
import org.meowcat.edxposed.manager.Constants;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityLogsBinding;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.DialogInstallWarningBinding;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ItemLogBinding;
|
||||||
|
import org.meowcat.edxposed.manager.util.LinearLayoutManagerFix;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
public class LogsActivity extends BaseActivity {
|
||||||
|
private boolean allLog = false;
|
||||||
|
private final File fileErrorLog = new File(Constants.getBaseDir() + "log/error.log");
|
||||||
|
private final File fileErrorLogOld = new File(
|
||||||
|
Constants.getBaseDir() + "log/error.log.old");
|
||||||
|
private final File fileAllLog = new File(Constants.getBaseDir() + "log/all.log");
|
||||||
|
private final File fileAllLogOld = new File(Constants.getBaseDir() + "log/all.log.old");
|
||||||
|
private LogsAdapter adapter;
|
||||||
|
private final Handler handler = new Handler();
|
||||||
|
private ActivityLogsBinding binding;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
binding = ActivityLogsBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
setSupportActionBar(binding.toolbar);
|
||||||
|
binding.toolbar.setNavigationOnClickListener(view -> finish());
|
||||||
|
ActionBar bar = getSupportActionBar();
|
||||||
|
if (bar != null) {
|
||||||
|
bar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
setupWindowInsets(binding.snackbar, binding.recyclerView);
|
||||||
|
|
||||||
|
if (!App.getPreferences().getBoolean("hide_logcat_warning", false)) {
|
||||||
|
DialogInstallWarningBinding binding = DialogInstallWarningBinding.inflate(getLayoutInflater());
|
||||||
|
new MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(R.string.install_warning_title)
|
||||||
|
.setMessage(R.string.not_logcat)
|
||||||
|
.setView(binding.getRoot())
|
||||||
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||||
|
if (binding.checkbox.isChecked())
|
||||||
|
App.getPreferences().edit().putBoolean("hide_logcat_warning", true).apply();
|
||||||
|
})
|
||||||
|
.setCancelable(false)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
adapter = new LogsAdapter();
|
||||||
|
binding.recyclerView.setAdapter(adapter);
|
||||||
|
binding.recyclerView.setLayoutManager(new LinearLayoutManagerFix(this));
|
||||||
|
if (App.getPreferences().getBoolean("disable_verbose_log", false)) {
|
||||||
|
binding.slidingTabs.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
binding.slidingTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||||
|
@Override
|
||||||
|
public void onTabSelected(TabLayout.Tab tab) {
|
||||||
|
allLog = tab.getPosition() != 0;
|
||||||
|
reloadErrorLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTabUnselected(TabLayout.Tab tab) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTabReselected(TabLayout.Tab tab) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
int itemId = item.getItemId();
|
||||||
|
if (itemId == R.id.menu_scroll_top) {
|
||||||
|
scrollTop();
|
||||||
|
} else if (itemId == R.id.menu_scroll_down) {
|
||||||
|
scrollDown();
|
||||||
|
} else if (itemId == R.id.menu_refresh) {
|
||||||
|
reloadErrorLog();
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_send) {
|
||||||
|
try {
|
||||||
|
send();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_save) {
|
||||||
|
save();
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_clear) {
|
||||||
|
clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scrollTop() {
|
||||||
|
binding.recyclerView.smoothScrollToPosition(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scrollDown() {
|
||||||
|
binding.recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadErrorLog() {
|
||||||
|
new LogsReader().execute(allLog ? fileAllLog : fileErrorLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clear() {
|
||||||
|
try {
|
||||||
|
new FileOutputStream(allLog ? fileAllLog : fileErrorLog).close();
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
(allLog ? fileAllLogOld : fileErrorLogOld).delete();
|
||||||
|
adapter.setEmpty();
|
||||||
|
Snackbar.make(binding.snackbar, R.string.logs_cleared, Snackbar.LENGTH_SHORT).show();
|
||||||
|
reloadErrorLog();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Snackbar.make(binding.snackbar, getResources().getString(R.string.logs_clear_failed) + "n" + e.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send() {
|
||||||
|
Uri uri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", allLog ? fileAllLog : fileErrorLog);
|
||||||
|
Intent sendIntent = new Intent();
|
||||||
|
sendIntent.setAction(Intent.ACTION_SEND);
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||||
|
sendIntent.setDataAndType(uri, "text/plain");
|
||||||
|
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
startActivity(Intent.createChooser(sendIntent, getResources().getString(R.string.menuSend)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
private void save() {
|
||||||
|
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));
|
||||||
|
|
||||||
|
Intent exportIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||||
|
exportIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
exportIntent.setType("text/*");
|
||||||
|
exportIntent.putExtra(Intent.EXTRA_TITLE, filename);
|
||||||
|
startActivityForResult(exportIntent, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (resultCode != RESULT_OK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (requestCode == 42) {
|
||||||
|
if (data != null) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null) {
|
||||||
|
try {
|
||||||
|
OutputStream os = getContentResolver().openOutputStream(uri);
|
||||||
|
if (os != null) {
|
||||||
|
FileInputStream in = new FileInputStream(allLog ? fileAllLog : fileErrorLog);
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
while ((len = in.read(buffer)) > 0) {
|
||||||
|
os.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
os.close();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Snackbar.make(binding.snackbar, getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private class LogsReader extends AsyncTask<File, Integer, ArrayList<String>> {
|
||||||
|
private AlertDialog mProgressDialog;
|
||||||
|
private final Runnable mRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mProgressDialog.show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
mProgressDialog = new MaterialAlertDialogBuilder(LogsActivity.this).create();
|
||||||
|
mProgressDialog.setMessage(getString(R.string.loading));
|
||||||
|
mProgressDialog.setCancelable(false);
|
||||||
|
handler.postDelayed(mRunnable, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ArrayList<String> doInBackground(File... log) {
|
||||||
|
Thread.currentThread().setPriority(Thread.NORM_PRIORITY + 2);
|
||||||
|
|
||||||
|
ArrayList<String> logs = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
File logfile = log[0];
|
||||||
|
try (Scanner scanner = new Scanner(logfile)) {
|
||||||
|
while (scanner.hasNextLine()) {
|
||||||
|
logs.add(scanner.nextLine());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return logs;
|
||||||
|
} catch (IOException e) {
|
||||||
|
logs.add(LogsActivity.this.getResources().getString(R.string.logs_cannot_read));
|
||||||
|
if (e.getMessage() != null) {
|
||||||
|
logs.addAll(Arrays.asList(e.getMessage().split("\n")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(ArrayList<String> logs) {
|
||||||
|
if (logs.size() == 0) {
|
||||||
|
adapter.setEmpty();
|
||||||
|
} else {
|
||||||
|
adapter.setLogs(logs);
|
||||||
|
}
|
||||||
|
handler.removeCallbacks(mRunnable);//It loaded so fast that no need to show progress
|
||||||
|
if (mProgressDialog.isShowing()) {
|
||||||
|
mProgressDialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LogsAdapter extends RecyclerView.Adapter<LogsAdapter.ViewHolder> {
|
||||||
|
ArrayList<String> logs = new ArrayList<>();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public LogsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
ItemLogBinding binding = ItemLogBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
return new LogsAdapter.ViewHolder(binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull LogsAdapter.ViewHolder holder, int position) {
|
||||||
|
TextView view = holder.textView;
|
||||||
|
view.setText(logs.get(position));
|
||||||
|
view.measure(0, 0);
|
||||||
|
int desiredWidth = view.getMeasuredWidth();
|
||||||
|
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
|
||||||
|
layoutParams.width = desiredWidth;
|
||||||
|
if (binding.recyclerView.getWidth() < desiredWidth) {
|
||||||
|
binding.recyclerView.requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void setLogs(ArrayList<String> logs) {
|
||||||
|
this.logs.clear();
|
||||||
|
this.logs.addAll(logs);
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEmpty() {
|
||||||
|
logs.clear();
|
||||||
|
logs.add(getString(R.string.log_is_empty));
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return logs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView textView;
|
||||||
|
|
||||||
|
ViewHolder(ItemLogBinding binding) {
|
||||||
|
super(binding.getRoot());
|
||||||
|
textView = binding.log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.appcompat.widget.PopupMenu;
|
||||||
|
import androidx.appcompat.widget.TooltipCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.Constants;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.adapters.AppHelper;
|
||||||
|
import org.meowcat.edxposed.manager.adapters.BlackListAdapter;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityMainBinding;
|
||||||
|
import org.meowcat.edxposed.manager.util.GlideHelper;
|
||||||
|
import org.meowcat.edxposed.manager.util.ModuleUtil;
|
||||||
|
import org.meowcat.edxposed.manager.util.RepoLoader;
|
||||||
|
import org.meowcat.edxposed.manager.util.light.Light;
|
||||||
|
|
||||||
|
public class MainActivity extends BaseActivity implements RepoLoader.RepoListener, ModuleUtil.ModuleListener {
|
||||||
|
ActivityMainBinding binding;
|
||||||
|
private RepoLoader repoLoader;
|
||||||
|
|
||||||
|
@SuppressLint("PrivateResource")
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
getWindow().getDecorView().post(() -> {
|
||||||
|
if (Light.setLightSourceAlpha(getWindow().getDecorView(), 0.01f, 0.029f)) {
|
||||||
|
binding.status.setElevation(24);
|
||||||
|
binding.modules.setElevation(12);
|
||||||
|
binding.downloads.setElevation(12);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setupWindowInsets(binding.snackbar, null);
|
||||||
|
repoLoader = RepoLoader.getInstance();
|
||||||
|
ModuleUtil.getInstance().addListener(this);
|
||||||
|
repoLoader.addListener(this, false);
|
||||||
|
binding.modules.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(getApplicationContext(), ModulesActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
binding.downloads.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(getApplicationContext(), DownloadActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
binding.apps.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(getApplicationContext(), BlackListActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
binding.status.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(getApplicationContext(), EdDownloadActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
binding.settings.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(getApplicationContext(), SettingsActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
binding.logs.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(getApplicationContext(), LogsActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
binding.about.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(getApplicationContext(), AboutActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
TooltipCompat.setTooltipText(binding.menuMore, getString(androidx.appcompat.R.string.abc_action_menu_overflow_description));
|
||||||
|
binding.menuMore.setOnClickListener(v -> {
|
||||||
|
PopupMenu appMenu = new PopupMenu(MainActivity.this, binding.menuMore);
|
||||||
|
appMenu.inflate(R.menu.menu_installer);
|
||||||
|
appMenu.setOnMenuItemClickListener(this::onOptionsItemSelected);
|
||||||
|
appMenu.show();
|
||||||
|
});
|
||||||
|
Glide.with(binding.appIcon)
|
||||||
|
.load(GlideHelper.wrapApplicationInfoForIconLoader(getApplicationInfo()))
|
||||||
|
.into(binding.appIcon);
|
||||||
|
String installedXposedVersion = Constants.getXposedVersion();
|
||||||
|
if (installedXposedVersion != null) {
|
||||||
|
if (Constants.getXposedApiVersion() != -1) {
|
||||||
|
binding.statusTitle.setText(R.string.Activated);
|
||||||
|
binding.statusSummary.setText(installedXposedVersion + " (" + Constants.getXposedVariant() + ")");
|
||||||
|
binding.status.setCardBackgroundColor(ContextCompat.getColor(this, R.color.download_status_update_available));
|
||||||
|
binding.statusIcon.setImageResource(R.drawable.ic_check_circle);
|
||||||
|
} else {
|
||||||
|
binding.statusTitle.setText(R.string.Inactivate);
|
||||||
|
binding.statusSummary.setText(R.string.installed_lollipop_inactive);
|
||||||
|
binding.status.setCardBackgroundColor(ContextCompat.getColor(this, R.color.amber_500));
|
||||||
|
binding.statusIcon.setImageResource(R.drawable.ic_warning);
|
||||||
|
}
|
||||||
|
} else if (Constants.getXposedApiVersion() > 0) {
|
||||||
|
binding.statusTitle.setText(R.string.Activated);
|
||||||
|
binding.statusSummary.setText(getString(R.string.version_x, Constants.getXposedApiVersion()));
|
||||||
|
binding.status.setCardBackgroundColor(ContextCompat.getColor(this, R.color.download_status_update_available));
|
||||||
|
binding.statusIcon.setImageResource(R.drawable.ic_check_circle);
|
||||||
|
} else {
|
||||||
|
binding.statusTitle.setText(R.string.Install);
|
||||||
|
binding.statusSummary.setText(R.string.InstallDetail);
|
||||||
|
binding.status.setCardBackgroundColor(ContextCompat.getColor(this, R.color.colorPrimary));
|
||||||
|
binding.statusIcon.setImageResource(R.drawable.ic_error);
|
||||||
|
}
|
||||||
|
notifyDataSetChanged();
|
||||||
|
new Thread(() -> new BlackListAdapter(getApplicationContext(), AppHelper.isWhiteListMode()).generateCheckedList());
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyDataSetChanged() {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
String frameworkUpdateVersion = repoLoader.getFrameworkUpdateVersion();
|
||||||
|
boolean moduleUpdateAvailable = repoLoader.hasModuleUpdates();
|
||||||
|
ModuleUtil.getInstance().getEnabledModules().size();
|
||||||
|
binding.modulesSummary.setText(String.format(getString(R.string.ModulesDetail), ModuleUtil.getInstance().getEnabledModules().size()));
|
||||||
|
if (frameworkUpdateVersion != null) {
|
||||||
|
binding.statusSummary.setText(String.format(getString(R.string.welcome_framework_update_available), frameworkUpdateVersion));
|
||||||
|
}
|
||||||
|
if (moduleUpdateAvailable) {
|
||||||
|
binding.downloadSummary.setText(R.string.modules_updates_available);
|
||||||
|
} else {
|
||||||
|
binding.downloadSummary.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);
|
||||||
|
repoLoader.removeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
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 org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.adapters.AppAdapter;
|
||||||
|
import org.meowcat.edxposed.manager.adapters.AppHelper;
|
||||||
|
import org.meowcat.edxposed.manager.adapters.ScopeAdapter;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityScopeListBinding;
|
||||||
|
import org.meowcat.edxposed.manager.util.LinearLayoutManagerFix;
|
||||||
|
|
||||||
|
import me.zhanghai.android.fastscroll.FastScrollerBuilder;
|
||||||
|
|
||||||
|
public class ModuleScopeActivity extends BaseActivity implements AppAdapter.Callback {
|
||||||
|
private SearchView searchView;
|
||||||
|
private ScopeAdapter appAdapter;
|
||||||
|
|
||||||
|
private SearchView.OnQueryTextListener searchListener;
|
||||||
|
private ActivityScopeListBinding binding;
|
||||||
|
private final Runnable runnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
binding.swipeRefreshLayout.setRefreshing(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private final Handler handler = new Handler();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
String modulePackageName = getIntent().getStringExtra("modulePackageName");
|
||||||
|
String moduleName = getIntent().getStringExtra("moduleName");
|
||||||
|
binding = ActivityScopeListBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
setSupportActionBar(binding.toolbar);
|
||||||
|
binding.toolbar.setNavigationOnClickListener(view -> finish());
|
||||||
|
ActionBar bar = getSupportActionBar();
|
||||||
|
if (bar != null) {
|
||||||
|
bar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
bar.setSubtitle(moduleName);
|
||||||
|
}
|
||||||
|
setupWindowInsets(binding.snackbar, binding.recyclerView);
|
||||||
|
appAdapter = new ScopeAdapter(this, modulePackageName, binding.masterSwitch);
|
||||||
|
appAdapter.setHasStableIds(true);
|
||||||
|
binding.recyclerView.setAdapter(appAdapter);
|
||||||
|
binding.recyclerView.setLayoutManager(new LinearLayoutManagerFix(this));
|
||||||
|
FastScrollerBuilder fastScrollerBuilder = new FastScrollerBuilder(binding.recyclerView);
|
||||||
|
if (!App.getPreferences().getBoolean("md2", false)) {
|
||||||
|
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this,
|
||||||
|
DividerItemDecoration.VERTICAL);
|
||||||
|
binding.recyclerView.addItemDecoration(dividerItemDecoration);
|
||||||
|
} else {
|
||||||
|
fastScrollerBuilder.useMd2Style();
|
||||||
|
}
|
||||||
|
fastScrollerBuilder.build();
|
||||||
|
appAdapter.setCallback(this);
|
||||||
|
handler.postDelayed(runnable, 300);
|
||||||
|
binding.swipeRefreshLayout.setOnRefreshListener(() -> appAdapter.refresh());
|
||||||
|
|
||||||
|
searchListener = new SearchView.OnQueryTextListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextSubmit(String query) {
|
||||||
|
appAdapter.filter(query);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextChange(String newText) {
|
||||||
|
appAdapter.filter(newText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.menu_app_list, menu);
|
||||||
|
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
|
||||||
|
searchView.setOnQueryTextListener(searchListener);
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDataReady() {
|
||||||
|
handler.removeCallbacks(runnable);
|
||||||
|
binding.swipeRefreshLayout.setRefreshing(false);
|
||||||
|
String queryStr = searchView != null ? searchView.getQuery().toString() : "";
|
||||||
|
runOnUiThread(() -> appAdapter.getFilter().filter(queryStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemClick(View v, ApplicationInfo info) {
|
||||||
|
AppHelper.showMenu(this, getSupportFragmentManager(), v, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (searchView.isIconified()) {
|
||||||
|
super.onBackPressed();
|
||||||
|
} else {
|
||||||
|
searchView.setIconified(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,690 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
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.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.widget.SearchView;
|
||||||
|
import androidx.appcompat.widget.SwitchCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.BuildConfig;
|
||||||
|
import org.meowcat.edxposed.manager.Constants;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityModulesBinding;
|
||||||
|
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.GlideApp;
|
||||||
|
import org.meowcat.edxposed.manager.util.InstallApkUtil;
|
||||||
|
import org.meowcat.edxposed.manager.util.LinearLayoutManagerFix;
|
||||||
|
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.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
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;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import me.zhanghai.android.fastscroll.FastScrollerBuilder;
|
||||||
|
|
||||||
|
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";
|
||||||
|
ActivityModulesBinding binding;
|
||||||
|
private int installedXposedVersion;
|
||||||
|
private ApplicationFilter filter;
|
||||||
|
private SearchView searchView;
|
||||||
|
private ApplicationInfo.DisplayNameComparator displayNameComparator;
|
||||||
|
private SearchView.OnQueryTextListener mSearchListener;
|
||||||
|
private PackageManager pm;
|
||||||
|
private Comparator<ApplicationInfo> cmp;
|
||||||
|
private final DateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||||
|
private ModuleUtil moduleUtil;
|
||||||
|
private ModuleAdapter adapter = null;
|
||||||
|
private final Runnable reloadModules = new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
String queryStr = searchView != null ? searchView.getQuery().toString() : "";
|
||||||
|
ArrayList<ModuleUtil.InstalledModule> showList;
|
||||||
|
ArrayList<ModuleUtil.InstalledModule> fullList = new ArrayList<>(moduleUtil.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, pm), filter)
|
||||||
|
|| lowercaseContains(info.packageName, filter)) {
|
||||||
|
showList.add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (App.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;
|
||||||
|
}
|
||||||
|
fullList.sort((a, b) -> {
|
||||||
|
boolean aChecked = moduleUtil.isModuleEnabled(a.packageName);
|
||||||
|
boolean bChecked = moduleUtil.isModuleEnabled(b.packageName);
|
||||||
|
if (aChecked == bChecked) {
|
||||||
|
return cmp.compare(a.app, b.app);
|
||||||
|
} else if (aChecked) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
adapter.addAll(showList);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
moduleUtil.updateModulesList(false);
|
||||||
|
binding.swipeRefreshLayout.setRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private String selectedPackageName;
|
||||||
|
|
||||||
|
private void filter(String constraint) {
|
||||||
|
filter.filter(constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
binding = ActivityModulesBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
setSupportActionBar(binding.toolbar);
|
||||||
|
binding.toolbar.setNavigationOnClickListener(view -> finish());
|
||||||
|
ActionBar bar = getSupportActionBar();
|
||||||
|
if (bar != null) {
|
||||||
|
bar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
setupWindowInsets(binding.snackbar, binding.recyclerView);
|
||||||
|
filter = new ApplicationFilter();
|
||||||
|
moduleUtil = ModuleUtil.getInstance();
|
||||||
|
pm = getPackageManager();
|
||||||
|
displayNameComparator = new ApplicationInfo.DisplayNameComparator(pm);
|
||||||
|
cmp = displayNameComparator;
|
||||||
|
installedXposedVersion = Constants.getXposedApiVersion();
|
||||||
|
if (installedXposedVersion <= 0) {
|
||||||
|
Snackbar.make(binding.snackbar, R.string.xposed_not_active, Snackbar.LENGTH_LONG).setAction(R.string.Settings, v -> {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(ModulesActivity.this, SettingsActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
}).show();
|
||||||
|
}
|
||||||
|
adapter = new ModuleAdapter();
|
||||||
|
adapter.setHasStableIds(true);
|
||||||
|
moduleUtil.addListener(this);
|
||||||
|
binding.recyclerView.setAdapter(adapter);
|
||||||
|
binding.recyclerView.setLayoutManager(new LinearLayoutManagerFix(this));
|
||||||
|
FastScrollerBuilder fastScrollerBuilder = new FastScrollerBuilder(binding.recyclerView);
|
||||||
|
if (!App.getPreferences().getBoolean("md2", false)) {
|
||||||
|
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this,
|
||||||
|
DividerItemDecoration.VERTICAL);
|
||||||
|
binding.recyclerView.addItemDecoration(dividerItemDecoration);
|
||||||
|
} else {
|
||||||
|
fastScrollerBuilder.useMd2Style();
|
||||||
|
}
|
||||||
|
fastScrollerBuilder.build();
|
||||||
|
binding.swipeRefreshLayout.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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.menu_modules, menu);
|
||||||
|
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
|
||||||
|
searchView.setOnQueryTextListener(mSearchListener);
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (resultCode != RESULT_OK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (requestCode == 42) {
|
||||||
|
File listModules = new File(Constants.getEnabledModulesListFile());
|
||||||
|
if (data != null) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null) {
|
||||||
|
try {
|
||||||
|
OutputStream os = getContentResolver().openOutputStream(uri);
|
||||||
|
if (os != null) {
|
||||||
|
FileInputStream in = new FileInputStream(listModules);
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
while ((len = in.read(buffer)) > 0) {
|
||||||
|
os.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
os.close();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Snackbar.make(binding.snackbar, getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (requestCode == 43) {
|
||||||
|
if (data != null) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null) {
|
||||||
|
try {
|
||||||
|
OutputStream os = getContentResolver().openOutputStream(uri);
|
||||||
|
if (os != null) {
|
||||||
|
PrintWriter fileOut = new PrintWriter(os);
|
||||||
|
|
||||||
|
Set<String> keys = ModuleUtil.getInstance().getModules().keySet();
|
||||||
|
for (String key1 : keys) {
|
||||||
|
fileOut.println(key1);
|
||||||
|
}
|
||||||
|
fileOut.close();
|
||||||
|
os.close();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Snackbar.make(binding.snackbar, getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (requestCode == 44) {
|
||||||
|
if (data != null) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null) {
|
||||||
|
try {
|
||||||
|
importModules(uri);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
|
Intent intent;
|
||||||
|
int itemId = item.getItemId();
|
||||||
|
if (itemId == R.id.export_enabled_modules) {
|
||||||
|
if (ModuleUtil.getInstance().getEnabledModules().isEmpty()) {
|
||||||
|
Snackbar.make(binding.snackbar, R.string.no_enabled_modules, Snackbar.LENGTH_SHORT).show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("text/*");
|
||||||
|
intent.putExtra(Intent.EXTRA_TITLE, "enabled_modules.list");
|
||||||
|
startActivityForResult(intent, 42);
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.export_installed_modules) {
|
||||||
|
Map<String, ModuleUtil.InstalledModule> installedModules = ModuleUtil.getInstance().getModules();
|
||||||
|
|
||||||
|
if (installedModules.isEmpty()) {
|
||||||
|
Snackbar.make(binding.snackbar, R.string.no_installed_modules, Snackbar.LENGTH_SHORT).show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("text/*");
|
||||||
|
intent.putExtra(Intent.EXTRA_TITLE, "installed_modules.list");
|
||||||
|
startActivityForResult(intent, 43);
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.import_installed_modules || itemId == R.id.import_enabled_modules) {
|
||||||
|
intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("*/*");
|
||||||
|
startActivityForResult(intent, 44);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importModules(Uri uri) {
|
||||||
|
RepoLoader repoLoader = RepoLoader.getInstance();
|
||||||
|
List<Module> list = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
InputStream inputStream = getContentResolver().openInputStream(uri);
|
||||||
|
InputStreamReader isr = new InputStreamReader(inputStream);
|
||||||
|
BufferedReader br = new BufferedReader(isr);
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
Module m = repoLoader.getModule(line);
|
||||||
|
|
||||||
|
if (m == null) {
|
||||||
|
Snackbar.make(binding.snackbar, getString(R.string.download_details_not_found, line), Snackbar.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
list.add(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
br.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Module m : list) {
|
||||||
|
if (moduleUtil.getModule(m.packageName) != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
NavUtil.startURL(this, mv.downloadLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ModuleUtil.getInstance().reloadInstalledModules();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
moduleUtil.removeListener(this);
|
||||||
|
binding.recyclerView.setAdapter(null);
|
||||||
|
adapter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, ModuleUtil.InstalledModule module) {
|
||||||
|
moduleUtil.updateModulesList(false);
|
||||||
|
runOnUiThread(reloadModules);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInstalledModulesReloaded(ModuleUtil moduleUtil) {
|
||||||
|
moduleUtil.updateModulesList(false);
|
||||||
|
runOnUiThread(reloadModules);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onContextItemSelected(@NonNull MenuItem item) {
|
||||||
|
ModuleUtil.InstalledModule module = ModuleUtil.getInstance().getModule(selectedPackageName);
|
||||||
|
if (module == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int itemId = item.getItemId();
|
||||||
|
if (itemId == R.id.menu_launch) {
|
||||||
|
String packageName = module.packageName;
|
||||||
|
if (packageName == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Intent intent = getSettingsIntent(packageName);
|
||||||
|
if (intent != null) {
|
||||||
|
startActivity(intent);
|
||||||
|
} else {
|
||||||
|
Snackbar.make(binding.snackbar, R.string.module_no_ui, Snackbar.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_download_updates) {
|
||||||
|
Intent intent = new Intent(this, DownloadDetailsActivity.class);
|
||||||
|
intent.setData(Uri.fromParts("package", module.packageName, null));
|
||||||
|
startActivity(intent);
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_support) {
|
||||||
|
NavUtil.startURL(this, Uri.parse(RepoDb.getModuleSupport(module.packageName)));
|
||||||
|
return true;
|
||||||
|
} else if (itemId == 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 e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_app_info) {
|
||||||
|
startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", module.packageName, null)));
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_uninstall) {
|
||||||
|
startActivity(new Intent(Intent.ACTION_UNINSTALL_PACKAGE, Uri.fromParts("package", module.packageName, null)));
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_scope) {
|
||||||
|
if (App.supportScope()) {
|
||||||
|
Intent intent = new Intent(this, ModuleScopeActivity.class);
|
||||||
|
intent.putExtra("modulePackageName", module.packageName);
|
||||||
|
intent.putExtra("moduleName", module.getAppName());
|
||||||
|
startActivity(intent);
|
||||||
|
} else {
|
||||||
|
new MaterialAlertDialogBuilder(this)
|
||||||
|
.setMessage(R.string.scope_not_supported)
|
||||||
|
.setPositiveButton(R.string.download_view_download, (dialog, which) -> {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(this, EdDownloadActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onContextItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean lowercaseContains(String s, CharSequence filter) {
|
||||||
|
return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (searchView.isIconified()) {
|
||||||
|
super.onBackPressed();
|
||||||
|
} else {
|
||||||
|
searchView.setIconified(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ModuleAdapter extends RecyclerView.Adapter<ModuleAdapter.ViewHolder> {
|
||||||
|
ArrayList<ModuleUtil.InstalledModule> items = new ArrayList<>();
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
ModuleUtil.InstalledModule item = items.get(position);
|
||||||
|
holder.itemView.setOnClickListener(v -> {
|
||||||
|
String packageName = item.packageName;
|
||||||
|
if (packageName == null || packageName.equals(BuildConfig.APPLICATION_ID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Intent launchIntent = getSettingsIntent(packageName);
|
||||||
|
if (launchIntent != null) {
|
||||||
|
startActivity(launchIntent);
|
||||||
|
} else {
|
||||||
|
Snackbar.make(findViewById(R.id.snackbar), R.string.module_no_ui, Snackbar.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.itemView.setOnLongClickListener(v -> {
|
||||||
|
selectedPackageName = item.packageName;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
|
||||||
|
getMenuInflater().inflate(R.menu.context_menu_modules, menu);
|
||||||
|
ModuleUtil.InstalledModule installedModule = ModuleUtil.getInstance().getModule(item.packageName);
|
||||||
|
if (installedModule == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String support = RepoDb.getModuleSupport(installedModule.packageName);
|
||||||
|
if (NavUtil.parseURL(support) == null) {
|
||||||
|
menu.removeItem(R.id.menu_support);
|
||||||
|
}
|
||||||
|
} catch (RepoDb.RowNotFoundException e) {
|
||||||
|
menu.removeItem(R.id.menu_download_updates);
|
||||||
|
menu.removeItem(R.id.menu_support);
|
||||||
|
}
|
||||||
|
if (installedModule.packageName.equals(BuildConfig.APPLICATION_ID)) {
|
||||||
|
menu.removeItem(R.id.menu_launch);
|
||||||
|
menu.removeItem(R.id.menu_scope);
|
||||||
|
menu.removeItem(R.id.menu_uninstall);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
holder.appName.setText(item.getAppName());
|
||||||
|
|
||||||
|
TextView version = holder.appVersion;
|
||||||
|
version.setText(Objects.requireNonNull(item).versionName);
|
||||||
|
version.setSelected(true);
|
||||||
|
|
||||||
|
TextView packageTv = holder.appPackage;
|
||||||
|
packageTv.setText(item.packageName);
|
||||||
|
|
||||||
|
String creationDate = dateformat.format(new Date(item.installTime));
|
||||||
|
String updateDate = dateformat.format(new Date(item.updateTime));
|
||||||
|
holder.timestamps.setText(getString(R.string.install_timestamps, creationDate, updateDate));
|
||||||
|
|
||||||
|
GlideApp.with(holder.appIcon)
|
||||||
|
.load(item.getPackageInfo())
|
||||||
|
.into(holder.appIcon);
|
||||||
|
|
||||||
|
TextView descriptionText = holder.appDescription;
|
||||||
|
descriptionText.setVisibility(View.VISIBLE);
|
||||||
|
if (!item.getDescription().isEmpty()) {
|
||||||
|
descriptionText.setText(item.getDescription());
|
||||||
|
} else {
|
||||||
|
descriptionText.setText(getString(R.string.module_empty_description));
|
||||||
|
descriptionText.setTextColor(ContextCompat.getColor(ModulesActivity.this, R.color.warning));
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchCompat mSwitch = holder.mSwitch;
|
||||||
|
mSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||||
|
String packageName = item.packageName;
|
||||||
|
boolean changed = moduleUtil.isModuleEnabled(packageName) ^ isChecked;
|
||||||
|
if (changed) {
|
||||||
|
moduleUtil.setModuleEnabled(packageName, isChecked);
|
||||||
|
moduleUtil.updateModulesList(true, binding);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mSwitch.setChecked(moduleUtil.isModuleEnabled(item.packageName));
|
||||||
|
TextView warningText = holder.warningText;
|
||||||
|
|
||||||
|
if (item.minVersion == 0) {
|
||||||
|
if (!App.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 (!App.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 (!App.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 (!App.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 (!App.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(ArrayList<ModuleUtil.InstalledModule> items) {
|
||||||
|
this.items = items;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
if (items != null) {
|
||||||
|
return items.size();
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return items.get(position).hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
ImageView appIcon;
|
||||||
|
TextView appName;
|
||||||
|
TextView appPackage;
|
||||||
|
TextView appDescription;
|
||||||
|
TextView appVersion;
|
||||||
|
TextView timestamps;
|
||||||
|
TextView warningText;
|
||||||
|
SwitchCompat mSwitch;
|
||||||
|
|
||||||
|
ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
appIcon = itemView.findViewById(R.id.app_icon);
|
||||||
|
appName = itemView.findViewById(R.id.app_name);
|
||||||
|
appDescription = itemView.findViewById(R.id.description);
|
||||||
|
appPackage = itemView.findViewById(R.id.package_name);
|
||||||
|
appVersion = itemView.findViewById(R.id.version_name);
|
||||||
|
timestamps = itemView.findViewById(R.id.timestamps);
|
||||||
|
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,507 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.activity;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.SwitchPreferenceCompat;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.takisoft.preferencex.PreferenceFragmentCompat;
|
||||||
|
import com.topjohnwu.superuser.Shell;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.Constants;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivitySettingsBinding;
|
||||||
|
import org.meowcat.edxposed.manager.ui.widget.IntegerListPreference;
|
||||||
|
import org.meowcat.edxposed.manager.util.RepoLoader;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class SettingsActivity extends BaseActivity {
|
||||||
|
private static final String KEY_PREFIX = SettingsActivity.class.getName() + '.';
|
||||||
|
private static final String EXTRA_SAVED_INSTANCE_STATE = KEY_PREFIX + "SAVED_INSTANCE_STATE";
|
||||||
|
ActivitySettingsBinding binding;
|
||||||
|
private boolean restarting;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static Intent newIntent(@NonNull Context context) {
|
||||||
|
return new Intent(context, SettingsActivity.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static Intent newIntent(@NonNull Bundle savedInstanceState, @NonNull Context context) {
|
||||||
|
return newIntent(context)
|
||||||
|
.putExtra(EXTRA_SAVED_INSTANCE_STATE, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
savedInstanceState = getIntent().getBundleExtra(EXTRA_SAVED_INSTANCE_STATE);
|
||||||
|
}
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
binding = ActivitySettingsBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
setSupportActionBar(binding.toolbar);
|
||||||
|
binding.toolbar.setNavigationOnClickListener(view -> finish());
|
||||||
|
ActionBar bar = getSupportActionBar();
|
||||||
|
if (bar != null) {
|
||||||
|
bar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
setupWindowInsets(binding.snackbar, null);
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
getSupportFragmentManager().beginTransaction()
|
||||||
|
.add(R.id.container, new SettingsFragment()).commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restart() {
|
||||||
|
Bundle savedInstanceState = new Bundle();
|
||||||
|
onSaveInstanceState(savedInstanceState);
|
||||||
|
finish();
|
||||||
|
startActivity(newIntent(savedInstanceState, this));
|
||||||
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||||
|
restarting = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
|
||||||
|
return restarting || super.dispatchKeyEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
@Override
|
||||||
|
public boolean dispatchKeyShortcutEvent(@NonNull KeyEvent event) {
|
||||||
|
return restarting || super.dispatchKeyShortcutEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchTouchEvent(@NonNull MotionEvent event) {
|
||||||
|
return restarting || super.dispatchTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchTrackballEvent(@NonNull MotionEvent event) {
|
||||||
|
return restarting || super.dispatchTrackballEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) {
|
||||||
|
return restarting || super.dispatchGenericMotionEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"ResultOfMethodCallIgnored"})
|
||||||
|
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
|
private static final File enableResourcesFlag = new File(Constants.getBaseDir() + "conf/enable_resources");
|
||||||
|
private static final File dynamicModulesFlag = new File(Constants.getBaseDir() + "conf/dynamicmodules");
|
||||||
|
private static final File deoptBootFlag = new File(Constants.getBaseDir() + "conf/deoptbootimage");
|
||||||
|
private static final File whiteListModeFlag = new File(Constants.getBaseDir() + "conf/usewhitelist");
|
||||||
|
private static final File blackWhiteListModeFlag = new File(Constants.getBaseDir() + "conf/blackwhitelist");
|
||||||
|
private static final File disableVerboseLogsFlag = new File(Constants.getBaseDir() + "conf/disable_verbose_log");
|
||||||
|
private static final File disableModulesLogsFlag = new File(Constants.getBaseDir() + "conf/disable_modules_log");
|
||||||
|
private static final File verboseLogProcessID = new File(Constants.getBaseDir() + "log/all.pid");
|
||||||
|
private static final File modulesLogProcessID = new File(Constants.getBaseDir() + "log/error.pid");
|
||||||
|
|
||||||
|
@SuppressLint({"ObsoleteSdkInt", "WorldReadableFiles"})
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
|
||||||
|
addPreferencesFromResource(R.xml.prefs);
|
||||||
|
|
||||||
|
Preference stopVerboseLog = findPreference("stop_verbose_log");
|
||||||
|
if (stopVerboseLog != null) {
|
||||||
|
stopVerboseLog.setOnPreferenceClickListener(preference -> {
|
||||||
|
areYouSure(R.string.stop_verbose_log_summary, (dialog, which) -> Shell.su("pkill -P $(cat " + verboseLogProcessID.getAbsolutePath() + ")").exec());
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Preference stopLog = findPreference("stop_log");
|
||||||
|
if (stopLog != null) {
|
||||||
|
stopLog.setOnPreferenceClickListener(preference -> {
|
||||||
|
areYouSure(R.string.stop_log_summary, (dialog, which) -> Shell.su("pkill -P $(cat " + modulesLogProcessID.getAbsolutePath() + ")").exec());
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Preference releaseType = findPreference("release_type_global");
|
||||||
|
if (releaseType != null) {
|
||||||
|
releaseType.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
RepoLoader.getInstance().setReleaseTypeGlobal((String) newValue);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPreferenceCompat prefWhiteListMode = findPreference("white_list_switch");
|
||||||
|
if (prefWhiteListMode != null) {
|
||||||
|
prefWhiteListMode.setChecked(whiteListModeFlag.exists());
|
||||||
|
prefWhiteListMode.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
boolean enabled = (Boolean) newValue;
|
||||||
|
if (enabled) {
|
||||||
|
FileOutputStream fos = null;
|
||||||
|
try {
|
||||||
|
fos = new FileOutputStream(whiteListModeFlag.getPath());
|
||||||
|
} 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 {
|
||||||
|
whiteListModeFlag.createNewFile();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
whiteListModeFlag.delete();
|
||||||
|
}
|
||||||
|
return (enabled == whiteListModeFlag.exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPreferenceCompat prefVerboseLogs = findPreference("disable_verbose_log");
|
||||||
|
if (prefVerboseLogs != null) {
|
||||||
|
prefVerboseLogs.setChecked(disableVerboseLogsFlag.exists());
|
||||||
|
prefVerboseLogs.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
boolean enabled = (Boolean) newValue;
|
||||||
|
if (enabled) {
|
||||||
|
FileOutputStream fos = null;
|
||||||
|
try {
|
||||||
|
fos = new FileOutputStream(disableVerboseLogsFlag.getPath());
|
||||||
|
} 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 {
|
||||||
|
disableVerboseLogsFlag.createNewFile();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disableVerboseLogsFlag.delete();
|
||||||
|
}
|
||||||
|
return (enabled == disableVerboseLogsFlag.exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPreferenceCompat prefModulesLogs = findPreference("disable_modules_log");
|
||||||
|
if (prefModulesLogs != null) {
|
||||||
|
prefModulesLogs.setChecked(disableModulesLogsFlag.exists());
|
||||||
|
prefModulesLogs.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
boolean enabled = (Boolean) newValue;
|
||||||
|
if (enabled) {
|
||||||
|
FileOutputStream fos = null;
|
||||||
|
try {
|
||||||
|
fos = new FileOutputStream(disableModulesLogsFlag.getPath());
|
||||||
|
} 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 {
|
||||||
|
disableModulesLogsFlag.createNewFile();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disableModulesLogsFlag.delete();
|
||||||
|
}
|
||||||
|
return (enabled == disableModulesLogsFlag.exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPreferenceCompat prefBlackWhiteListMode = findPreference("black_white_list_switch");
|
||||||
|
if (prefBlackWhiteListMode != null) {
|
||||||
|
prefBlackWhiteListMode.setChecked(blackWhiteListModeFlag.exists());
|
||||||
|
prefBlackWhiteListMode.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
boolean enabled = (Boolean) newValue;
|
||||||
|
if (enabled) {
|
||||||
|
FileOutputStream fos = null;
|
||||||
|
try {
|
||||||
|
fos = new FileOutputStream(blackWhiteListModeFlag.getPath());
|
||||||
|
} 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 {
|
||||||
|
blackWhiteListModeFlag.createNewFile();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blackWhiteListModeFlag.delete();
|
||||||
|
}
|
||||||
|
return (enabled == blackWhiteListModeFlag.exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPreferenceCompat prefEnableDeopt = findPreference("enable_boot_image_deopt");
|
||||||
|
if (prefEnableDeopt != null) {
|
||||||
|
prefEnableDeopt.setChecked(deoptBootFlag.exists());
|
||||||
|
prefEnableDeopt.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
boolean enabled = (Boolean) newValue;
|
||||||
|
if (enabled) {
|
||||||
|
FileOutputStream fos = null;
|
||||||
|
try {
|
||||||
|
fos = new FileOutputStream(deoptBootFlag.getPath());
|
||||||
|
} 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 {
|
||||||
|
deoptBootFlag.createNewFile();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deoptBootFlag.delete();
|
||||||
|
}
|
||||||
|
return (enabled == deoptBootFlag.exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPreferenceCompat prefDynamicResources = findPreference("is_dynamic_modules");
|
||||||
|
if (prefDynamicResources != null) {
|
||||||
|
prefDynamicResources.setChecked(dynamicModulesFlag.exists());
|
||||||
|
prefDynamicResources.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
boolean enabled = (Boolean) newValue;
|
||||||
|
if (enabled) {
|
||||||
|
FileOutputStream fos = null;
|
||||||
|
try {
|
||||||
|
fos = new FileOutputStream(dynamicModulesFlag.getPath());
|
||||||
|
} 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 {
|
||||||
|
dynamicModulesFlag.createNewFile();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dynamicModulesFlag.delete();
|
||||||
|
}
|
||||||
|
return (enabled == dynamicModulesFlag.exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPreferenceCompat prefDisableResources = findPreference("disable_resources");
|
||||||
|
if (prefDisableResources != null) {
|
||||||
|
prefDisableResources.setChecked(!enableResourcesFlag.exists());
|
||||||
|
prefDisableResources.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
boolean enabled = (Boolean) newValue;
|
||||||
|
if (!enabled) {
|
||||||
|
FileOutputStream fos = null;
|
||||||
|
try {
|
||||||
|
fos = new FileOutputStream(enableResourcesFlag.getPath());
|
||||||
|
} 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 {
|
||||||
|
enableResourcesFlag.createNewFile();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enableResourcesFlag.delete();
|
||||||
|
}
|
||||||
|
return (enabled != enableResourcesFlag.exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPreferenceCompat transparent = findPreference("transparent_status_bar");
|
||||||
|
if (transparent != null) {
|
||||||
|
transparent.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
boolean enabled = (Boolean) newValue;
|
||||||
|
SettingsActivity activity = (SettingsActivity) getActivity();
|
||||||
|
if (activity != null) {
|
||||||
|
if (enabled) {
|
||||||
|
activity.getWindow().setStatusBarColor(activity.getThemedColor(R.attr.colorActionBar));
|
||||||
|
} else {
|
||||||
|
activity.getWindow().setStatusBarColor(activity.getThemedColor(R.attr.colorPrimaryDark));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Preference compat_mode = findPreference("compat_mode");
|
||||||
|
if (compat_mode != null) {
|
||||||
|
compat_mode.setOnPreferenceClickListener(preference -> {
|
||||||
|
Activity activity = getActivity();
|
||||||
|
if (activity != null) {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.putExtra("compat_list", true);
|
||||||
|
intent.setClass(activity, BlackListActivity.class);
|
||||||
|
activity.startActivity(intent);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
IntegerListPreference theme = findPreference("theme");
|
||||||
|
if (theme != null) {
|
||||||
|
theme.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(Integer.parseInt((String) newValue));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPreferenceCompat black_dark_theme = findPreference("black_dark_theme");
|
||||||
|
if (black_dark_theme != null) {
|
||||||
|
black_dark_theme.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
SettingsActivity activity = (SettingsActivity) getActivity();
|
||||||
|
if (activity != null && isNightMode(getResources().getConfiguration())) {
|
||||||
|
activity.restart();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Preference primary_color = findPreference("primary_color");
|
||||||
|
if (primary_color != null) {
|
||||||
|
primary_color.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
SettingsActivity activity = (SettingsActivity) getActivity();
|
||||||
|
if (activity != null) {
|
||||||
|
activity.restart();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Preference accent_color = findPreference("accent_color");
|
||||||
|
if (accent_color != null) {
|
||||||
|
accent_color.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
SettingsActivity activity = (SettingsActivity) getActivity();
|
||||||
|
if (activity != null) {
|
||||||
|
activity.restart();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Preference colorized_action_bar = findPreference("colorized_action_bar");
|
||||||
|
if (colorized_action_bar != null) {
|
||||||
|
colorized_action_bar.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
SettingsActivity activity = (SettingsActivity) getActivity();
|
||||||
|
if (activity != null && !(isBlackNightTheme() && isNightMode(getResources().getConfiguration()))) {
|
||||||
|
activity.restart();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchPreferenceCompat md2 = findPreference("md2");
|
||||||
|
if (md2 != null) {
|
||||||
|
md2.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
SettingsActivity activity = (SettingsActivity) getActivity();
|
||||||
|
if (activity != null) {
|
||||||
|
updatePreference(!md2.isChecked());
|
||||||
|
activity.restart();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
updatePreference(!md2.isChecked());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void areYouSure(int contentTextId, DialogInterface.OnClickListener listener) {
|
||||||
|
Activity activity = getActivity();
|
||||||
|
if (activity != null) {
|
||||||
|
new MaterialAlertDialogBuilder(activity)
|
||||||
|
.setTitle(R.string.areyousure)
|
||||||
|
.setMessage(contentTextId)
|
||||||
|
.setPositiveButton(android.R.string.yes, listener)
|
||||||
|
.setNegativeButton(android.R.string.no, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePreference(boolean show) {
|
||||||
|
Preference black_dark_theme = findPreference("black_dark_theme");
|
||||||
|
if (black_dark_theme != null) {
|
||||||
|
black_dark_theme.setVisible(show);
|
||||||
|
}
|
||||||
|
Preference transparent_status_bar = findPreference("transparent_status_bar");
|
||||||
|
if (transparent_status_bar != null) {
|
||||||
|
transparent_status_bar.setVisible(show);
|
||||||
|
}
|
||||||
|
Preference colorized_action_bar = findPreference("colorized_action_bar");
|
||||||
|
if (colorized_action_bar != null) {
|
||||||
|
colorized_action_bar.setVisible(show);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
((LinearLayout) view).setClipToPadding(false);
|
||||||
|
((LinearLayout) view).setClipChildren(false);
|
||||||
|
((FrameLayout) getListView().getParent()).setClipChildren(false);
|
||||||
|
((FrameLayout) getListView().getParent()).setClipToPadding(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.fragment;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.widget.TooltipCompat;
|
||||||
|
import androidx.core.text.HtmlCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.SingleInstallerViewBinding;
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.BaseActivity;
|
||||||
|
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.Objects;
|
||||||
|
|
||||||
|
public class BaseAdvancedInstaller extends Fragment {
|
||||||
|
private SingleInstallerViewBinding binding;
|
||||||
|
|
||||||
|
public static BaseAdvancedInstaller newInstance(XposedTab tab) {
|
||||||
|
BaseAdvancedInstaller myFragment = new BaseAdvancedInstaller();
|
||||||
|
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putParcelable("tab", tab);
|
||||||
|
myFragment.setArguments(args);
|
||||||
|
|
||||||
|
return myFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
|
||||||
|
Bundle arguments = getArguments();
|
||||||
|
if (arguments == null) {
|
||||||
|
return null;
|
||||||
|
} else if (arguments.getParcelable("tab") == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
XposedTab tab = arguments.getParcelable("tab");
|
||||||
|
|
||||||
|
if (tab == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
binding = SingleInstallerViewBinding.inflate(inflater, container, false);
|
||||||
|
TooltipCompat.setTooltipText(binding.infoInstaller, getString(R.string.info));
|
||||||
|
TooltipCompat.setTooltipText(binding.infoUninstaller, getString(R.string.info));
|
||||||
|
try {
|
||||||
|
binding.chooserInstallers.setAdapter(new XposedZip.MyAdapter(getContext(), tab.installers));
|
||||||
|
binding.chooserUninstallers.setAdapter(new XposedZip.MyAdapter(getContext(), tab.uninstallers));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
binding.infoInstaller.setOnClickListener(v -> {
|
||||||
|
XposedZip selectedInstaller = (XposedZip) binding.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();
|
||||||
|
});
|
||||||
|
binding.infoUninstaller.setOnClickListener(v -> {
|
||||||
|
XposedZip selectedUninstaller = (XposedZip) binding.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();
|
||||||
|
});
|
||||||
|
|
||||||
|
binding.btnInstall.setOnClickListener(v -> warningArchitecture(
|
||||||
|
(dialog, which) -> {
|
||||||
|
XposedZip selectedInstaller = (XposedZip) binding.chooserInstallers.getSelectedItem();
|
||||||
|
Uri uri = Uri.parse(selectedInstaller.link);
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
||||||
|
startActivity(intent);
|
||||||
|
}, tab.description));
|
||||||
|
|
||||||
|
binding.btnUninstall.setOnClickListener(v -> warningArchitecture(
|
||||||
|
(dialog, which) -> {
|
||||||
|
XposedZip selectedUninstaller = (XposedZip) binding.chooserUninstallers.getSelectedItem();
|
||||||
|
Uri uri = Uri.parse(selectedUninstaller.link);
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
||||||
|
startActivity(intent);
|
||||||
|
}, tab.description));
|
||||||
|
|
||||||
|
binding.noticeTv.setText(HtmlCompat.fromHtml(tab.notice, HtmlCompat.FROM_HTML_MODE_LEGACY));
|
||||||
|
binding.author.setText(getString(R.string.download_author, tab.author));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (tab.uninstallers.size() == 0) {
|
||||||
|
binding.infoUninstaller.setVisibility(View.GONE);
|
||||||
|
binding.chooserUninstallers.setVisibility(View.GONE);
|
||||||
|
binding.btnUninstall.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tab.stable) {
|
||||||
|
binding.warningUnstable.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tab.official) {
|
||||||
|
binding.warningUnofficial.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.showOnXda.setOnClickListener(v -> NavUtil.startURL((BaseActivity) getActivity(), tab.support));
|
||||||
|
binding.updateDescription.setOnClickListener(v -> new MaterialAlertDialogBuilder(Objects.requireNonNull(getContext()))
|
||||||
|
.setTitle(R.string.changes)
|
||||||
|
.setMessage(HtmlCompat.fromHtml(tab.description, HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
|
.setPositiveButton(android.R.string.ok, null).show());
|
||||||
|
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void warningArchitecture(DialogInterface.OnClickListener listener, String description) {
|
||||||
|
Activity activity = getActivity();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(getString(R.string.warningArchitecture));
|
||||||
|
sb.append("\n\n");
|
||||||
|
sb.append(getString(R.string.changes));
|
||||||
|
sb.append("\n");
|
||||||
|
sb.append(HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_LEGACY));
|
||||||
|
if (activity != null) {
|
||||||
|
new MaterialAlertDialogBuilder(activity)
|
||||||
|
.setTitle(R.string.areyousure)
|
||||||
|
.setMessage(sb.toString())
|
||||||
|
.setPositiveButton(android.R.string.yes, listener)
|
||||||
|
.setNegativeButton(android.R.string.no, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.fragment;
|
||||||
|
|
||||||
|
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.text.TextUtils;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
|
||||||
|
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.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.FragmentCompileDialogBinding;
|
||||||
|
import org.meowcat.edxposed.manager.util.ToastUtil;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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);
|
||||||
|
FragmentCompileDialogBinding binding = FragmentCompileDialogBinding.inflate(LayoutInflater.from(requireContext()), null, false);
|
||||||
|
builder.setView(binding.getRoot());
|
||||||
|
binding.message.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);
|
||||||
|
}
|
||||||
|
// Also get STDERR
|
||||||
|
List<String> stdout = new ArrayList<>();
|
||||||
|
List<String> stderr = new ArrayList<>();
|
||||||
|
Shell.Result result = Shell.su(commands).to(stdout, stderr).exec();
|
||||||
|
if (stderr.size() > 0) {
|
||||||
|
return "Error: " + TextUtils.join("\n", stderr);
|
||||||
|
} else if (!result.isSuccess()) { // they might don't write to stderr
|
||||||
|
return "Error: " + TextUtils.join("\n", stdout);
|
||||||
|
} else {
|
||||||
|
return TextUtils.join("\n", stdout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String result) {
|
||||||
|
if (outerRef.get() == null || !outerRef.get().isAdded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Context ctx = outerRef.get().requireContext();
|
||||||
|
if (result.length() == 0) {
|
||||||
|
ToastUtil.showLongToast(ctx, R.string.compile_failed);
|
||||||
|
} else if (result.length() >= 5 && "Error".equals(result.substring(0, 5))) {
|
||||||
|
ToastUtil.showLongToast(ctx, ctx.getString(R.string.compile_failed_with_info) + " " + result.substring(6));
|
||||||
|
} else {
|
||||||
|
ToastUtil.showLongToast(ctx, R.string.done);
|
||||||
|
}
|
||||||
|
outerRef.get().dismissAllowingStateLoss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.fragment;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
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 androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.DownloadDetailsBinding;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.DownloadMoreinfoBinding;
|
||||||
|
import org.meowcat.edxposed.manager.repo.Module;
|
||||||
|
import org.meowcat.edxposed.manager.repo.RepoParser;
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.BaseActivity;
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.DownloadDetailsActivity;
|
||||||
|
import org.meowcat.edxposed.manager.util.NavUtil;
|
||||||
|
import org.meowcat.edxposed.manager.util.chrome.LinkTransformationMethod;
|
||||||
|
|
||||||
|
public class DownloadDetailsFragment extends Fragment {
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
DownloadDetailsActivity mActivity = (DownloadDetailsActivity) getActivity();
|
||||||
|
if (mActivity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Module module = mActivity.getModule();
|
||||||
|
if (module == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
DownloadDetailsBinding binding = DownloadDetailsBinding.inflate(inflater, container, false);
|
||||||
|
binding.downloadTitle.setText(module.name);
|
||||||
|
binding.downloadTitle.setTextIsSelectable(true);
|
||||||
|
|
||||||
|
if (module.author != null && !module.author.isEmpty())
|
||||||
|
binding.downloadAuthor.setText(getString(R.string.download_author, module.author));
|
||||||
|
else
|
||||||
|
binding.downloadAuthor.setText(R.string.download_unknown_author);
|
||||||
|
|
||||||
|
if (module.description != null) {
|
||||||
|
if (module.descriptionIsHtml) {
|
||||||
|
binding.downloadDescription.setText(RepoParser.parseSimpleHtml(getActivity(), module.description, binding.downloadDescription));
|
||||||
|
binding.downloadDescription.setTransformationMethod(new LinkTransformationMethod((BaseActivity) getActivity()));
|
||||||
|
binding.downloadDescription.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
} else {
|
||||||
|
binding.downloadDescription.setText(module.description);
|
||||||
|
}
|
||||||
|
binding.downloadDescription.setTextIsSelectable(true);
|
||||||
|
} else {
|
||||||
|
binding.downloadDescription.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Pair<String, String> moreInfoEntry : module.moreInfo) {
|
||||||
|
DownloadMoreinfoBinding moreinfoBinding = DownloadMoreinfoBinding.inflate(inflater, binding.downloadMoreinfoContainer, false);
|
||||||
|
|
||||||
|
moreinfoBinding.title.setText(moreInfoEntry.first + ":");
|
||||||
|
moreinfoBinding.message.setText(moreInfoEntry.second);
|
||||||
|
|
||||||
|
final Uri link = NavUtil.parseURL(moreInfoEntry.second);
|
||||||
|
if (link != null) {
|
||||||
|
moreinfoBinding.message.setTextColor(moreinfoBinding.message.getLinkTextColors());
|
||||||
|
moreinfoBinding.getRoot().setOnClickListener(v -> NavUtil.startURL((BaseActivity) getActivity(), link));
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.downloadMoreinfoContainer.addView(moreinfoBinding.getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.fragment;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.takisoft.preferencex.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.repo.Module;
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.DownloadDetailsActivity;
|
||||||
|
import org.meowcat.edxposed.manager.util.PrefixedSharedPreferences;
|
||||||
|
import org.meowcat.edxposed.manager.util.RepoLoader;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class DownloadDetailsSettingsFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
|
||||||
|
DownloadDetailsActivity mActivity = (DownloadDetailsActivity) getActivity();
|
||||||
|
if (mActivity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 (("global").equals(prefs.getString(k.getKey(), ""))) {
|
||||||
|
editor.putString(k.getKey(), "").apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.putBoolean("no_global", false).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
Preference releaseType = findPreference("release_type");
|
||||||
|
if (releaseType != null) {
|
||||||
|
releaseType.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
RepoLoader.getInstance().setReleaseTypeLocal(packageName, (String) newValue);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.fragment;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
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 androidx.annotation.NonNull;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.fragment.app.ListFragment;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
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.ui.activity.BaseActivity;
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.DownloadDetailsActivity;
|
||||||
|
import org.meowcat.edxposed.manager.ui.widget.DownloadView;
|
||||||
|
import org.meowcat.edxposed.manager.util.ModuleUtil.InstalledModule;
|
||||||
|
import org.meowcat.edxposed.manager.util.RepoLoader;
|
||||||
|
import org.meowcat.edxposed.manager.util.chrome.LinkTransformationMethod;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class DownloadDetailsVersionsFragment extends ListFragment {
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private DownloadDetailsActivity activity;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityCreated(Bundle savedInstanceState) {
|
||||||
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
activity = (DownloadDetailsActivity) getActivity();
|
||||||
|
if (activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Module module = activity.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(ContextCompat.getColor(activity, R.color.warning));
|
||||||
|
txtHeader.setOnClickListener(v -> activity.gotoPage(DownloadDetailsActivity.DOWNLOAD_SETTINGS));
|
||||||
|
getListView().addHeaderView(txtHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
VersionsAdapter sAdapter = new VersionsAdapter(activity, activity.getInstalledModule()/*, activity.findViewById(R.id.snackbar)*/);
|
||||||
|
for (ModuleVersion version : module.versions) {
|
||||||
|
if (repoLoader.isVersionShown(version))
|
||||||
|
sAdapter.add(version);
|
||||||
|
}
|
||||||
|
setListAdapter(sAdapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
setListAdapter(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder {
|
||||||
|
TextView txtStatus;
|
||||||
|
TextView txtVersion;
|
||||||
|
TextView txtRelType;
|
||||||
|
TextView txtUploadDate;
|
||||||
|
DownloadView downloadView;
|
||||||
|
TextView txtChangesTitle;
|
||||||
|
TextView txtChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class VersionsAdapter extends ArrayAdapter<ModuleVersion> {
|
||||||
|
private final DateFormat dateFormatter = DateFormat
|
||||||
|
.getDateInstance(DateFormat.SHORT);
|
||||||
|
private final int colorRelTypeStable;
|
||||||
|
private final int colorRelTypeOthers;
|
||||||
|
private final int colorInstalled;
|
||||||
|
private final int colorUpdateAvailable;
|
||||||
|
private final String textInstalled;
|
||||||
|
private final String textUpdateAvailable;
|
||||||
|
private final long installedVersionCode;
|
||||||
|
|
||||||
|
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);
|
||||||
|
colorRelTypeStable = color;
|
||||||
|
colorRelTypeOthers = ContextCompat.getColor(activity, R.color.warning);
|
||||||
|
colorInstalled = color;
|
||||||
|
colorUpdateAvailable = ContextCompat.getColor(activity, R.color.download_status_update_available);
|
||||||
|
textInstalled = getString(R.string.download_section_installed) + ":";
|
||||||
|
textUpdateAvailable = getString(R.string.download_section_update_available) + ":";
|
||||||
|
installedVersionCode = (installed != null) ? installed.versionCode : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public View getView(int position, View convertView, @NonNull 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);
|
||||||
|
if (item == null) {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
holder.txtVersion.setText(item.name);
|
||||||
|
holder.txtRelType.setText(item.relType.getTitleId());
|
||||||
|
holder.txtRelType.setTextColor(item.relType == ReleaseType.STABLE
|
||||||
|
? colorRelTypeStable : colorRelTypeOthers);
|
||||||
|
|
||||||
|
if (item.uploaded > 0) {
|
||||||
|
holder.txtUploadDate.setText(
|
||||||
|
dateFormatter.format(new Date(item.uploaded)));
|
||||||
|
holder.txtUploadDate.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
holder.txtUploadDate.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.code <= 0 || installedVersionCode <= 0
|
||||||
|
|| item.code < installedVersionCode) {
|
||||||
|
holder.txtStatus.setVisibility(View.GONE);
|
||||||
|
} else if (item.code == installedVersionCode) {
|
||||||
|
holder.txtStatus.setText(textInstalled);
|
||||||
|
holder.txtStatus.setTextColor(colorInstalled);
|
||||||
|
holder.txtStatus.setVisibility(View.VISIBLE);
|
||||||
|
} else { // item.code > installedVersionCode
|
||||||
|
holder.txtStatus.setText(textUpdateAvailable);
|
||||||
|
holder.txtStatus.setTextColor(colorUpdateAvailable);
|
||||||
|
holder.txtStatus.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.downloadView.setUrl(item.downloadLink);
|
||||||
|
holder.downloadView.setTitle(activity.getModule().name);
|
||||||
|
|
||||||
|
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((BaseActivity) 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,289 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.fragment;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.text.HtmlCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.BuildConfig;
|
||||||
|
import org.meowcat.edxposed.manager.Constants;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.StatusInstallerBinding;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
public class StatusInstallerFragment extends Fragment {
|
||||||
|
private static StatusInstallerBinding binding;
|
||||||
|
private static String updateLink;
|
||||||
|
|
||||||
|
public static void setUpdate(final String link, final String changelog, Context context) {
|
||||||
|
updateLink = link;
|
||||||
|
|
||||||
|
binding.updateView.setVisibility(View.VISIBLE);
|
||||||
|
binding.clickToUpdate.setVisibility(View.VISIBLE);
|
||||||
|
binding.clickToUpdate.setOnClickListener(v -> new MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(R.string.changes)
|
||||||
|
.setMessage(HtmlCompat.fromHtml(changelog, HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
|
.setPositiveButton(R.string.update, (dialog, which) -> update(context))
|
||||||
|
.setNegativeButton(R.string.later, null).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void update(Context context) {
|
||||||
|
Uri uri = Uri.parse(updateLink);
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
||||||
|
context.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() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("WorldReadableFiles")
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
binding = StatusInstallerBinding.inflate(inflater, container, false);
|
||||||
|
|
||||||
|
String installedXposedVersion = Constants.getXposedVersion();
|
||||||
|
String mAppVer = String.format("v%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE);
|
||||||
|
binding.manager.setText(mAppVer);
|
||||||
|
if (installedXposedVersion != null) {
|
||||||
|
binding.api.setText(Constants.getXposedApiVersion() + ".0");
|
||||||
|
binding.framework.setText(installedXposedVersion + " (" + Constants.getXposedVariant() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.androidVersion.setText(getString(R.string.android_sdk, getAndroidVersion(), Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
|
||||||
|
binding.manufacturer.setText(getUIFramework());
|
||||||
|
binding.cpu.setText(getCompleteArch());
|
||||||
|
|
||||||
|
determineVerifiedBootState(binding);
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void determineVerifiedBootState(StatusInstallerBinding binding) {
|
||||||
|
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 = false;
|
||||||
|
if (propSystemVerified != null) {
|
||||||
|
verified = !propSystemVerified.equals("0");
|
||||||
|
}
|
||||||
|
boolean detected = false;
|
||||||
|
if (propState != null) {
|
||||||
|
detected = !propState.isEmpty() || fileDmVerityModule.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verified) {
|
||||||
|
binding.dmverity.setText(R.string.verified_boot_active);
|
||||||
|
binding.dmverity.setTextColor(ContextCompat.getColor(requireActivity(), R.color.warning));
|
||||||
|
} else if (detected) {
|
||||||
|
binding.dmverity.setText(R.string.verified_boot_deactivated);
|
||||||
|
binding.dmverityExplanation.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
binding.dmverity.setText(R.string.verified_boot_none);
|
||||||
|
binding.dmverity.setTextColor(ContextCompat.getColor(requireActivity(), R.color.warning));
|
||||||
|
binding.dmverityExplanation.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(App.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(App.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(App.TAG, "Base directory: " + getPathWithCanonicalPath(baseDir, baseDirCanonical));
|
||||||
|
Log.e(App.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 "Ten";
|
||||||
|
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(App.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,55 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.widget;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.DownloadViewBinding;
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.BaseActivity;
|
||||||
|
import org.meowcat.edxposed.manager.util.NavUtil;
|
||||||
|
|
||||||
|
public class DownloadView extends LinearLayout {
|
||||||
|
public Fragment fragment;
|
||||||
|
private String mUrl = null;
|
||||||
|
private String mTitle = null;
|
||||||
|
private final DownloadViewBinding binding;
|
||||||
|
|
||||||
|
public DownloadView(Context context, final AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
setFocusable(false);
|
||||||
|
setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
binding = DownloadViewBinding.inflate(LayoutInflater.from(context), this);
|
||||||
|
|
||||||
|
binding.btnDownload.setOnClickListener(v -> NavUtil.startURL((BaseActivity) context, mUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return mUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
mUrl = url;
|
||||||
|
if (mUrl != null) {
|
||||||
|
binding.btnDownload.setVisibility(View.VISIBLE);
|
||||||
|
binding.txtInfo.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
binding.btnDownload.setVisibility(View.GONE);
|
||||||
|
binding.txtInfo.setVisibility(View.VISIBLE);
|
||||||
|
binding.txtInfo.setText(R.string.download_view_no_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return mTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.mTitle = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.widget;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import com.takisoft.preferencex.SimpleMenuPreference;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class IntegerListPreference extends SimpleMenuPreference {
|
||||||
|
public IntegerListPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerListPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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,112 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.widget;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.StateListDrawable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Checkable;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.widget.SwitchCompat;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
|
||||||
|
public class MasterSwitch extends FrameLayout implements View.OnClickListener, Checkable {
|
||||||
|
|
||||||
|
private TextView masterTitle;
|
||||||
|
private SwitchCompat switchCompat;
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
private OnCheckedChangeListener listener;
|
||||||
|
|
||||||
|
private boolean isChecked;
|
||||||
|
|
||||||
|
public MasterSwitch(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MasterSwitch(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MasterSwitch(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
init(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context, AttributeSet attrs) {
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||||
|
inflater.inflate(R.layout.master_switch, this, true);
|
||||||
|
|
||||||
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MasterSwitch);
|
||||||
|
int colorOn = a.getColor(R.styleable.MasterSwitch_masterSwitchBackgroundOn, 0);
|
||||||
|
int colorOff = a.getColor(R.styleable.MasterSwitch_masterSwitchBackgroundOff, 0);
|
||||||
|
a.recycle();
|
||||||
|
|
||||||
|
StateListDrawable drawable = new StateListDrawable();
|
||||||
|
drawable.addState(new int[]{android.R.attr.state_selected}, new ColorDrawable(colorOn));
|
||||||
|
drawable.addState(new int[]{}, new ColorDrawable(colorOff));
|
||||||
|
setBackground(drawable);
|
||||||
|
|
||||||
|
masterTitle = findViewById(android.R.id.title);
|
||||||
|
switchCompat = findViewById(R.id.switchWidget);
|
||||||
|
|
||||||
|
setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
masterTitle.setText(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateViews() {
|
||||||
|
if (switchCompat != null) {
|
||||||
|
setSelected(isChecked);
|
||||||
|
switchCompat.setChecked(isChecked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isChecked() {
|
||||||
|
return isChecked;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toggle() {
|
||||||
|
setChecked(!isChecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setChecked(boolean checked) {
|
||||||
|
final boolean changed = isChecked != checked;
|
||||||
|
if (changed) {
|
||||||
|
isChecked = checked;
|
||||||
|
updateViews();
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onCheckedChanged(checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnCheckedChangedListener(OnCheckedChangeListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class OnCheckedChangeListener {
|
||||||
|
public abstract void onCheckedChanged(boolean checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.widget;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.widget.EdgeEffect;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import me.zhanghai.android.fastscroll.FixItemDecorationRecyclerView;
|
||||||
|
|
||||||
|
public class RecyclerViewBugFixed extends FixItemDecorationRecyclerView {
|
||||||
|
|
||||||
|
|
||||||
|
public RecyclerViewBugFixed(@NonNull Context context) {
|
||||||
|
super(context);
|
||||||
|
setEdgeEffectFactory(getClipToPadding() ? new EdgeEffectFactory() : new AlwaysClipToPaddingEdgeEffectFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecyclerViewBugFixed(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
setEdgeEffectFactory(getClipToPadding() ? new EdgeEffectFactory() : new AlwaysClipToPaddingEdgeEffectFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecyclerViewBugFixed(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
setEdgeEffectFactory(getClipToPadding() ? new EdgeEffectFactory() : new AlwaysClipToPaddingEdgeEffectFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AlwaysClipToPaddingEdgeEffectFactory extends RecyclerView.EdgeEffectFactory {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected EdgeEffect createEdgeEffect(@NonNull RecyclerView view, int direction) {
|
||||||
|
return new EdgeEffect(view.getContext()) {
|
||||||
|
private boolean ensureSize = false;
|
||||||
|
|
||||||
|
private void ensureSize() {
|
||||||
|
if (ensureSize) return;
|
||||||
|
ensureSize = true;
|
||||||
|
switch (direction) {
|
||||||
|
case DIRECTION_LEFT:
|
||||||
|
case DIRECTION_RIGHT:
|
||||||
|
setSize(view.getMeasuredHeight() - view.getPaddingTop() - view.getPaddingBottom(),
|
||||||
|
view.getMeasuredWidth() - view.getPaddingLeft() - view.getPaddingRight());
|
||||||
|
break;
|
||||||
|
case DIRECTION_TOP:
|
||||||
|
case DIRECTION_BOTTOM:
|
||||||
|
setSize(view.getMeasuredWidth() - view.getPaddingLeft() - view.getPaddingRight(),
|
||||||
|
view.getMeasuredHeight() - view.getPaddingTop() - view.getPaddingBottom());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean draw(Canvas c) {
|
||||||
|
ensureSize();
|
||||||
|
|
||||||
|
int restore = c.save();
|
||||||
|
switch (direction) {
|
||||||
|
case DIRECTION_LEFT:
|
||||||
|
c.translate(view.getPaddingBottom(), 0f);
|
||||||
|
break;
|
||||||
|
case DIRECTION_TOP:
|
||||||
|
c.translate(view.getPaddingLeft(), view.getPaddingTop());
|
||||||
|
break;
|
||||||
|
case DIRECTION_RIGHT:
|
||||||
|
c.translate(-view.getPaddingTop(), 0f);
|
||||||
|
break;
|
||||||
|
case DIRECTION_BOTTOM:
|
||||||
|
c.translate(view.getPaddingRight(), view.getPaddingBottom());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
boolean res = super.draw(c);
|
||||||
|
c.restoreToCount(restore);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.meowcat.edxposed.manager.ui.widget;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.takisoft.preferencex.ColorPickerPreference;
|
||||||
|
import com.takisoft.preferencex.ColorPickerPreferenceDialogFragmentCompat;
|
||||||
|
import com.takisoft.preferencex.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.util.CustomThemeColor;
|
||||||
|
import org.meowcat.edxposed.manager.util.CustomThemeColors;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class ThemeColorPreference extends ColorPickerPreference {
|
||||||
|
|
||||||
|
static {
|
||||||
|
PreferenceFragmentCompat.registerPreferenceFragment(ThemeColorPreference.class,
|
||||||
|
ColorPickerPreferenceDialogFragmentCompat.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThemeColorPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThemeColorPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThemeColorPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThemeColorPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
String key = getKey();
|
||||||
|
Context context = getContext();
|
||||||
|
CustomThemeColor[] colors;
|
||||||
|
if (Objects.equals(key, "primary_color")) {
|
||||||
|
colors = CustomThemeColors.Primary.values();
|
||||||
|
} else if (Objects.equals(key, "accent_color")) {
|
||||||
|
colors = CustomThemeColors.Accent.values();
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unknown custom theme color preference key: " + key);
|
||||||
|
}
|
||||||
|
int[] mEntryValues = new int[colors.length];
|
||||||
|
for (int i = 0; i < colors.length; ++i) {
|
||||||
|
CustomThemeColor color = colors[i];
|
||||||
|
mEntryValues[i] = ContextCompat.getColor(context, color.getResourceId());
|
||||||
|
}
|
||||||
|
setColors(mEntryValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.R;
|
||||||
|
import org.meowcat.edxposed.manager.ui.fragment.CompileDialogFragment;
|
||||||
|
|
||||||
|
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,18 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||||
|
* All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public interface CustomThemeColor {
|
||||||
|
|
||||||
|
@ColorRes
|
||||||
|
int getResourceId();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
String getResourceEntryName();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
|
||||||
|
public class CustomThemeColors {
|
||||||
|
|
||||||
|
private CustomThemeColors() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Primary implements CustomThemeColor {
|
||||||
|
|
||||||
|
COLORPRIMARY(R.color.colorPrimary, "colorPrimary"),
|
||||||
|
MATERIAL_RED_500(R.color.material_red_500, "material_red_500"),
|
||||||
|
MATERIAL_PINK_500(R.color.material_pink_500, "material_pink_500"),
|
||||||
|
MATERIAL_PURPLE_500(R.color.material_purple_500, "material_purple_500"),
|
||||||
|
MATERIAL_DEEP_PURPLE_500(R.color.material_deep_purple_500, "material_deep_purple_500"),
|
||||||
|
MATERIAL_INDIGO_500(R.color.material_indigo_500, "material_indigo_500"),
|
||||||
|
MATERIAL_BLUE_500(R.color.material_blue_500, "material_blue_500"),
|
||||||
|
MATERIAL_LIGHT_BLUE_500(R.color.material_light_blue_500, "material_light_blue_500"),
|
||||||
|
MATERIAL_CYAN_500(R.color.material_cyan_500, "material_cyan_500"),
|
||||||
|
MATERIAL_TEAL_500(R.color.material_teal_500, "material_teal_500"),
|
||||||
|
MATERIAL_GREEN_500(R.color.material_green_500, "material_green_500"),
|
||||||
|
MATERIAL_LIGHT_GREEN_500(R.color.material_light_green_500, "material_light_green_500"),
|
||||||
|
MATERIAL_LIME_500(R.color.material_lime_500, "material_lime_500"),
|
||||||
|
MATERIAL_YELLOW_500(R.color.material_yellow_500, "material_yellow_500"),
|
||||||
|
MATERIAL_AMBER_500(R.color.material_amber_500, "material_amber_500"),
|
||||||
|
MATERIAL_ORANGE_500(R.color.material_orange_500, "material_orange_500"),
|
||||||
|
MATERIAL_DEEP_ORANGE_500(R.color.material_deep_orange_500, "material_deep_orange_500"),
|
||||||
|
MATERIAL_BROWN_500(R.color.material_brown_500, "material_brown_500"),
|
||||||
|
MATERIAL_GREY_500(R.color.material_grey_500, "material_grey_500"),
|
||||||
|
MATERIAL_BLUE_GREY_500(R.color.material_blue_grey_500, "material_blue_grey_500");
|
||||||
|
|
||||||
|
@ColorRes
|
||||||
|
private final int mResourceId;
|
||||||
|
@NonNull
|
||||||
|
private final String mResourceEntryName;
|
||||||
|
|
||||||
|
Primary(@ColorRes int resourceId, @NonNull String resourceEntryName) {
|
||||||
|
mResourceId = resourceId;
|
||||||
|
mResourceEntryName = resourceEntryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorRes
|
||||||
|
@Override
|
||||||
|
public int getResourceId() {
|
||||||
|
return mResourceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getResourceEntryName() {
|
||||||
|
return mResourceEntryName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Accent implements CustomThemeColor {
|
||||||
|
|
||||||
|
COLORACCENT(R.color.colorAccent, "colorAccent"),
|
||||||
|
MATERIAL_RED_A200(R.color.material_red_a200, "material_red_a200"),
|
||||||
|
MATERIAL_PINK_A200(R.color.material_pink_a200, "material_pink_a200"),
|
||||||
|
MATERIAL_PURPLE_A200(R.color.material_purple_a200, "material_purple_a200"),
|
||||||
|
MATERIAL_DEEP_PURPLE_A200(R.color.material_deep_purple_a200, "material_deep_purple_a200"),
|
||||||
|
MATERIAL_INDIGO_A200(R.color.material_indigo_a200, "material_indigo_a200"),
|
||||||
|
MATERIAL_BLUE_A200(R.color.material_blue_a200, "material_blue_a200"),
|
||||||
|
MATERIAL_LIGHT_BLUE_500(R.color.material_light_blue_500, "material_light_blue_500"),
|
||||||
|
MATERIAL_CYAN_500(R.color.material_cyan_500, "material_cyan_500"),
|
||||||
|
MATERIAL_TEAL_500(R.color.material_teal_500, "material_teal_500"),
|
||||||
|
MATERIAL_GREEN_500(R.color.material_green_500, "material_green_500"),
|
||||||
|
MATERIAL_LIGHT_GREEN_500(R.color.material_light_green_500, "material_light_green_500"),
|
||||||
|
MATERIAL_LIME_500(R.color.material_lime_500, "material_lime_500"),
|
||||||
|
MATERIAL_YELLOW_500(R.color.material_yellow_500, "material_yellow_500"),
|
||||||
|
MATERIAL_AMBER_500(R.color.material_amber_500, "material_amber_500"),
|
||||||
|
MATERIAL_ORANGE_500(R.color.material_orange_500, "material_orange_500"),
|
||||||
|
MATERIAL_DEEP_ORANGE_500(R.color.material_deep_orange_500, "material_deep_orange_500"),
|
||||||
|
MATERIAL_BROWN_500(R.color.material_brown_500, "material_brown_500"),
|
||||||
|
MATERIAL_GREY_500(R.color.material_grey_500, "material_grey_500"),
|
||||||
|
MATERIAL_BLUE_GREY_500(R.color.material_blue_grey_500, "material_blue_grey_500");
|
||||||
|
|
||||||
|
@ColorRes
|
||||||
|
private final int mResourceId;
|
||||||
|
@NonNull
|
||||||
|
private final String mResourceEntryName;
|
||||||
|
|
||||||
|
Accent(@ColorRes int resourceId, @NonNull String resourceEntryName) {
|
||||||
|
mResourceId = resourceId;
|
||||||
|
mResourceEntryName = resourceEntryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorRes
|
||||||
|
@Override
|
||||||
|
public int getResourceId() {
|
||||||
|
return mResourceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getResourceEntryName() {
|
||||||
|
return mResourceEntryName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class DownloadsUtil {
|
||||||
|
private static final SharedPreferences pref = App.getInstance().getSharedPreferences("download_cache", Context.MODE_PRIVATE);
|
||||||
|
|
||||||
|
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 = pref.getString("download_" + url + "_modified", null);
|
||||||
|
String etag = pref.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,
|
||||||
|
App.getInstance().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");
|
||||||
|
|
||||||
|
pref.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,
|
||||||
|
App.getInstance().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) {
|
||||||
|
pref.edit().remove("download_" + url + "_modified")
|
||||||
|
.remove("download_" + url + "_etag").apply();
|
||||||
|
} else {
|
||||||
|
pref.edit().clear().apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,12 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
|
||||||
|
public class GlideHelper {
|
||||||
|
public static PackageInfo wrapApplicationInfoForIconLoader(ApplicationInfo applicationInfo) {
|
||||||
|
PackageInfo packageInfo = new PackageInfo();
|
||||||
|
packageInfo.applicationInfo = applicationInfo;
|
||||||
|
return packageInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,27 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.Registry;
|
||||||
|
import com.bumptech.glide.annotation.GlideModule;
|
||||||
|
import com.bumptech.glide.module.AppGlideModule;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
|
||||||
|
import me.zhanghai.android.appiconloader.glide.AppIconModelLoader;
|
||||||
|
|
||||||
|
@GlideModule
|
||||||
|
public class IconLoader extends AppGlideModule {
|
||||||
|
@Override
|
||||||
|
public void registerComponents(Context context, @NonNull Glide glide, Registry registry) {
|
||||||
|
int iconSize = context.getResources().getDimensionPixelSize(R.dimen.app_icon_size);
|
||||||
|
registry.prepend(PackageInfo.class, Bitmap.class, new AppIconModelLoader.Factory(iconSize,
|
||||||
|
false, context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
|
||||||
|
public class InstallApkUtil {
|
||||||
|
|
||||||
|
public static String getAppLabel(ApplicationInfo info, PackageManager pm) {
|
||||||
|
try {
|
||||||
|
if (info.labelRes > 0) {
|
||||||
|
Resources res = pm.getResourcesForApplication(info);
|
||||||
|
return res.getString(info.labelRes);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return info.loadLabel(pm).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
public class LinearLayoutManagerFix extends LinearLayoutManager {
|
||||||
|
public LinearLayoutManagerFix(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinearLayoutManagerFix(Context context, int orientation, boolean reverseLayout) {
|
||||||
|
super(context, orientation, reverseLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinearLayoutManagerFix(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
|
||||||
|
try {
|
||||||
|
super.onLayoutChildren(recycler, state);
|
||||||
|
} catch (IndexOutOfBoundsException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,390 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
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.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.Constants;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.databinding.ActivityModulesBinding;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public final class ModuleUtil {
|
||||||
|
private static final String PLAY_STORE_PACKAGE = "com.android.vending";
|
||||||
|
// xposedminversion below this
|
||||||
|
public static int MIN_MODULE_VERSION = 2; // reject modules with
|
||||||
|
private static ModuleUtil instance = null;
|
||||||
|
private final PackageManager pm;
|
||||||
|
private final String frameworkPackageName;
|
||||||
|
private final List<ModuleListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
private final SharedPreferences pref;
|
||||||
|
//private InstalledModule framework = null;
|
||||||
|
private Map<String, InstalledModule> installedModules;
|
||||||
|
private boolean isReloading = false;
|
||||||
|
private Toast toast;
|
||||||
|
|
||||||
|
private ModuleUtil() {
|
||||||
|
pref = App.getInstance().getSharedPreferences("enabled_modules", Context.MODE_PRIVATE);
|
||||||
|
pm = App.getInstance().getPackageManager();
|
||||||
|
frameworkPackageName = App.getInstance().getPackageName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized ModuleUtil getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new ModuleUtil();
|
||||||
|
instance.reloadInstalledModules();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (isReloading)
|
||||||
|
return;
|
||||||
|
isReloading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, InstalledModule> modules = new HashMap<>();
|
||||||
|
RepoDb.beginTransation();
|
||||||
|
try {
|
||||||
|
RepoDb.deleteAllInstalledModules();
|
||||||
|
|
||||||
|
for (PackageInfo pkg : pm.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)) {
|
||||||
|
framework = installed = new InstalledModule(pkg, true);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
if (installed != null)
|
||||||
|
RepoDb.insertInstalledModule(installed);
|
||||||
|
}
|
||||||
|
|
||||||
|
RepoDb.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
RepoDb.endTransation();
|
||||||
|
}
|
||||||
|
|
||||||
|
installedModules = modules;
|
||||||
|
synchronized (this) {
|
||||||
|
isReloading = false;
|
||||||
|
}
|
||||||
|
for (ModuleListener listener : listeners) {
|
||||||
|
listener.onInstalledModulesReloaded(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstalledModule reloadSingleModule(String packageName) {
|
||||||
|
PackageInfo pkg;
|
||||||
|
try {
|
||||||
|
pkg = pm.getPackageInfo(packageName, PackageManager.GET_META_DATA);
|
||||||
|
} catch (NameNotFoundException e) {
|
||||||
|
RepoDb.deleteInstalledModule(packageName);
|
||||||
|
InstalledModule old = installedModules.remove(packageName);
|
||||||
|
if (old != null) {
|
||||||
|
for (ModuleListener listener : listeners) {
|
||||||
|
listener.onSingleInstalledModuleReloaded(instance, 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);
|
||||||
|
installedModules.put(packageName, module);
|
||||||
|
for (ModuleListener listener : listeners) {
|
||||||
|
listener.onSingleInstalledModuleReloaded(instance, packageName,
|
||||||
|
module);
|
||||||
|
}
|
||||||
|
return module;
|
||||||
|
} else {
|
||||||
|
RepoDb.deleteInstalledModule(packageName);
|
||||||
|
InstalledModule old = installedModules.remove(packageName);
|
||||||
|
if (old != null) {
|
||||||
|
for (ModuleListener listener : listeners) {
|
||||||
|
listener.onSingleInstalledModuleReloaded(instance, packageName, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isLoading() {
|
||||||
|
return isReloading;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* public InstalledModule getFramework() {
|
||||||
|
return framework;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
public String getFrameworkPackageName() {
|
||||||
|
return frameworkPackageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* private boolean isFramework(String packageName) {
|
||||||
|
return frameworkPackageName.equals(packageName);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// public boolean isInstalled(String packageName) {
|
||||||
|
// return installedModules.containsKey(packageName) || isFramework(packageName);
|
||||||
|
// }
|
||||||
|
|
||||||
|
public InstalledModule getModule(String packageName) {
|
||||||
|
return installedModules.get(packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, InstalledModule> getModules() {
|
||||||
|
return installedModules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModuleEnabled(String packageName, boolean enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
pref.edit().putInt(packageName, 1).apply();
|
||||||
|
} else {
|
||||||
|
pref.edit().remove(packageName).apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isModuleEnabled(String packageName) {
|
||||||
|
return pref.contains(packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<InstalledModule> getEnabledModules() {
|
||||||
|
LinkedList<InstalledModule> result = new LinkedList<>();
|
||||||
|
|
||||||
|
for (String packageName : pref.getAll().keySet()) {
|
||||||
|
InstalledModule module = getModule(packageName);
|
||||||
|
if (module != null)
|
||||||
|
result.add(module);
|
||||||
|
else
|
||||||
|
setModuleEnabled(packageName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void updateModulesList(boolean showToast) {
|
||||||
|
updateModulesList(showToast, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void updateModulesList(boolean showToast, ActivityModulesBinding binding) {
|
||||||
|
try {
|
||||||
|
Log.i(App.TAG, "ModuleUtil -> updating modules.list");
|
||||||
|
int installedXposedVersion = Constants.getXposedApiVersion();
|
||||||
|
if (!App.getPreferences().getBoolean("skip_xposedminversion_check", false) && installedXposedVersion <= 0 && showToast) {
|
||||||
|
if (binding != null) {
|
||||||
|
Snackbar.make(binding.snackbar, R.string.notinstalled, Snackbar.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
showToast(R.string.notinstalled);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintWriter modulesList = new PrintWriter(Constants.getModulesListFile());
|
||||||
|
PrintWriter enabledModulesList = new PrintWriter(Constants.getEnabledModulesListFile());
|
||||||
|
List<InstalledModule> enabledModules = getEnabledModules();
|
||||||
|
for (InstalledModule module : enabledModules) {
|
||||||
|
|
||||||
|
if (!App.getPreferences().getBoolean("skip_xposedminversion_check", false) && (module.minVersion > installedXposedVersion || module.minVersion < MIN_MODULE_VERSION) && showToast) {
|
||||||
|
if (binding != null) {
|
||||||
|
Snackbar.make(binding.snackbar, R.string.notinstalled, Snackbar.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
showToast(R.string.notinstalled);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
modulesList.println(module.app.sourceDir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String installer = pm.getInstallerPackageName(module.app.packageName);
|
||||||
|
if (!PLAY_STORE_PACKAGE.equals(installer))
|
||||||
|
enabledModulesList.println(module.app.packageName);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modulesList.close();
|
||||||
|
enabledModulesList.close();
|
||||||
|
|
||||||
|
if (showToast) {
|
||||||
|
if (binding != null) {
|
||||||
|
Snackbar.make(binding.snackbar, R.string.xposed_module_list_updated, Snackbar.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
showToast(R.string.xposed_module_list_updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(App.TAG, "ModuleUtil -> cannot write " + Constants.getModulesListFile(), e);
|
||||||
|
if (binding != null) {
|
||||||
|
Snackbar.make(binding.snackbar, "cannot write " + Constants.getModulesListFile() + e, Snackbar.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(App.getInstance(), "cannot write " + Constants.getModulesListFile() + e, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
private void showToast(int message) {
|
||||||
|
if (toast != null) {
|
||||||
|
toast.cancel();
|
||||||
|
toast = null;
|
||||||
|
}
|
||||||
|
toast = Toast.makeText(App.getInstance(), App.getInstance().getString(message), Toast.LENGTH_SHORT);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(ModuleListener listener) {
|
||||||
|
if (!listeners.contains(listener))
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(ModuleListener listener) {
|
||||||
|
listeners.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;
|
||||||
|
public PackageInfo pkg;
|
||||||
|
private String appName; // loaded lazyily
|
||||||
|
private String description; // loaded lazyily
|
||||||
|
|
||||||
|
private InstalledModule(PackageInfo pkg, boolean isFramework) {
|
||||||
|
this.app = pkg.applicationInfo;
|
||||||
|
this.pkg = pkg;
|
||||||
|
this.packageName = pkg.packageName;
|
||||||
|
this.isFramework = isFramework;
|
||||||
|
this.versionName = pkg.versionName;
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
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 = Constants.getXposedApiVersion();
|
||||||
|
if (version > 0 && App.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAppName() {
|
||||||
|
if (appName == null)
|
||||||
|
appName = app.loadLabel(pm).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 = pm.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 PackageInfo getPackageInfo() {
|
||||||
|
return pkg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getAppName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
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 com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.BaseActivity;
|
||||||
|
|
||||||
|
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(BaseActivity activity, Uri uri) {
|
||||||
|
CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder();
|
||||||
|
customTabsIntent.setShowTitle(true);
|
||||||
|
customTabsIntent.setToolbarColor(activity.getThemedColor(R.attr.colorActionBar));
|
||||||
|
customTabsIntent.build().launchUrl(activity, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startURL(BaseActivity activity, String url) {
|
||||||
|
startURL(activity, parseURL(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
public static void showMessage(final @NonNull Context context, final CharSequence message) {
|
||||||
|
App.runOnUiThread(() -> new MaterialAlertDialogBuilder(context)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
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.core.app.NotificationCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.topjohnwu.superuser.Shell;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.MainActivity;
|
||||||
|
|
||||||
|
public final class NotificationUtil {
|
||||||
|
|
||||||
|
public static final int NOTIFICATION_MODULE_NOT_ACTIVATED_YET = 0;
|
||||||
|
private static final int NOTIFICATION_MODULES_UPDATED = 1;
|
||||||
|
private static final int NOTIFICATION_INSTALLER_UPDATE = 2;
|
||||||
|
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 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";
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private static Context context = null;
|
||||||
|
private static NotificationManager notificationManager;
|
||||||
|
private static SharedPreferences prefs;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
if (context != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context = App.getInstance();
|
||||||
|
prefs = App.getPreferences();
|
||||||
|
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationChannel channelUpdate = new NotificationChannel(NOTIFICATION_UPDATE_CHANNEL, context.getString(R.string.download_section_update_available), NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
NotificationChannel channelModule = new NotificationChannel(NOTIFICATION_MODULES_CHANNEL, context.getString(R.string.nav_item_modules), NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
notificationManager.createNotificationChannel(channelUpdate);
|
||||||
|
notificationManager.createNotificationChannel(channelModule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void cancel(String tag, int id) {
|
||||||
|
notificationManager.cancel(tag, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void cancelAll() {
|
||||||
|
notificationManager.cancelAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showNotActivatedNotification(String packageName, String appName) {
|
||||||
|
Intent intent = new Intent(context, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtra(FRAGMENT_ID, 1);
|
||||||
|
PendingIntent pModulesTab = PendingIntent.getActivity(context, PENDING_INTENT_OPEN_MODULES, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
String title = context.getString(R.string.module_is_not_activated_yet);
|
||||||
|
NotificationCompat.Builder builder = getNotificationBuilder(title, appName, NOTIFICATION_MODULES_CHANNEL)
|
||||||
|
.setContentIntent(pModulesTab);
|
||||||
|
if (prefs.getBoolean(HEADS_UP, true)) {
|
||||||
|
builder.setPriority(2);
|
||||||
|
}
|
||||||
|
Intent iActivateAndReboot = new Intent(context, RebootReceiver.class);
|
||||||
|
iActivateAndReboot.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName);
|
||||||
|
PendingIntent pActivateAndReboot = PendingIntent.getBroadcast(context, PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT,
|
||||||
|
iActivateAndReboot, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
Intent iActivate = new Intent(context, RebootReceiver.class);
|
||||||
|
iActivate.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName);
|
||||||
|
iActivate.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE_AND_RETURN, true);
|
||||||
|
PendingIntent pActivate = PendingIntent.getBroadcast(context, PENDING_INTENT_ACTIVATE_MODULE,
|
||||||
|
iActivate, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle();
|
||||||
|
style.setBigContentTitle(title);
|
||||||
|
style.bigText(context.getString(R.string.module_is_not_activated_yet_detailed, appName));
|
||||||
|
builder.setStyle(style);
|
||||||
|
|
||||||
|
// 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, context.getString(R.string.activate_and_reboot), pActivateAndReboot).build());
|
||||||
|
builder.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_apps, context.getString(R.string.activate_only), pActivate).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(packageName, NOTIFICATION_MODULE_NOT_ACTIVATED_YET, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showModulesUpdatedNotification() {
|
||||||
|
Intent intent = new Intent(context, MainActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.putExtra(FRAGMENT_ID, 0);
|
||||||
|
|
||||||
|
PendingIntent pInstallTab = PendingIntent.getActivity(context, PENDING_INTENT_OPEN_INSTALL,
|
||||||
|
intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
String title = context
|
||||||
|
.getString(R.string.xposed_module_updated_notification_title);
|
||||||
|
String message = context
|
||||||
|
.getString(R.string.xposed_module_updated_notification);
|
||||||
|
NotificationCompat.Builder builder = getNotificationBuilder(title, message, NOTIFICATION_MODULES_CHANNEL)
|
||||||
|
.setContentIntent(pInstallTab);
|
||||||
|
if (prefs.getBoolean(HEADS_UP, true)) {
|
||||||
|
builder.setPriority(2);
|
||||||
|
}
|
||||||
|
Intent iSoftReboot = new Intent(context, RebootReceiver.class);
|
||||||
|
iSoftReboot.putExtra(RebootReceiver.EXTRA_SOFT_REBOOT, true);
|
||||||
|
PendingIntent pSoftReboot = PendingIntent.getBroadcast(context, PENDING_INTENT_SOFT_REBOOT,
|
||||||
|
iSoftReboot, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
Intent iReboot = new Intent(context, RebootReceiver.class);
|
||||||
|
PendingIntent pReboot = PendingIntent.getBroadcast(context, PENDING_INTENT_REBOOT,
|
||||||
|
iReboot, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
builder.addAction(new NotificationCompat.Action.Builder(0, context.getString(R.string.reboot), pReboot).build());
|
||||||
|
builder.addAction(new NotificationCompat.Action.Builder(0, context.getString(R.string.soft_reboot), pSoftReboot).build());
|
||||||
|
|
||||||
|
notificationManager.notify(null, NOTIFICATION_MODULES_UPDATED, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showInstallerUpdateNotification() {
|
||||||
|
Intent intent = new Intent(context, MainActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.putExtra(FRAGMENT_ID, 0);
|
||||||
|
|
||||||
|
PendingIntent pInstallTab = PendingIntent.getActivity(context, PENDING_INTENT_OPEN_INSTALL,
|
||||||
|
intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
String title = context.getString(R.string.app_name);
|
||||||
|
String message = context.getString(R.string.newVersion);
|
||||||
|
NotificationCompat.Builder builder = getNotificationBuilder(title, message, NOTIFICATION_UPDATE_CHANNEL)
|
||||||
|
.setContentIntent(pInstallTab);
|
||||||
|
|
||||||
|
if (prefs.getBoolean(HEADS_UP, true)) {
|
||||||
|
builder.setPriority(2);
|
||||||
|
}
|
||||||
|
NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle();
|
||||||
|
notiStyle.setBigContentTitle(title);
|
||||||
|
notiStyle.bigText(message);
|
||||||
|
builder.setStyle(notiStyle);
|
||||||
|
|
||||||
|
notificationManager.notify(null, NOTIFICATION_INSTALLER_UPDATE, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NotificationCompat.Builder getNotificationBuilder(String title, String message, String channel) {
|
||||||
|
return new NotificationCompat.Builder(context, channel)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(message)
|
||||||
|
.setVibrate(new long[]{0})
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setColor(ContextCompat.getColor(context, R.color.colorPrimary));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
context.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(context, R.string.module_activated, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
if (intent.hasExtra(EXTRA_ACTIVATE_MODULE_AND_RETURN)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Shell.rootAccess()) {
|
||||||
|
Log.e(App.TAG, "NotificationUtil -> Could not start root shell");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(App.TAG, "NotificationUtil -> Could not reboot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
private 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<>(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,434 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
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 com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.App;
|
||||||
|
import org.meowcat.edxposed.manager.R;
|
||||||
|
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.ui.activity.DownloadActivity;
|
||||||
|
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 instance = null;
|
||||||
|
private final List<RepoListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
private final Map<String, ReleaseType> localReleaseTypesCache = new HashMap<>();
|
||||||
|
private final App app;
|
||||||
|
private final SharedPreferences pref;
|
||||||
|
private final SharedPreferences modulePref;
|
||||||
|
private final ConnectivityManager conMgr;
|
||||||
|
private boolean isLoading = false;
|
||||||
|
private boolean reloadTriggeredOnce = false;
|
||||||
|
private Map<Long, Repository> repositories = null;
|
||||||
|
private ReleaseType globalReleaseType;
|
||||||
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
|
|
||||||
|
private RepoLoader() {
|
||||||
|
instance = this;
|
||||||
|
app = App.getInstance();
|
||||||
|
pref = app.getSharedPreferences("repo", Context.MODE_PRIVATE);
|
||||||
|
DEFAULT_REPOSITORIES = App.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";
|
||||||
|
modulePref = app.getSharedPreferences("module_settings", Context.MODE_PRIVATE);
|
||||||
|
conMgr = (ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
globalReleaseType = ReleaseType.fromString(App.getPreferences().getString("release_type_global", "stable"));
|
||||||
|
refreshRepositories();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized RepoLoader getInstance() {
|
||||||
|
if (instance == null)
|
||||||
|
new RepoLoader();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean refreshRepositories() {
|
||||||
|
repositories = RepoDb.getRepositories();
|
||||||
|
|
||||||
|
// Unlikely case (usually only during initial load): DB state doesn't
|
||||||
|
// fit to configuration
|
||||||
|
boolean needReload = false;
|
||||||
|
String[] config = (pref.getString("repositories", DEFAULT_REPOSITORIES) + "").split("\\|");
|
||||||
|
if (repositories.size() != config.length) {
|
||||||
|
needReload = true;
|
||||||
|
} else {
|
||||||
|
int i = 0;
|
||||||
|
for (Repository repo : repositories.values()) {
|
||||||
|
if (!repo.url.equals(config[i++])) {
|
||||||
|
needReload = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needReload)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
clear(false);
|
||||||
|
for (String url : config) {
|
||||||
|
RepoDb.insertRepository(url);
|
||||||
|
}
|
||||||
|
repositories = RepoDb.getRepositories();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReleaseTypeGlobal(String relTypeString) {
|
||||||
|
ReleaseType relType = ReleaseType.fromString(relTypeString);
|
||||||
|
if (globalReleaseType == relType)
|
||||||
|
return;
|
||||||
|
|
||||||
|
globalReleaseType = 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 (localReleaseTypesCache) {
|
||||||
|
if (relType != null) {
|
||||||
|
localReleaseTypesCache.put(packageName, relType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RepoDb.updateModuleLatestVersion(packageName);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReleaseType getReleaseTypeLocal(String packageName) {
|
||||||
|
synchronized (localReleaseTypesCache) {
|
||||||
|
if (localReleaseTypesCache.containsKey(packageName))
|
||||||
|
return localReleaseTypesCache.get(packageName);
|
||||||
|
|
||||||
|
String value = modulePref.getString(packageName + "_release_type",
|
||||||
|
null);
|
||||||
|
ReleaseType result = (!TextUtils.isEmpty(value)) ? ReleaseType.fromString(value) : null;
|
||||||
|
if (result != null) {
|
||||||
|
localReleaseTypesCache.put(packageName, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Repository getRepository(long repoId) {
|
||||||
|
return repositories.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 globalReleaseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void triggerReload(final boolean force) {
|
||||||
|
reloadTriggeredOnce = true;
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
resetLastUpdateCheck();
|
||||||
|
} else {
|
||||||
|
long lastUpdateCheck = pref.getLong("last_update_check", 0);
|
||||||
|
if (System.currentTimeMillis() < lastUpdateCheck + UPDATE_FREQUENCY)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkInfo netInfo = conMgr.getActiveNetworkInfo();
|
||||||
|
if (netInfo == null || !netInfo.isConnected())
|
||||||
|
return;
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
if (isLoading)
|
||||||
|
return;
|
||||||
|
isLoading = true;
|
||||||
|
}
|
||||||
|
app.updateProgressIndicator(swipeRefreshLayout);
|
||||||
|
|
||||||
|
new Thread("RepositoryReload") {
|
||||||
|
public void run() {
|
||||||
|
final List<String> messages = new LinkedList<>();
|
||||||
|
boolean hasChanged = downloadAndParseFiles(messages);
|
||||||
|
|
||||||
|
pref.edit().putLong("last_update_check", System.currentTimeMillis()).apply();
|
||||||
|
|
||||||
|
if (!messages.isEmpty()) {
|
||||||
|
App.runOnUiThread(() -> {
|
||||||
|
for (String message : messages) {
|
||||||
|
Toast.makeText(app, message, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanged)
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
app.updateProgressIndicator(swipeRefreshLayout);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSwipeRefreshLayout(SwipeRefreshLayout swipeRefreshLayout) {
|
||||||
|
this.swipeRefreshLayout = swipeRefreshLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void triggerFirstLoadIfNecessary() {
|
||||||
|
if (!reloadTriggeredOnce)
|
||||||
|
triggerReload(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetLastUpdateCheck() {
|
||||||
|
pref.edit().remove("last_update_check").apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isLoading() {
|
||||||
|
return isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(boolean notify) {
|
||||||
|
synchronized (this) {
|
||||||
|
// TODO Stop reloading repository when it should be cleared
|
||||||
|
if (isLoading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RepoDb.deleteRepositories();
|
||||||
|
repositories = new LinkedHashMap<>(0);
|
||||||
|
DownloadsUtil.clearCache(null);
|
||||||
|
resetLastUpdateCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notify)
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRepositories(String... repos) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < repos.length; i++) {
|
||||||
|
if (i > 0)
|
||||||
|
sb.append("|");
|
||||||
|
sb.append(repos[i]);
|
||||||
|
}
|
||||||
|
pref.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(app.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 : repositories.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(App.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(App.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) {
|
||||||
|
App.runOnUiThread(() -> new MaterialAlertDialogBuilder(app)
|
||||||
|
.setTitle(R.string.restart_needed)
|
||||||
|
.setMessage(R.string.cache_cleaned)
|
||||||
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||||
|
Intent i = new Intent(app, DownloadActivity.class);
|
||||||
|
PendingIntent pi = PendingIntent.getActivity(app, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||||
|
AlarmManager mgr = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pi);
|
||||||
|
System.exit(0);
|
||||||
|
})
|
||||||
|
.setCancelable(false)
|
||||||
|
.show());
|
||||||
|
|
||||||
|
DownloadsUtil.clearCache(url);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Log.e(App.TAG, "RepoLoader -> Cannot load repository from " + url, t);
|
||||||
|
messages.add(app.getString(R.string.repo_load_failed, url, t.getMessage()));
|
||||||
|
DownloadsUtil.clearCache(url);
|
||||||
|
} finally {
|
||||||
|
if (in != null)
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
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 (!listeners.contains(listener))
|
||||||
|
listeners.add(listener);
|
||||||
|
|
||||||
|
if (triggerImmediately)
|
||||||
|
listener.onRepoReloaded(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(RepoListener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyListeners() {
|
||||||
|
for (RepoListener listener : listeners) {
|
||||||
|
listener.onRepoReloaded(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface RepoListener {
|
||||||
|
/**
|
||||||
|
* Called whenever the list of modules from repositories has been
|
||||||
|
* successfully reloaded
|
||||||
|
*/
|
||||||
|
void onRepoReloaded(RepoLoader loader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.meowcat.edxposed.manager.util;
|
||||||
|
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class TaskRunner {
|
||||||
|
private final Executor executor = Executors.newSingleThreadExecutor(); // change according to your requirements
|
||||||
|
|
||||||
|
public <R> void executeAsync(Callable<R> callable) {
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
callable.call();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showLongToast(Context context, String msg) {
|
||||||
|
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.meowcat.edxposed.manager.util.chrome;
|
||||||
|
|
||||||
|
import android.text.style.URLSpan;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.BaseActivity;
|
||||||
|
import org.meowcat.edxposed.manager.util.NavUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Nikola D. on 12/23/2015.
|
||||||
|
*/
|
||||||
|
public class CustomTabsURLSpan extends URLSpan {
|
||||||
|
|
||||||
|
private final BaseActivity activity;
|
||||||
|
|
||||||
|
CustomTabsURLSpan(BaseActivity activity, String url) {
|
||||||
|
super(url);
|
||||||
|
this.activity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View widget) {
|
||||||
|
String url = getURL();
|
||||||
|
NavUtil.startURL(activity, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package org.meowcat.edxposed.manager.util.chrome;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
import org.meowcat.edxposed.manager.ui.activity.BaseActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Nikola D. on 12/23/2015.
|
||||||
|
*/
|
||||||
|
public class LinkTransformationMethod implements TransformationMethod {
|
||||||
|
|
||||||
|
private final BaseActivity activity;
|
||||||
|
|
||||||
|
public LinkTransformationMethod(BaseActivity 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,43 @@
|
||||||
|
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 = "https://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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class XposedJson {
|
||||||
|
public List<XposedTab> tabs;
|
||||||
|
public ApkRelease apk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ApkRelease {
|
||||||
|
public String version;
|
||||||
|
public String changelog;
|
||||||
|
public String link;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.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 androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
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_spinner_dropdown_item, 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 = ((AppCompatActivity) 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 static class ItemHolder {
|
||||||
|
TextView name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.meowcat.edxposed.manager.util.light;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.graphics.HardwareRenderer;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked", "ConstantConditions"})
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
|
public class Light {
|
||||||
|
|
||||||
|
public static boolean setLightSourceAlpha(View view, float ambientShadowAlpha, float spotShadowAlpha) {
|
||||||
|
try {
|
||||||
|
@SuppressWarnings("rawtypes") Class threadedRendererClass = Class.forName("android.view.ThreadedRenderer");
|
||||||
|
|
||||||
|
Object threadedRenderer = Hack.into(View.class)
|
||||||
|
.method("getThreadedRenderer")
|
||||||
|
.returning(threadedRendererClass)
|
||||||
|
.withoutParams()
|
||||||
|
.invoke()
|
||||||
|
.on(view);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
((HardwareRenderer) threadedRenderer).setLightSourceAlpha(ambientShadowAlpha, spotShadowAlpha);
|
||||||
|
} else {
|
||||||
|
long mNativeProxy = (long) Hack.into(threadedRendererClass)
|
||||||
|
.field("mNativeProxy").ofType(long.class).get(threadedRenderer);
|
||||||
|
|
||||||
|
float mLightRadius = (float) Hack.into(threadedRendererClass)
|
||||||
|
.field("mLightRadius")
|
||||||
|
.ofType(float.class)
|
||||||
|
.get(threadedRenderer);
|
||||||
|
|
||||||
|
Hack.into(threadedRendererClass)
|
||||||
|
.staticMethod("nSetup")
|
||||||
|
.withParams(long.class, float.class, int.class, int.class)
|
||||||
|
.invoke(mNativeProxy, mLightRadius,
|
||||||
|
(int) (255 * ambientShadowAlpha + 0.5f), (int) (255 * spotShadowAlpha + 0.5f))
|
||||||
|
.statically();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (Throwable tr) {
|
||||||
|
tr.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71s-0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z" />
|
||||||
|
</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="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
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,5s-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,8zM16,12v3c0,0.22 -0.03,0.47 -0.07,0.7l-0.1,0.65 -0.37,0.65c-0.72,1.24 -2.04,2 -3.46,2s-2.74,-0.77 -3.46,-2l-0.37,-0.64 -0.1,-0.65C8.03,15.48 8,15.23 8,15v-4c0,-0.23 0.03,-0.48 0.07,-0.7l0.1,-0.65 0.37,-0.65c0.3,-0.52 0.72,-0.97 1.21,-1.31l0.57,-0.39 0.74,-0.18c0.31,-0.08 0.63,-0.12 0.94,-0.12 0.32,0 0.63,0.04 0.95,0.12l0.68,0.16 0.61,0.42c0.5,0.34 0.91,0.78 1.21,1.31l0.38,0.65 0.1,0.65c0.04,0.22 0.07,0.47 0.07,0.69v1zM10,14h4v2h-4zM10,10h4v2h-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,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="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M8,16h8v2L8,18zM8,12h8v2L8,14zM14,2L6,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM18,20L6,20L6,4h7v5h5v11z" />
|
||||||
|
</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="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
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.25,2.52 0.77,-1.28 -3.52,-2.09L13.5,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="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
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,2s0.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,-2s0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2s-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,-2s-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,40 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="315"
|
||||||
|
android:viewportHeight="315">
|
||||||
|
<group
|
||||||
|
android:translateX="64.166664"
|
||||||
|
android:translateY="65.875">
|
||||||
|
<path
|
||||||
|
android:fillColor="#34bfc9"
|
||||||
|
android:pathData="M94,94m-94,0a94,94 0,1 1,188 0a94,94 0,1 1,-188 0" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#272727"
|
||||||
|
android:pathData="M91,41L144,41A6,6 0,0 1,150 47L150,139A6,6 0,0 1,144 145L91,145A6,6 0,0 1,85 139L85,47A6,6 0,0 1,91 41z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#34bfc9"
|
||||||
|
android:pathData="M84,72h37v42h-37z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#272727"
|
||||||
|
android:pathData="M31,72h36v42h-36z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#34bfc9"
|
||||||
|
android:pathData="M130,93m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#34bfc9"
|
||||||
|
android:pathData="M49,81m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#272727"
|
||||||
|
android:pathData="M99,71h4v8h-4z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#272727"
|
||||||
|
android:pathData="M101,84m-7,0a7,7 0,1 1,14 0a7,7 0,1 1,-14 0" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#272727"
|
||||||
|
android:pathData="M66,91h8v4h-8z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#272727"
|
||||||
|
android:pathData="M79,93m-7,0a7,7 0,1 1,14 0a7,7 0,1 1,-14 0" />
|
||||||
|
</group>
|
||||||
|
</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="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-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="?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="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,5.9c1.16,0 2.1,0.94 2.1,2.1s-0.94,2.1 -2.1,2.1S9.9,9.16 9.9,8s0.94,-2.1 2.1,-2.1m0,9c2.97,0 6.1,1.46 6.1,2.1v1.1L5.9,18.1L5.9,17c0,-0.64 3.13,-2.1 6.1,-2.1M12,4C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,13c-2.67,0 -8,1.34 -8,4v3h16v-3c0,-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="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6.54,5c0.06,0.89 0.21,1.76 0.45,2.59l-1.2,1.2c-0.41,-1.2 -0.67,-2.47 -0.76,-3.79h1.51m9.86,12.02c0.85,0.24 1.72,0.39 2.6,0.45v1.49c-1.32,-0.09 -2.59,-0.35 -3.8,-0.75l1.2,-1.19M7.5,3H4c-0.55,0 -1,0.45 -1,1 0,9.39 7.61,17 17,17 0.55,0 1,-0.45 1,-1v-3.49c0,-0.55 -0.45,-1 -1,-1 -1.24,0 -2.45,-0.2 -3.57,-0.57 -0.1,-0.04 -0.21,-0.05 -0.31,-0.05 -0.26,0 -0.51,0.1 -0.71,0.29l-2.2,2.2c-2.83,-1.45 -5.15,-3.76 -6.59,-6.59l2.2,-2.2c0.28,-0.28 0.36,-0.67 0.25,-1.02C8.7,6.45 8.5,5.25 8.5,4c0,-0.55 -0.45,-1 -1,-1z" />
|
||||||
|
</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="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
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,-4zM19,19L5,19L5,5h11.17L19,7.83L19,19zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3zM6,6h9v4L6,10z" />
|
||||||
|
</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.92s2.92,-1.31 2.92,-2.92c0,-1.61 -1.31,-2.92 -2.92,-2.92zM18,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM6,13c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM18,20.02c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z" />
|
||||||
|
</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="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue