mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-04-29 22:24:34 +02:00

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Co-authored-by: kitadai31 <90122968+kitadai31@users.noreply.github.com>
742 lines
27 KiB
Java
742 lines
27 KiB
Java
package app.revanced.integrations.shared;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.app.*;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.pm.PackageInfo;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.res.Resources;
|
|
import android.net.ConnectivityManager;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.preference.Preference;
|
|
import android.preference.PreferenceGroup;
|
|
import android.preference.PreferenceScreen;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.ViewParent;
|
|
import android.view.animation.Animation;
|
|
import android.view.animation.AnimationUtils;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.RelativeLayout;
|
|
import android.widget.Toast;
|
|
import android.widget.Toolbar;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import java.text.Bidi;
|
|
import java.util.*;
|
|
import java.util.regex.Pattern;
|
|
import java.util.concurrent.Callable;
|
|
import java.util.concurrent.Future;
|
|
import java.util.concurrent.SynchronousQueue;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import app.revanced.integrations.shared.settings.BooleanSetting;
|
|
import app.revanced.integrations.shared.settings.preference.ReVancedAboutPreference;
|
|
|
|
public class Utils {
|
|
|
|
@SuppressLint("StaticFieldLeak")
|
|
private static Context context;
|
|
|
|
private static String versionName;
|
|
|
|
private Utils() {
|
|
} // utility class
|
|
|
|
/**
|
|
* Injection point.
|
|
*
|
|
* @return The manifest 'Version' entry of the patches.jar used during patching.
|
|
*/
|
|
@SuppressWarnings("SameReturnValue")
|
|
public static String getPatchesReleaseVersion() {
|
|
return ""; // Value is replaced during patching.
|
|
}
|
|
|
|
/**
|
|
* @return The version name of the app, such as 19.11.43
|
|
*/
|
|
public static String getAppVersionName() {
|
|
if (versionName == null) {
|
|
try {
|
|
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
|
|
|
|
PackageManager packageManager = context.getPackageManager();
|
|
PackageInfo packageInfo;
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
packageInfo = packageManager.getPackageInfo(
|
|
packageName,
|
|
PackageManager.PackageInfoFlags.of(0)
|
|
);
|
|
} else {
|
|
packageInfo = packageManager.getPackageInfo(
|
|
packageName,
|
|
0
|
|
);
|
|
}
|
|
versionName = packageInfo.versionName;
|
|
} catch (Exception ex) {
|
|
Logger.printException(() -> "Failed to get package info", ex);
|
|
versionName = "Unknown";
|
|
}
|
|
}
|
|
|
|
return versionName;
|
|
}
|
|
|
|
|
|
/**
|
|
* Hide a view by setting its layout height and width to 1dp.
|
|
*
|
|
* @param condition The setting to check for hiding the view.
|
|
* @param view The view to hide.
|
|
*/
|
|
public static void hideViewBy0dpUnderCondition(BooleanSetting condition, View view) {
|
|
if (hideViewBy0dpUnderCondition(condition.get(), view)) {
|
|
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide a view by setting its layout height and width to 1dp.
|
|
*
|
|
* @param condition The setting to check for hiding the view.
|
|
* @param view The view to hide.
|
|
*/
|
|
public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) {
|
|
if (condition) {
|
|
hideViewByLayoutParams(view);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Hide a view by setting its visibility to GONE.
|
|
*
|
|
* @param condition The setting to check for hiding the view.
|
|
* @param view The view to hide.
|
|
*/
|
|
public static void hideViewUnderCondition(BooleanSetting condition, View view) {
|
|
if (hideViewUnderCondition(condition.get(), view)) {
|
|
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide a view by setting its visibility to GONE.
|
|
*
|
|
* @param condition The setting to check for hiding the view.
|
|
* @param view The view to hide.
|
|
*/
|
|
public static boolean hideViewUnderCondition(boolean condition, View view) {
|
|
if (condition) {
|
|
view.setVisibility(View.GONE);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting condition, View view) {
|
|
if (hideViewByRemovingFromParentUnderCondition(condition.get(), view)) {
|
|
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
|
}
|
|
}
|
|
|
|
public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) {
|
|
if (setting) {
|
|
ViewParent parent = view.getParent();
|
|
if (parent instanceof ViewGroup) {
|
|
((ViewGroup) parent).removeView(view);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* General purpose pool for network calls and other background tasks.
|
|
* All tasks run at max thread priority.
|
|
*/
|
|
private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
|
|
3, // 3 threads always ready to go
|
|
Integer.MAX_VALUE,
|
|
10, // For any threads over the minimum, keep them alive 10 seconds after they go idle
|
|
TimeUnit.SECONDS,
|
|
new SynchronousQueue<>(),
|
|
r -> { // ThreadFactory
|
|
Thread t = new Thread(r);
|
|
t.setPriority(Thread.MAX_PRIORITY); // run at max priority
|
|
return t;
|
|
});
|
|
|
|
public static void runOnBackgroundThread(@NonNull Runnable task) {
|
|
backgroundThreadPool.execute(task);
|
|
}
|
|
|
|
@NonNull
|
|
public static <T> Future<T> submitOnBackgroundThread(@NonNull Callable<T> call) {
|
|
return backgroundThreadPool.submit(call);
|
|
}
|
|
|
|
/**
|
|
* Simulates a delay by doing meaningless calculations.
|
|
* Used for debugging to verify UI timeout logic.
|
|
*/
|
|
@SuppressWarnings("UnusedReturnValue")
|
|
public static long doNothingForDuration(long amountOfTimeToWaste) {
|
|
final long timeCalculationStarted = System.currentTimeMillis();
|
|
Logger.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms");
|
|
|
|
long meaninglessValue = 0;
|
|
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
|
|
// could do a thread sleep, but that will trigger an exception if the thread is interrupted
|
|
meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
|
|
}
|
|
// return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
|
|
// leaving an empty loop that hammers on the System.currentTimeMillis native call
|
|
return meaninglessValue;
|
|
}
|
|
|
|
|
|
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
|
|
return indexOfFirstFound(value, targets) >= 0;
|
|
}
|
|
|
|
public static int indexOfFirstFound(@NonNull String value, @NonNull String... targets) {
|
|
for (String string : targets) {
|
|
if (!string.isEmpty()) {
|
|
final int indexOf = value.indexOf(string);
|
|
if (indexOf >= 0) return indexOf;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* @return zero, if the resource is not found
|
|
*/
|
|
@SuppressLint("DiscouragedApi")
|
|
public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) {
|
|
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
|
|
}
|
|
|
|
/**
|
|
* @return zero, if the resource is not found
|
|
*/
|
|
public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) {
|
|
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
|
}
|
|
|
|
public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
|
return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer"));
|
|
}
|
|
|
|
@NonNull
|
|
public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
|
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim"));
|
|
}
|
|
|
|
public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
|
//noinspection deprecation
|
|
return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color"));
|
|
}
|
|
|
|
public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
|
return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
|
}
|
|
|
|
public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
|
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
|
}
|
|
|
|
public interface MatchFilter<T> {
|
|
boolean matches(T object);
|
|
}
|
|
|
|
/**
|
|
* Includes sub children.
|
|
*
|
|
* @noinspection unchecked
|
|
*/
|
|
public static <R extends View> R getChildViewByResourceName(@NonNull View view, @NonNull String str) {
|
|
var child = view.findViewById(Utils.getResourceIdentifier(str, "id"));
|
|
if (child != null) {
|
|
return (R) child;
|
|
}
|
|
|
|
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
|
|
}
|
|
|
|
/**
|
|
* @param searchRecursively If children ViewGroups should also be
|
|
* recursively searched using depth first search.
|
|
* @return The first child view that matches the filter.
|
|
*/
|
|
@Nullable
|
|
public static <T extends View> T getChildView(@NonNull ViewGroup viewGroup, boolean searchRecursively,
|
|
@NonNull MatchFilter<View> filter) {
|
|
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
|
|
View childAt = viewGroup.getChildAt(i);
|
|
|
|
if (filter.matches(childAt)) {
|
|
//noinspection unchecked
|
|
return (T) childAt;
|
|
}
|
|
// Must do recursive after filter check, in case the filter is looking for a ViewGroup.
|
|
if (searchRecursively && childAt instanceof ViewGroup) {
|
|
T match = getChildView((ViewGroup) childAt, true, filter);
|
|
if (match != null) return match;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@Nullable
|
|
public static ViewParent getParentView(@NonNull View view, int nthParent) {
|
|
ViewParent parent = view.getParent();
|
|
|
|
int currentDepth = 0;
|
|
while (++currentDepth < nthParent && parent != null) {
|
|
parent = parent.getParent();
|
|
}
|
|
|
|
if (currentDepth == nthParent) {
|
|
return parent;
|
|
}
|
|
|
|
final int currentDepthLog = currentDepth;
|
|
Logger.printDebug(() -> "Could not find parent view of depth: " + nthParent
|
|
+ " and instead found at: " + currentDepthLog + " view: " + view);
|
|
return null;
|
|
}
|
|
|
|
public static void restartApp(@NonNull Context context) {
|
|
String packageName = context.getPackageName();
|
|
Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
|
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
|
|
// Required for API 34 and later
|
|
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
|
|
mainIntent.setPackage(packageName);
|
|
context.startActivity(mainIntent);
|
|
System.exit(0);
|
|
}
|
|
|
|
public static Context getContext() {
|
|
if (context == null) {
|
|
Logger.initializationException(Utils.class, "Context is null, returning null!", null);
|
|
}
|
|
return context;
|
|
}
|
|
|
|
public static void setContext(Context appContext) {
|
|
context = appContext;
|
|
// In some apps like TikTok, the Setting classes can load in weird orders due to cyclic class dependencies.
|
|
// Calling the regular printDebug method here can cause a Settings context null pointer exception,
|
|
// even though the context is already set before the call.
|
|
//
|
|
// The initialization logger methods do not directly or indirectly
|
|
// reference the Context or any Settings and are unaffected by this problem.
|
|
//
|
|
// Info level also helps debug if a patch hook is called before
|
|
// the context is set since debug logging is off by default.
|
|
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
|
|
}
|
|
|
|
public static void setClipboard(@NonNull String text) {
|
|
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
|
|
clipboard.setPrimaryClip(clip);
|
|
}
|
|
|
|
public static boolean isTablet() {
|
|
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
|
|
}
|
|
|
|
@Nullable
|
|
private static Boolean isRightToLeftTextLayout;
|
|
|
|
/**
|
|
* If the device language uses right to left text layout (hebrew, arabic, etc)
|
|
*/
|
|
public static boolean isRightToLeftTextLayout() {
|
|
if (isRightToLeftTextLayout == null) {
|
|
String displayLanguage = Locale.getDefault().getDisplayLanguage();
|
|
isRightToLeftTextLayout = new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
|
|
}
|
|
return isRightToLeftTextLayout;
|
|
}
|
|
|
|
/**
|
|
* @return if the text contains at least 1 number character,
|
|
* including any unicode numbers such as Arabic.
|
|
*/
|
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
|
public static boolean containsNumber(@NonNull CharSequence text) {
|
|
for (int index = 0, length = text.length(); index < length;) {
|
|
final int codePoint = Character.codePointAt(text, index);
|
|
if (Character.isDigit(codePoint)) {
|
|
return true;
|
|
}
|
|
index += Character.charCount(codePoint);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Ignore this class. It must be public to satisfy Android requirements.
|
|
*/
|
|
@SuppressWarnings("deprecation")
|
|
public static final class DialogFragmentWrapper extends DialogFragment {
|
|
|
|
private Dialog dialog;
|
|
@Nullable
|
|
private DialogFragmentOnStartAction onStartAction;
|
|
|
|
@Override
|
|
public void onSaveInstanceState(Bundle outState) {
|
|
// Do not call super method to prevent state saving.
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
return dialog;
|
|
}
|
|
|
|
@Override
|
|
public void onStart() {
|
|
try {
|
|
super.onStart();
|
|
|
|
if (onStartAction != null) {
|
|
onStartAction.onStart((AlertDialog) getDialog());
|
|
}
|
|
} catch (Exception ex) {
|
|
Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}.
|
|
*/
|
|
@FunctionalInterface
|
|
public interface DialogFragmentOnStartAction {
|
|
void onStart(AlertDialog dialog);
|
|
}
|
|
|
|
public static void showDialog(Activity activity, AlertDialog dialog) {
|
|
showDialog(activity, dialog, true, null);
|
|
}
|
|
|
|
/**
|
|
* Utility method to allow showing an AlertDialog on top of other alert dialogs.
|
|
* Calling this will always display the dialog on top of all other dialogs
|
|
* previously called using this method.
|
|
* <br>
|
|
* Be aware the on start action can be called multiple times for some situations,
|
|
* such as the user switching apps without dismissing the dialog then switching back to this app.
|
|
*<br>
|
|
* This method is only useful during app startup and multiple patches may show their own dialog,
|
|
* and the most important dialog can be called last (using a delay) so it's always on top.
|
|
*<br>
|
|
* For all other situations it's better to not use this method and
|
|
* call {@link AlertDialog#show()} on the dialog.
|
|
*/
|
|
@SuppressWarnings("deprecation")
|
|
public static void showDialog(Activity activity,
|
|
AlertDialog dialog,
|
|
boolean isCancelable,
|
|
@Nullable DialogFragmentOnStartAction onStartAction) {
|
|
verifyOnMainThread();
|
|
|
|
DialogFragmentWrapper fragment = new DialogFragmentWrapper();
|
|
fragment.dialog = dialog;
|
|
fragment.onStartAction = onStartAction;
|
|
fragment.setCancelable(isCancelable);
|
|
|
|
fragment.show(activity.getFragmentManager(), null);
|
|
}
|
|
|
|
/**
|
|
* Safe to call from any thread
|
|
*/
|
|
public static void showToastShort(@NonNull String messageToToast) {
|
|
showToast(messageToToast, Toast.LENGTH_SHORT);
|
|
}
|
|
|
|
/**
|
|
* Safe to call from any thread
|
|
*/
|
|
public static void showToastLong(@NonNull String messageToToast) {
|
|
showToast(messageToToast, Toast.LENGTH_LONG);
|
|
}
|
|
|
|
private static void showToast(@NonNull String messageToToast, int toastDuration) {
|
|
Objects.requireNonNull(messageToToast);
|
|
runOnMainThreadNowOrLater(() -> {
|
|
if (context == null) {
|
|
Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
|
|
} else {
|
|
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
|
Toast.makeText(context, messageToToast, toastDuration).show();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Automatically logs any exceptions the runnable throws.
|
|
*
|
|
* @see #runOnMainThreadNowOrLater(Runnable)
|
|
*/
|
|
public static void runOnMainThread(@NonNull Runnable runnable) {
|
|
runOnMainThreadDelayed(runnable, 0);
|
|
}
|
|
|
|
/**
|
|
* Automatically logs any exceptions the runnable throws
|
|
*/
|
|
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
|
|
Runnable loggingRunnable = () -> {
|
|
try {
|
|
runnable.run();
|
|
} catch (Exception ex) {
|
|
Logger.printException(() -> runnable.getClass().getSimpleName() + ": " + ex.getMessage(), ex);
|
|
}
|
|
};
|
|
new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis);
|
|
}
|
|
|
|
/**
|
|
* If called from the main thread, the code is run immediately.<p>
|
|
* If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}.
|
|
*/
|
|
public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) {
|
|
if (isCurrentlyOnMainThread()) {
|
|
runnable.run();
|
|
} else {
|
|
runOnMainThread(runnable);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return if the calling thread is on the main thread
|
|
*/
|
|
public static boolean isCurrentlyOnMainThread() {
|
|
return Looper.getMainLooper().isCurrentThread();
|
|
}
|
|
|
|
/**
|
|
* @throws IllegalStateException if the calling thread is _off_ the main thread
|
|
*/
|
|
public static void verifyOnMainThread() throws IllegalStateException {
|
|
if (!isCurrentlyOnMainThread()) {
|
|
throw new IllegalStateException("Must call _on_ the main thread");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @throws IllegalStateException if the calling thread is _on_ the main thread
|
|
*/
|
|
public static void verifyOffMainThread() throws IllegalStateException {
|
|
if (isCurrentlyOnMainThread()) {
|
|
throw new IllegalStateException("Must call _off_ the main thread");
|
|
}
|
|
}
|
|
|
|
public enum NetworkType {
|
|
NONE,
|
|
MOBILE,
|
|
OTHER,
|
|
}
|
|
|
|
public static boolean isNetworkConnected() {
|
|
NetworkType networkType = getNetworkType();
|
|
return networkType == NetworkType.MOBILE
|
|
|| networkType == NetworkType.OTHER;
|
|
}
|
|
|
|
@SuppressLint("MissingPermission") // permission already included in YouTube
|
|
public static NetworkType getNetworkType() {
|
|
Context networkContext = getContext();
|
|
if (networkContext == null) {
|
|
return NetworkType.NONE;
|
|
}
|
|
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
var networkInfo = cm.getActiveNetworkInfo();
|
|
|
|
if (networkInfo == null || !networkInfo.isConnected()) {
|
|
return NetworkType.NONE;
|
|
}
|
|
var type = networkInfo.getType();
|
|
return (type == ConnectivityManager.TYPE_MOBILE)
|
|
|| (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER;
|
|
}
|
|
|
|
/**
|
|
* Hide a view by setting its layout params to 0x0
|
|
* @param view The view to hide.
|
|
*/
|
|
public static void hideViewByLayoutParams(View view) {
|
|
if (view instanceof LinearLayout) {
|
|
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0);
|
|
view.setLayoutParams(layoutParams);
|
|
} else if (view instanceof FrameLayout) {
|
|
FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0);
|
|
view.setLayoutParams(layoutParams2);
|
|
} else if (view instanceof RelativeLayout) {
|
|
RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(0, 0);
|
|
view.setLayoutParams(layoutParams3);
|
|
} else if (view instanceof Toolbar) {
|
|
Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(0, 0);
|
|
view.setLayoutParams(layoutParams4);
|
|
} else if (view instanceof ViewGroup) {
|
|
ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(0, 0);
|
|
view.setLayoutParams(layoutParams5);
|
|
} else {
|
|
ViewGroup.LayoutParams params = view.getLayoutParams();
|
|
params.width = 0;
|
|
params.height = 0;
|
|
view.setLayoutParams(params);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@link PreferenceScreen} and {@link PreferenceGroup} sorting styles.
|
|
*/
|
|
private enum Sort {
|
|
/**
|
|
* Sort by the localized preference title.
|
|
*/
|
|
BY_TITLE("_sort_by_title"),
|
|
|
|
/**
|
|
* Sort by the preference keys.
|
|
*/
|
|
BY_KEY("_sort_by_key"),
|
|
|
|
/**
|
|
* Unspecified sorting.
|
|
*/
|
|
UNSORTED("_sort_by_unsorted");
|
|
|
|
final String keySuffix;
|
|
|
|
Sort(String keySuffix) {
|
|
this.keySuffix = keySuffix;
|
|
}
|
|
|
|
@NonNull
|
|
static Sort fromKey(@Nullable String key, @NonNull Sort defaultSort) {
|
|
if (key != null) {
|
|
for (Sort sort : values()) {
|
|
if (key.endsWith(sort.keySuffix)) {
|
|
return sort;
|
|
}
|
|
}
|
|
}
|
|
return defaultSort;
|
|
}
|
|
}
|
|
|
|
private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+");
|
|
|
|
/**
|
|
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
|
|
*/
|
|
public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) {
|
|
if (original == null) return "";
|
|
return punctuationPattern.matcher(original).replaceAll("").toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Sort a PreferenceGroup and all it's sub groups by title or key.
|
|
*
|
|
* Sort order is determined by the preferences key {@link Sort} suffix.
|
|
*
|
|
* If a preference has no key or no {@link Sort} suffix,
|
|
* then the preferences are left unsorted.
|
|
*/
|
|
@SuppressWarnings("deprecation")
|
|
public static void sortPreferenceGroups(@NonNull PreferenceGroup group) {
|
|
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
|
SortedMap<String, Preference> preferences = new TreeMap<>();
|
|
|
|
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
|
Preference preference = group.getPreference(i);
|
|
|
|
final Sort preferenceSort;
|
|
if (preference instanceof PreferenceGroup) {
|
|
sortPreferenceGroups((PreferenceGroup) preference);
|
|
preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
|
|
} else {
|
|
// Allow individual preferences to set a key sorting.
|
|
// Used to force a preference to the top or bottom of a group.
|
|
preferenceSort = Sort.fromKey(preference.getKey(), groupSort);
|
|
}
|
|
|
|
final String sortValue;
|
|
switch (preferenceSort) {
|
|
case BY_TITLE:
|
|
sortValue = removePunctuationConvertToLowercase(preference.getTitle());
|
|
break;
|
|
case BY_KEY:
|
|
sortValue = preference.getKey();
|
|
break;
|
|
case UNSORTED:
|
|
continue; // Keep original sorting.
|
|
default:
|
|
throw new IllegalStateException();
|
|
}
|
|
|
|
preferences.put(sortValue, preference);
|
|
}
|
|
|
|
int index = 0;
|
|
for (Preference pref : preferences.values()) {
|
|
int order = index++;
|
|
|
|
// Move any screens, intents, and the one off About preference to the top.
|
|
if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference
|
|
|| pref.getIntent() != null) {
|
|
// Arbitrary high number.
|
|
order -= 1000;
|
|
}
|
|
|
|
pref.setOrder(order);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If {@link Fragment} uses [Android library] rather than [AndroidX library],
|
|
* the Dialog theme corresponding to [Android library] should be used.
|
|
* <p>
|
|
* If not, the following issues will occur:
|
|
* <a href="https://github.com/ReVanced/revanced-patches/issues/3061">ReVanced/revanced-patches#3061</a>
|
|
* <p>
|
|
* To prevent these issues, apply the Dialog theme corresponding to [Android library].
|
|
*/
|
|
public static void setEditTextDialogTheme(AlertDialog.Builder builder) {
|
|
final int editTextDialogStyle = getResourceIdentifier(
|
|
"revanced_edit_text_dialog_style", "style");
|
|
if (editTextDialogStyle != 0) {
|
|
builder.getContext().setTheme(editTextDialogStyle);
|
|
}
|
|
}
|
|
}
|