mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-06-02 11:26:18 +02:00
44c3cc4636
If Kotlin Regex would be used, then apps need to have the Kotlin libraries for Regex to work which isn't always the case.
545 lines
20 KiB
Java
545 lines
20 KiB
Java
package app.revanced.integrations.shared;
|
|
|
|
import android.annotation.SuppressLint;
|
|
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.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.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.
|
|
*/
|
|
public static String getPatchesReleaseVersion() {
|
|
return ""; // Value is replaced during patching.
|
|
}
|
|
|
|
/**
|
|
* @return The version name of the app, such as "YouTube".
|
|
*/
|
|
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 hideViewBy1dpUnderCondition(BooleanSetting condition, View view) {
|
|
if (!condition.get()) return;
|
|
|
|
Logger.printDebug(() -> "Hiding view with setting: " + condition);
|
|
|
|
hideViewByLayoutParams(view);
|
|
}
|
|
|
|
/**
|
|
* 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 (!condition.get()) return;
|
|
|
|
Logger.printDebug(() -> "Hiding view with setting: " + condition);
|
|
|
|
view.setVisibility(View.GONE);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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 1x1
|
|
* @param view The view to hide.
|
|
*/
|
|
public static void hideViewByLayoutParams(View view) {
|
|
if (view instanceof LinearLayout) {
|
|
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(1, 1);
|
|
view.setLayoutParams(layoutParams);
|
|
} else if (view instanceof FrameLayout) {
|
|
FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(1, 1);
|
|
view.setLayoutParams(layoutParams2);
|
|
} else if (view instanceof RelativeLayout) {
|
|
RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(1, 1);
|
|
view.setLayoutParams(layoutParams3);
|
|
} else if (view instanceof Toolbar) {
|
|
Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(1, 1);
|
|
view.setLayoutParams(layoutParams4);
|
|
} else if (view instanceof ViewGroup) {
|
|
ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(1, 1);
|
|
view.setLayoutParams(layoutParams5);
|
|
} else {
|
|
Logger.printDebug(() -> "Hidden view with id " + view.getId());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@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);
|
|
}
|
|
}
|
|
}
|