Merge remote-tracking branch 'app/master'

This commit is contained in:
kotori0 2021-01-25 20:55:14 +08:00
commit ffb83c0ba8
No known key found for this signature in database
GPG Key ID: 3FEE57ED0385A6B2
186 changed files with 19424 additions and 8 deletions

View File

@ -18,7 +18,11 @@ jobs:
- name: Get version code
run: echo APPVEYOR_BUILD_NUMBER=$(expr $GITHUB_RUN_NUMBER + 4999) >> $GITHUB_ENV
- 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
if: success()
run: unzip edxp-core/release/LSPosed-v*-release.zip -d LSPosed-release;

3
.gitignore vendored
View File

@ -19,5 +19,4 @@ elf-cleaner.sh
.settings/
.vscode/
dalvikdx/bin/
dexmaker/bin/
.cxx

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

84
app/build.gradle Normal file
View File

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

BIN
app/edxpmanager.jks Normal file

Binary file not shown.

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

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

View File

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

View File

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

View File

@ -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 + "/";
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
package org.meowcat.edxposed.manager.repo;
import android.util.Pair;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class Module {
@SuppressWarnings("WeakerAccess")
public final Repository repository;
public final List<Pair<String, String>> moreInfo = new LinkedList<>();
public final List<ModuleVersion> versions = new ArrayList<>();
final List<String> screenshots = new ArrayList<>();
public String packageName;
public String name;
public String summary;
public String description;
public boolean descriptionIsHtml = false;
public String author;
public String support;
long created = -1;
long updated = -1;
Module(Repository repository) {
this.repository = repository;
}
}

View File

@ -0,0 +1,17 @@
package org.meowcat.edxposed.manager.repo;
public class ModuleVersion {
public final Module module;
public String name;
public int code;
public String downloadLink;
public String md5sum;
public String changelog;
public boolean changelogIsHtml = false;
public ReleaseType relType = ReleaseType.STABLE;
public long uploaded = -1;
/* package */ ModuleVersion(Module module) {
this.module = module;
}
}

View File

@ -0,0 +1,40 @@
package org.meowcat.edxposed.manager.repo;
import org.meowcat.edxposed.manager.R;
public enum ReleaseType {
STABLE(R.string.reltype_stable, R.string.reltype_stable_summary), BETA(R.string.reltype_beta, R.string.reltype_beta_summary), EXPERIMENTAL(R.string.reltype_experimental, R.string.reltype_experimental_summary);
private static final ReleaseType[] sValuesCache = values();
private final int mTitleId;
private final int mSummaryId;
ReleaseType(int titleId, int summaryId) {
mTitleId = titleId;
mSummaryId = summaryId;
}
public static ReleaseType fromString(String value) {
if (value == null || value.equals("stable"))
return STABLE;
else if (value.equals("beta"))
return BETA;
else if (value.equals("experimental"))
return EXPERIMENTAL;
else
return STABLE;
}
public static ReleaseType fromOrdinal(int ordinal) {
return sValuesCache[ordinal];
}
public int getTitleId() {
return mTitleId;
}
public int getSummaryId() {
return mSummaryId;
}
}

View File

@ -0,0 +1,492 @@
package org.meowcat.edxposed.manager.repo;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import android.util.Pair;
import org.meowcat.edxposed.manager.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);
}
}
}

View File

@ -0,0 +1,216 @@
package org.meowcat.edxposed.manager.repo;
import android.database.Cursor;
import android.provider.BaseColumns;
public class RepoDbDefinitions {
static final int DATABASE_VERSION = 4;
static final String DATABASE_NAME = "repo_cache.db";
static final String SQL_CREATE_TABLE_REPOSITORIES = "CREATE TABLE "
+ RepositoriesColumns.TABLE_NAME + " (" + RepositoriesColumns._ID
+ " INTEGER PRIMARY KEY AUTOINCREMENT," + RepositoriesColumns.URL
+ " TEXT NOT NULL, " + RepositoriesColumns.TITLE + " TEXT, "
+ RepositoriesColumns.PARTIAL_URL + " TEXT, "
+ RepositoriesColumns.VERSION + " TEXT, " + "UNIQUE ("
+ RepositoriesColumns.URL + ") ON CONFLICT REPLACE)";
static final String SQL_CREATE_TABLE_MODULES = "CREATE TABLE "
+ ModulesColumns.TABLE_NAME + " (" + ModulesColumns._ID
+ " INTEGER PRIMARY KEY AUTOINCREMENT," +
ModulesColumns.REPO_ID + " INTEGER NOT NULL REFERENCES "
+ RepositoriesColumns.TABLE_NAME + " ON DELETE CASCADE, "
+ ModulesColumns.PKGNAME + " TEXT NOT NULL, " + ModulesColumns.TITLE
+ " TEXT NOT NULL, " + ModulesColumns.SUMMARY + " TEXT, "
+ ModulesColumns.DESCRIPTION + " TEXT, "
+ ModulesColumns.DESCRIPTION_IS_HTML + " INTEGER DEFAULT 0, "
+ ModulesColumns.AUTHOR + " TEXT, " + ModulesColumns.SUPPORT
+ " TEXT, " + ModulesColumns.CREATED + " INTEGER DEFAULT -1, "
+ ModulesColumns.UPDATED + " INTEGER DEFAULT -1, "
+ ModulesColumns.PREFERRED + " INTEGER DEFAULT 1, "
+ ModulesColumns.LATEST_VERSION + " INTEGER REFERENCES "
+ ModuleVersionsColumns.TABLE_NAME + ", " + "UNIQUE ("
+ ModulesColumns.PKGNAME + ", " + ModulesColumns.REPO_ID
+ ") ON CONFLICT REPLACE)";
static final String SQL_CREATE_TABLE_MODULE_VERSIONS = "CREATE TABLE "
+ ModuleVersionsColumns.TABLE_NAME + " ("
+ ModuleVersionsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ ModuleVersionsColumns.MODULE_ID + " INTEGER NOT NULL REFERENCES "
+ ModulesColumns.TABLE_NAME + " ON DELETE CASCADE, "
+ ModuleVersionsColumns.NAME + " TEXT NOT NULL, "
+ ModuleVersionsColumns.CODE + " INTEGER NOT NULL, "
+ ModuleVersionsColumns.DOWNLOAD_LINK + " TEXT, "
+ ModuleVersionsColumns.MD5SUM + " TEXT, "
+ ModuleVersionsColumns.CHANGELOG + " TEXT, "
+ ModuleVersionsColumns.CHANGELOG_IS_HTML + " INTEGER DEFAULT 0, "
+ ModuleVersionsColumns.RELTYPE + " INTEGER DEFAULT 0, "
+ ModuleVersionsColumns.UPLOADED + " INTEGER DEFAULT -1)";
static final String SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID = "CREATE INDEX "
+ ModuleVersionsColumns.IDX_MODULE_ID + " ON "
+ ModuleVersionsColumns.TABLE_NAME + " ("
+ ModuleVersionsColumns.MODULE_ID + ")";
static final String SQL_CREATE_TABLE_MORE_INFO = "CREATE TABLE "
+ MoreInfoColumns.TABLE_NAME + " (" + MoreInfoColumns._ID
+ " INTEGER PRIMARY KEY AUTOINCREMENT," + MoreInfoColumns.MODULE_ID
+ " INTEGER NOT NULL REFERENCES " + ModulesColumns.TABLE_NAME
+ " ON DELETE CASCADE, " + MoreInfoColumns.LABEL
+ " TEXT NOT NULL, " + MoreInfoColumns.VALUE + " TEXT)";
static final String SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES = "CREATE TEMP TABLE "
+ InstalledModulesColumns.TABLE_NAME + " ("
+ InstalledModulesColumns.PKGNAME
+ " TEXT PRIMARY KEY ON CONFLICT REPLACE, "
+ InstalledModulesColumns.VERSION_CODE + " INTEGER NOT NULL, "
+ InstalledModulesColumns.VERSION_NAME + " TEXT)";
static final String SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES = "CREATE TEMP VIEW "
+ InstalledModulesUpdatesColumns.VIEW_NAME + " AS SELECT " + "m."
+ ModulesColumns._ID + " AS "
+ InstalledModulesUpdatesColumns.MODULE_ID + ", " + "i."
+ InstalledModulesColumns.PKGNAME + " AS "
+ InstalledModulesUpdatesColumns.PKGNAME + ", " + "i."
+ InstalledModulesColumns.VERSION_CODE + " AS "
+ InstalledModulesUpdatesColumns.INSTALLED_CODE + ", " + "i."
+ InstalledModulesColumns.VERSION_NAME + " AS "
+ InstalledModulesUpdatesColumns.INSTALLED_NAME + ", " + "v."
+ ModuleVersionsColumns._ID + " AS "
+ InstalledModulesUpdatesColumns.LATEST_ID + ", " + "v."
+ ModuleVersionsColumns.CODE + " AS "
+ InstalledModulesUpdatesColumns.LATEST_CODE + ", " + "v."
+ ModuleVersionsColumns.NAME + " AS "
+ InstalledModulesUpdatesColumns.LATEST_NAME + " FROM "
+ InstalledModulesColumns.TABLE_NAME + " AS i" + " INNER JOIN "
+ ModulesColumns.TABLE_NAME + " AS m" + " ON m."
+ ModulesColumns.PKGNAME + " = i." + InstalledModulesColumns.PKGNAME
+ " INNER JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v"
+ " ON v." + ModuleVersionsColumns._ID + " = m."
+ ModulesColumns.LATEST_VERSION + " WHERE "
+ InstalledModulesUpdatesColumns.LATEST_CODE + " > "
+ InstalledModulesUpdatesColumns.INSTALLED_CODE + " AND "
+ ModulesColumns.PREFERRED + " = 1";
//////////////////////////////////////////////////////////////////////////
public interface RepositoriesColumns extends BaseColumns {
String TABLE_NAME = "repositories";
String URL = "url";
String TITLE = "title";
String PARTIAL_URL = "partial_url";
String VERSION = "version";
}
//////////////////////////////////////////////////////////////////////////
public interface ModulesColumns extends BaseColumns {
String TABLE_NAME = "modules";
String REPO_ID = "repo_id";
String PKGNAME = "pkgname";
String TITLE = "title";
String SUMMARY = "summary";
String DESCRIPTION = "description";
String DESCRIPTION_IS_HTML = "description_is_html";
String AUTHOR = "author";
String SUPPORT = "support";
String CREATED = "created";
String UPDATED = "updated";
String PREFERRED = "preferred";
String LATEST_VERSION = "latest_version_id";
}
//////////////////////////////////////////////////////////////////////////
public interface ModuleVersionsColumns extends BaseColumns {
String TABLE_NAME = "module_versions";
String IDX_MODULE_ID = "module_versions_module_id_idx";
String MODULE_ID = "module_id";
String NAME = "name";
String CODE = "code";
String DOWNLOAD_LINK = "download_link";
String MD5SUM = "md5sum";
String CHANGELOG = "changelog";
String CHANGELOG_IS_HTML = "changelog_is_html";
String RELTYPE = "reltype";
String UPLOADED = "uploaded";
}
//////////////////////////////////////////////////////////////////////////
public interface MoreInfoColumns extends BaseColumns {
String TABLE_NAME = "more_info";
String MODULE_ID = "module_id";
String LABEL = "label";
String VALUE = "value";
}
//////////////////////////////////////////////////////////////////////////
public interface InstalledModulesColumns {
String TABLE_NAME = "installed_modules";
String PKGNAME = "pkgname";
String VERSION_CODE = "version_code";
String VERSION_NAME = "version_name";
}
//////////////////////////////////////////////////////////////////////////
public interface InstalledModulesUpdatesColumns {
String VIEW_NAME = InstalledModulesColumns.TABLE_NAME + "_updates";
String MODULE_ID = "module_id";
String PKGNAME = "pkgname";
String INSTALLED_CODE = "installed_code";
String INSTALLED_NAME = "installed_name";
String LATEST_ID = "latest_id";
String LATEST_CODE = "latest_code";
String LATEST_NAME = "latest_name";
}
//////////////////////////////////////////////////////////////////////////
public interface OverviewColumns extends BaseColumns {
String PKGNAME = ModulesColumns.PKGNAME;
String TITLE = ModulesColumns.TITLE;
String SUMMARY = ModulesColumns.SUMMARY;
String CREATED = ModulesColumns.CREATED;
String UPDATED = ModulesColumns.UPDATED;
String INSTALLED_VERSION = "installed_version";
String LATEST_VERSION = "latest_version";
String IS_FRAMEWORK = "is_framework";
String IS_INSTALLED = "is_installed";
String HAS_UPDATE = "has_update";
}
public static class OverviewColumnsIndexes {
public static int PKGNAME = -1;
public static int TITLE = -1;
public static int SUMMARY = -1;
public static int CREATED = -1;
public static int UPDATED = -1;
public static int INSTALLED_VERSION = -1;
public static int LATEST_VERSION = -1;
public static int IS_FRAMEWORK = -1;
public static int IS_INSTALLED = -1;
public static int HAS_UPDATE = -1;
private static boolean isFilled = false;
private OverviewColumnsIndexes() {
}
static void fillFromCursor(Cursor cursor) {
if (isFilled || cursor == null)
return;
PKGNAME = cursor.getColumnIndexOrThrow(OverviewColumns.PKGNAME);
TITLE = cursor.getColumnIndexOrThrow(OverviewColumns.TITLE);
SUMMARY = cursor.getColumnIndexOrThrow(OverviewColumns.SUMMARY);
CREATED = cursor.getColumnIndexOrThrow(OverviewColumns.CREATED);
UPDATED = cursor.getColumnIndexOrThrow(OverviewColumns.UPDATED);
INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION);
LATEST_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.LATEST_VERSION);
INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION);
IS_FRAMEWORK = cursor.getColumnIndexOrThrow(OverviewColumns.IS_FRAMEWORK);
IS_INSTALLED = cursor.getColumnIndexOrThrow(OverviewColumns.IS_INSTALLED);
HAS_UPDATE = cursor.getColumnIndexOrThrow(OverviewColumns.HAS_UPDATE);
isFilled = true;
}
}
}

View File

@ -0,0 +1,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);
}
}

View File

@ -0,0 +1,12 @@
package org.meowcat.edxposed.manager.repo;
public class Repository {
public String name;
public String url;
public boolean isPartial = false;
public String partialUrl;
public String version;
Repository() {
}
}

View File

@ -0,0 +1,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());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,43 @@
package org.meowcat.edxposed.manager.util;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import androidx.fragment.app.FragmentManager;
import org.meowcat.edxposed.manager.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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,64 @@
package org.meowcat.edxposed.manager.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class HashUtil {
private static String hash(String input, @SuppressWarnings("SameParameterValue") String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] messageDigest = md.digest(input.getBytes());
return toHexString(messageDigest);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
}
static String md5(String input) {
return hash(input, "MD5");
}
// public static String sha1(String input) {
// return hash(input, "SHA-1");
// }
private static String hash(File file, @SuppressWarnings("SameParameterValue") String algorithm) throws IOException {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
InputStream is = new FileInputStream(file);
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) > 0) {
md.update(buffer, 0, read);
}
is.close();
byte[] messageDigest = md.digest();
return toHexString(messageDigest);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
}
public static String md5(File input) throws IOException {
return hash(input, "MD5");
}
// public static String sha1(File input) throws IOException {
// return hash(input, "SHA-1");
// }
private static String toHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
int unsignedB = b & 0xff;
if (unsignedB < 0x10)
sb.append("0");
sb.append(Integer.toHexString(unsignedB));
}
return sb.toString();
}
}

View File

@ -0,0 +1,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));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,161 @@
package org.meowcat.edxposed.manager.util;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class PrefixedSharedPreferences implements SharedPreferences {
private final SharedPreferences mBase;
private final String mPrefix;
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();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="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>

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?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>

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?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>

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?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>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?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>

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?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>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="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>

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?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>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?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