From a2f40cf27a26a621eec313c46c390924e689f7f5 Mon Sep 17 00:00:00 2001 From: NekoInverter <42698724+NekoInverter@users.noreply.github.com> Date: Sat, 21 Nov 2020 22:42:13 +0800 Subject: [PATCH] Fix MainActivity lighting --- app/src/main/AndroidManifest.xml | 4 - .../edxposed/manager/MainActivity.java | 17 +- .../edxposed/manager/util/light/Hack.java | 1203 +++++++++++++++++ .../edxposed/manager/util/light/Light.java | 47 + app/src/main/res/layout/activity_main.xml | 6 +- app/src/main/res/values/styles.xml | 5 - 6 files changed, 1266 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/light/Hack.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/light/Light.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b6b4bcb6..63843887 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,7 +43,6 @@ android:label="@string/menu_scope"/> @@ -83,9 +82,6 @@ - { + if (Light.setLightSourceAlpha(getWindow().getDecorView(), 0.01f, 0.029f)) { + binding.status.setElevation(24); + binding.modules.setElevation(12); + binding.downloads.setElevation(12); + } + }); setupWindowInsets(binding.snackbar, binding.nestedScrollView); repoLoader = RepoLoader.getInstance(); ModuleUtil.getInstance().addListener(this); @@ -84,23 +93,23 @@ public class MainActivity extends BaseActivity implements RepoLoader.RepoListene binding.statusTitle.setText(R.string.Activated); binding.statusSummary.setText(installedXposedVersion.replace(installedXposedVersionStr + "-", "")); binding.status.setCardBackgroundColor(ContextCompat.getColor(this, R.color.download_status_update_available)); - binding.statusIcon.setImageDrawable(getDrawable(R.drawable.ic_check_circle)); + 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.setImageDrawable(getDrawable(R.drawable.ic_warning)); + binding.statusIcon.setImageResource(R.drawable.ic_warning); } } else if (XposedApp.getXposedVersion() > 0) { binding.statusTitle.setText(R.string.Activated); binding.statusSummary.setText(getString(R.string.version_x, XposedApp.getXposedVersion())); binding.status.setCardBackgroundColor(ContextCompat.getColor(this, R.color.download_status_update_available)); - binding.statusIcon.setImageDrawable(getDrawable(R.drawable.ic_check_circle)); + 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.setImageDrawable(getDrawable(R.drawable.ic_error)); + binding.statusIcon.setImageResource(R.drawable.ic_error); } notifyDataSetChanged(); new Thread(() -> new BlackListAdapter(getApplicationContext(), AppHelper.isWhiteListMode()).generateCheckedList()); diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/light/Hack.java b/app/src/main/java/org/meowcat/edxposed/manager/util/light/Hack.java new file mode 100644 index 00000000..69198517 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/light/Hack.java @@ -0,0 +1,1203 @@ +package org.meowcat.edxposed.manager.util.light; + +import android.util.Log; + +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Comparator; + +/** + * Java reflection helper optimized for hacking non-public APIs. + * The core design philosophy behind is compile-time consistency enforcement. + *

+ * It's suggested to declare all hacks in a centralized point, typically as static fields in a class. + * Then call it during application initialization, thus they are verified all together in an early stage. + * If any assertion failed, a fall-back strategy is suggested. + * + *

https://gist.github.com/oasisfeng/75d3774ca5441372f049818de4d52605 + * + * @author Oasis + * @see Demo + */ +@SuppressWarnings({"Convert2Lambda", "WeakerAccess", "unused"}) +class Hack { + + public static Class ANY_TYPE = $.class; + private static final HackedClass FALLBACK = new HackedClass<>(ANY_TYPE); + private static AssertionFailureHandler sFailureHandler; + + private Hack() { + } + + public static HackedClass into(final @NonNull Class clazz) { + return new HackedClass<>(clazz); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static HackedClass into(final String class_name) { + try { + return new HackedClass(Class.forName(class_name)); + } catch (final ClassNotFoundException e) { + fail(new AssertionException(e)); + return new HackedClass(ANY_TYPE); // Use AnyType as a lazy trick to make fallback working and avoid null. + } + } + + @SuppressWarnings("unchecked") + public static HackedClass onlyIf(final boolean condition, final Hacking> hacking) { + if (condition) return hacking.hack(); + return (HackedClass) FALLBACK; + } + + public static ConditionalHack onlyIf(final boolean condition) { + return condition ? new ConditionalHack() { + @Override + public HackedClass into(@NonNull final Class clazz) { + return Hack.into(clazz); + } + + @Override + public HackedClass into(final String class_name) { + return Hack.into(class_name); + } + } : new ConditionalHack() { + @SuppressWarnings("unchecked") + @Override + public HackedClass into(@NonNull final Class clazz) { + return (HackedClass) FALLBACK; + } + + @SuppressWarnings("unchecked") + @Override + public HackedClass into(final String class_name) { + return (HackedClass) FALLBACK; + } + }; + } + + private static void fail(final AssertionException e) { + if (sFailureHandler != null) sFailureHandler.onAssertionFailure(e); + } + + /** + * Specify a handler to deal with assertion failure, and decide whether the failure should be thrown. + */ + public static AssertionFailureHandler setAssertionFailureHandler(final AssertionFailureHandler handler) { + final AssertionFailureHandler old = sFailureHandler; + sFailureHandler = handler; + return old; + } + + /** + * Use {@link Hack#setAssertionFailureHandler(AssertionFailureHandler) } to set the global handler + */ + public interface AssertionFailureHandler { + void onAssertionFailure(AssertionException failure); + } + + public interface HackedField { + T get(C instance); + + void set(C instance, T value); + + HackedTargetField on(C target); + + Class getType(); + + boolean isAbsent(); + } + + public interface HackedTargetField { + T get(); + + void set(T value); + + Class getType(); + + boolean isAbsent(); + } + + public interface HackedInvokable { + @CheckResult + HackedInvokable throwing(Class type); + + @CheckResult + HackedInvokable throwing(Class type1, Class type2); + + @CheckResult + HackedInvokable throwing(Class type1, Class type2, Class type3); + + @Nullable + HackedMethod0 withoutParams(); + + @Nullable + HackedMethod1 withParam(Class type); + + @Nullable + HackedMethod2 withParams(Class type1, Class type2); + + @Nullable + HackedMethod3 withParams(Class type1, Class type2, Class type3); + + @Nullable + HackedMethod4 withParams(Class type1, Class type2, Class type3, Class type4); + + @Nullable + HackedMethod5 withParams(Class type1, final Class type2, final Class type3, final Class type4, final Class type5); + + @Nullable + HackedMethodN withParams(Class... types); + } + + public interface NonNullHackedInvokable extends HackedInvokable { + @CheckResult + NonNullHackedInvokable throwing(Class type); + + @CheckResult + NonNullHackedInvokable throwing(Class type1, Class type2); + + @CheckResult + NonNullHackedInvokable throwing(Class type1, Class type2, Class type3); + + @NonNull + HackedMethod0 withoutParams(); + + @NonNull + HackedMethod1 withParam(Class type); + + @NonNull + HackedMethod2 withParams(Class type1, Class type2); + + @NonNull + HackedMethod3 withParams(Class type1, Class type2, Class type3); + + @NonNull + HackedMethod4 withParams(Class type1, Class type2, Class type3, Class type4); + + @NonNull + HackedMethod5 withParams(Class type1, final Class type2, final Class type3, final Class type4, final Class type5); + + @NonNull + HackedMethodN withParams(Class... types); + } + + public interface HackedMethod extends HackedInvokable { + /** + * Optional + */ + @CheckResult + HackedMethod returning(Class type); + + /** + * Fallback to the given value if this field is unavailable at runtime. (Optional) + */ + @CheckResult + NonNullHackedMethod fallbackReturning(R return_value); + + @CheckResult + HackedMethod throwing(Class type); + + @CheckResult + HackedMethod throwing(Class type1, Class type2); + + @CheckResult + HackedMethod throwing(Class type1, Class type2, Class type3); + + @CheckResult + HackedMethod throwing(Class... types); + } + + @SuppressWarnings("NullableProblems") // Force to NonNull + public interface NonNullHackedMethod extends HackedMethod, NonNullHackedInvokable { + /** + * Optional + */ + @CheckResult + HackedMethod returning(Class type); + + @CheckResult + NonNullHackedMethod throwing(Class type); + + @CheckResult + NonNullHackedMethod throwing(Class type1, Class type2); + + @CheckResult + NonNullHackedMethod throwing(Class type1, Class type2, Class type3); + } + + interface Invokable { + Object invoke(C target, Object[] args) throws InvocationTargetException, IllegalAccessException, InstantiationException; + + Class getReturnType(); + } + + public interface Hacking { + T hack(); + } + + public interface ConditionalHack { + /** + * WARNING: Never use this method if the target class may not exist when the condition is not met, use {@link #onlyIf(boolean, Hacking)} instead. + */ + HackedClass into(final @NonNull Class clazz); + + HackedClass into(final String class_name); + } + + private static class $ { + } + + public static class AssertionException extends Throwable { + + private static final long serialVersionUID = 1L; + private Class mClass; + private Field mHackedField; + private Method mHackedMethod; + private String mHackedFieldName; + private String mHackedMethodName; + private Class[] mParamTypes; + + AssertionException(final String e) { + super(e); + } + + AssertionException(final Exception e) { + super(e); + } + + @Override + public String toString() { + return getCause() != null ? getClass().getName() + ": " + getCause() : super.toString(); + } + + public String getDebugInfo() { + final StringBuilder info = new StringBuilder(getCause() != null ? getCause().toString() : super.toString()); + final Throwable cause = getCause(); + if (cause instanceof NoSuchMethodException) { + info.append(" Potential candidates:"); + final int initial_length = info.length(); + final String name = getHackedMethodName(); + if (name != null) { + for (final Method method : getHackedClass().getDeclaredMethods()) + if (method.getName().equals(name)) // Exact name match + info.append(' ').append(method); + if (info.length() == initial_length) + for (final Method method : getHackedClass().getDeclaredMethods()) + if (method.getName().startsWith(name)) // Name prefix match + info.append(' ').append(method); + if (info.length() == initial_length) + for (final Method method : getHackedClass().getDeclaredMethods()) + if (!method.getName().startsWith("-")) // Dump all but generated methods + info.append(' ').append(method); + } else + for (final Constructor constructor : getHackedClass().getDeclaredConstructors()) + info.append(' ').append(constructor); + } else if (cause instanceof NoSuchFieldException) { + info.append(" Potential candidates:"); + final int initial_length = info.length(); + final String name = getHackedFieldName(); + final Field[] fields = getHackedClass().getDeclaredFields(); + for (final Field field : fields) + if (field.getName().equals(name)) // Exact name match + info.append(' ').append(field); + if (info.length() == initial_length) for (final Field field : fields) + if (field.getName().startsWith(name)) // Name prefix match + info.append(' ').append(field); + if (info.length() == initial_length) for (final Field field : fields) + if (!field.getName().startsWith("$")) // Dump all but generated fields + info.append(' ').append(field); + } + return info.toString(); + } + + public Class getHackedClass() { + return mClass; + } + + AssertionException setHackedClass(final Class hacked_class) { + mClass = hacked_class; + return this; + } + + public Method getHackedMethod() { + return mHackedMethod; + } + + AssertionException setHackedMethod(final Method method) { + mHackedMethod = method; + return this; + } + + public String getHackedMethodName() { + return mHackedMethodName; + } + + AssertionException setHackedMethodName(final String method) { + mHackedMethodName = method; + return this; + } + + public Class[] getParamTypes() { + return mParamTypes; + } + + AssertionException setParamTypes(final Class[] param_types) { + mParamTypes = param_types; + return this; + } + + public Field getHackedField() { + return mHackedField; + } + + AssertionException setHackedField(final Field field) { + mHackedField = field; + return this; + } + + public String getHackedFieldName() { + return mHackedFieldName; + } + + AssertionException setHackedFieldName(final String field) { + mHackedFieldName = field; + return this; + } + } + + public static class FieldToHack { + + protected final Class mClass; + protected final String mName; + protected final int mModifiers; + + /** + * @param modifiers the modifiers this field must have + */ + protected FieldToHack(final Class clazz, final String name, final int modifiers) { + mClass = clazz; + mName = name; + mModifiers = modifiers; + } + + protected @Nullable + Field findField(final @Nullable Class type) { + if (mClass == ANY_TYPE) + return null; // AnyType as a internal indicator for class not found. + Field field = null; + try { + field = mClass.getDeclaredField(mName); + if (Modifier.isStatic(mModifiers) != Modifier.isStatic(field.getModifiers())) { + fail(new AssertionException(field + (Modifier.isStatic(mModifiers) ? " is not static" : " is static")).setHackedFieldName(mName)); + field = null; + } else if (mModifiers > 0 && (field.getModifiers() & mModifiers) != mModifiers) { + fail(new AssertionException(field + " does not match modifiers: " + mModifiers).setHackedFieldName(mName)); + field = null; + } else if (!field.isAccessible()) field.setAccessible(true); + } catch (final NoSuchFieldException e) { + final AssertionException hae = new AssertionException(e); + hae.setHackedClass(mClass); + hae.setHackedFieldName(mName); + fail(hae); + } + + if (type != null && field != null && !type.isAssignableFrom(field.getType())) + fail(new AssertionException(new ClassCastException(field + " is not of type " + type)).setHackedField(field)); + return field; + } + } + + public static class MemberFieldToHack extends FieldToHack { + + /** + * @param modifiers the modifiers this field must have + */ + private MemberFieldToHack(final Class clazz, final String name, final int modifiers) { + super(clazz, name, modifiers); + } + + /** + * Assert the field type. + */ + public @Nullable + HackedField ofType(final Class type) { + return ofType(type, false, null); + } + + public @Nullable + HackedField ofType(final String type_name) { + try { //noinspection unchecked + return ofType((Class) Class.forName(type_name, false, mClass.getClassLoader())); + } catch (final ClassNotFoundException e) { + fail(new AssertionException(e)); + return null; + } + } + + public @NonNull + HackedField fallbackTo(final byte value) { //noinspection ConstantConditions + return ofType(byte.class, true, value); + } + + public @NonNull + HackedField fallbackTo(final char value) { //noinspection ConstantConditions + return ofType(char.class, true, value); + } + + public @NonNull + HackedField fallbackTo(final short value) { //noinspection ConstantConditions + return ofType(short.class, true, value); + } + + public @NonNull + HackedField fallbackTo(final int value) { //noinspection ConstantConditions + return ofType(int.class, true, value); + } + + public @NonNull + HackedField fallbackTo(final long value) { //noinspection ConstantConditions + return ofType(long.class, true, value); + } + + public @NonNull + HackedField fallbackTo(final boolean value) { //noinspection ConstantConditions + return ofType(boolean.class, true, value); + } + + public @NonNull + HackedField fallbackTo(final float value) { //noinspection ConstantConditions + return ofType(float.class, true, value); + } + + public @NonNull + HackedField fallbackTo(final double value) { //noinspection ConstantConditions + return ofType(double.class, true, value); + } + + /** + * Fallback to the given value if this field is unavailable at runtime + */ + public @NonNull + HackedField fallbackTo(final T value) { + @SuppressWarnings("unchecked") final Class type = value == null ? null : (Class) value.getClass(); + //noinspection ConstantConditions + return ofType(type, true, value); + } + + private HackedField ofType(final Class type, final boolean fallback, final T fallback_value) { + final Field field = findField(type); + return field != null ? new HackedFieldImpl(field) : fallback ? new FallbackField(type, fallback_value) : null; + } + } + + public static class StaticFieldToHack extends FieldToHack { + + /** + * @param modifiers the modifiers this field must have + */ + private StaticFieldToHack(final Class clazz, final String name, final int modifiers) { + super(clazz, name, modifiers); + } + + /** + * Assert the field type. + */ + public @Nullable + HackedTargetField ofType(final Class type) { + return ofType(type, false, null); + } + + public @Nullable + HackedTargetField ofType(final String type_name) { + try { //noinspection unchecked + return ofType((Class) Class.forName(type_name, false, mClass.getClassLoader())); + } catch (final ClassNotFoundException e) { + fail(new AssertionException(e)); + return null; + } + } + + /** + * Fallback to the given value if this field is unavailable at runtime + */ + public @NonNull + HackedTargetField fallbackTo(final T value) { + @SuppressWarnings("unchecked") final Class type = value == null ? null : (Class) value.getClass(); + //noinspection ConstantConditions + return ofType(type, true, value); + } + + private HackedTargetField ofType(final Class type, final boolean fallback, final T fallback_value) { + final Field field = findField(type); + return field != null ? new HackedFieldImpl(field).onTarget(null) : fallback ? new FallbackField(type, fallback_value) : null; + } + } + + private static class HackedFieldImpl implements HackedField { + + private final @NonNull + Field mField; + + HackedFieldImpl(final @NonNull Field field) { + mField = field; + } + + @Override + public HackedTargetFieldImpl on(final C target) { + if (target == null) throw new IllegalArgumentException("target is null"); + return onTarget(target); + } + + private HackedTargetFieldImpl onTarget(final @Nullable C target) { + return new HackedTargetFieldImpl<>(mField, target); + } + + /** + * Get current value of this field + */ + @Override + public T get(final C instance) { + try { + @SuppressWarnings("unchecked") final T value = (T) mField.get(instance); + return value; + } catch (final IllegalAccessException e) { + return null; + } // Should never happen + } + + /** + * Set value of this field + * + *

No type enforced here since most type mismatch can be easily tested and exposed early.

+ */ + @Override + public void set(final C instance, final T value) { + try { + mField.set(instance, value); + } catch (final IllegalAccessException ignored) { + } // Should never happen + } + + @Override + @SuppressWarnings("unchecked") + public @Nullable + Class getType() { + return (Class) mField.getType(); + } + + @Override + public boolean isAbsent() { + return false; + } + + public @Nullable + Field getField() { + return mField; + } + } + + private static class FallbackField implements HackedField, HackedTargetField { + + private final Class mType; + private final T mValue; + + private FallbackField(final Class type, final T value) { + mType = type; + mValue = value; + } + + @Override + public T get(final C instance) { + return mValue; + } + + @Override + public void set(final C instance, final T value) { + } + + @Override + public T get() { + return mValue; + } + + @Override + public void set(final T value) { + } + + @Override + public HackedTargetField on(final C target) { + return this; + } + + @Override + public Class getType() { + return mType; + } + + @Override + public boolean isAbsent() { + return true; + } + } + + public static class HackedTargetFieldImpl implements HackedTargetField { + + private final Field mField; + private final Object mInstance; // Instance type is already checked + private @Nullable + T mFallbackValue; + + HackedTargetFieldImpl(final Field field, final @Nullable Object instance) { + mField = field; + mInstance = instance; + } + + @Override + public T get() { + if (mField == null) return mFallbackValue; + try { + @SuppressWarnings("unchecked") final T value = (T) mField.get(mInstance); + return value; + } catch (final IllegalAccessException e) { + return null; + } // Should never happen + } + + @Override + public void set(final T value) { + if (mField != null) try { + mField.set(mInstance, value); + } catch (final IllegalAccessException ignored) { + } // Should never happen + } + + @Override + @SuppressWarnings("unchecked") + public @Nullable + Class getType() { + return (Class) mField.getType(); + } + + @Override + public boolean isAbsent() { + return mField == null; + } + } + + public static class CheckedHackedMethod { + + private final Invokable mInvokable; + + CheckedHackedMethod(final Invokable invokable) { + mInvokable = invokable; + } + + @SuppressWarnings("unchecked") + public Class getReturnType() { + return (Class) mInvokable.getReturnType(); + } + + protected HackInvocation invoke(final Object... args) { + return new HackInvocation<>(mInvokable, args); + } + + /** + * Whether this hack is absent, thus will be fallen-back when invoked + */ + public boolean isAbsent() { + return mInvokable instanceof FallbackInvokable; + } + } + + public static class HackedMethod0 extends CheckedHackedMethod { + HackedMethod0(final Invokable invokable) { + super(invokable); + } + + public @CheckResult + HackInvocation invoke() { + return super.invoke(); + } + } + + public static class HackedMethod1 extends CheckedHackedMethod { + HackedMethod1(final Invokable invokable) { + super(invokable); + } + + public @CheckResult + HackInvocation invoke(final A1 arg) { + return super.invoke(arg); + } + } + + public static class HackedMethod2 extends CheckedHackedMethod { + HackedMethod2(final Invokable invokable) { + super(invokable); + } + + public @CheckResult + HackInvocation invoke(final A1 arg1, final A2 arg2) { + return super.invoke(arg1, arg2); + } + } + + public static class HackedMethod3 extends CheckedHackedMethod { + HackedMethod3(final Invokable invokable) { + super(invokable); + } + + public @CheckResult + HackInvocation invoke(final A1 arg1, final A2 arg2, final A3 arg3) { + return super.invoke(arg1, arg2, arg3); + } + } + + public static class HackedMethod4 extends CheckedHackedMethod { + HackedMethod4(final Invokable invokable) { + super(invokable); + } + + public @CheckResult + HackInvocation invoke(final A1 arg1, final A2 arg2, final A3 arg3, final A4 arg4) { + return super.invoke(arg1, arg2, arg3, arg4); + } + } + + public static class HackedMethod5 extends CheckedHackedMethod { + HackedMethod5(final Invokable invokable) { + super(invokable); + } + + public @CheckResult + HackInvocation invoke(final A1 arg1, final A2 arg2, final A3 arg3, final A4 arg4, final A5 arg5) { + return super.invoke(arg1, arg2, arg3, arg4, arg5); + } + } + + public static class HackedMethodN extends CheckedHackedMethod { + HackedMethodN(final Invokable invokable) { + super(invokable); + } + + public @CheckResult + HackInvocation invoke(final Object... args) { + return super.invoke(args); + } + } + + public static class HackInvocation { + + private final Invokable invokable; + private final Object[] args; + + HackInvocation(final Invokable invokable, final Object... args) { + this.invokable = invokable; + this.args = args; + } + + public R on(final @NonNull C target) throws T1, T2, T3 { + return onTarget(target); + } + + public R statically() throws T1, T2, T3 { + return onTarget(null); + } + + private R onTarget(final C target) throws T1 { //noinspection TryWithIdenticalCatches + try { + @SuppressWarnings("unchecked") final R result = (R) invokable.invoke(target, args); + return result; + } catch (final IllegalAccessException e) { + throw new RuntimeException(e); // Should never happen + } catch (final InstantiationException e) { + throw new RuntimeException(e); + } catch (final InvocationTargetException e) { + final Throwable ex = e.getTargetException(); + //noinspection unchecked + throw (T1) ex; + } + } + } + + private static class HackedMethodImpl implements NonNullHackedMethod { + + private static final Comparator CLASS_COMPARATOR = new Comparator() { + @Override + public int compare(final Class lhs, final Class rhs) { + return lhs.toString().compareTo(rhs.toString()); + } + + @Override + public boolean equals(final Object object) { + return this == object; + } + }; + private final Class mClass; + private final @Nullable + String mName; // Null for constructor + private final int mModifiers; + private Class mReturnType; + private Class[] mThrowTypes; + private R mFallbackReturnValue; + private boolean mHasFallback; + + HackedMethodImpl(final Class clazz, @Nullable final String name, final int modifiers) { + //noinspection unchecked, to be compatible with HackedClass.staticMethod() + mClass = (Class) clazz; + mName = name; + mModifiers = modifiers; + } + + @Override + public HackedMethod returning(final Class type) { + mReturnType = type; + @SuppressWarnings("unchecked") final HackedMethod casted = (HackedMethod) this; + return casted; + } + + @Override + public NonNullHackedMethod fallbackReturning(final R value) { + mFallbackReturnValue = value; + mHasFallback = true; + return this; + } + + @Override + public NonNullHackedMethod throwing(final Class type) { + mThrowTypes = new Class[]{type}; + @SuppressWarnings("unchecked") final NonNullHackedMethod casted = (NonNullHackedMethod) this; + return casted; + } + + @Override + public NonNullHackedMethod + throwing(final Class type1, final Class type2) { + mThrowTypes = new Class[]{type1, type2}; + Arrays.sort(mThrowTypes, CLASS_COMPARATOR); + @SuppressWarnings("unchecked") final NonNullHackedMethod cast = (NonNullHackedMethod) this; + return cast; + } + + @Override + public NonNullHackedMethod + throwing(final Class type1, final Class type2, final Class type3) { + mThrowTypes = new Class[]{type1, type2, type3}; + Arrays.sort(mThrowTypes, CLASS_COMPARATOR); + @SuppressWarnings("unchecked") final NonNullHackedMethod cast = (NonNullHackedMethod) this; + return cast; + } + + @Override + public HackedMethod throwing(final Class... types) { + mThrowTypes = types; + Arrays.sort(mThrowTypes, CLASS_COMPARATOR); + @SuppressWarnings("unchecked") final HackedMethod cast = (HackedMethod) this; + return cast; + } + + @NonNull + @SuppressWarnings("ConstantConditions") + @Override + public HackedMethod0 withoutParams() { + final Invokable invokable = findInvokable(); + return invokable == null ? null : new HackedMethod0(invokable); + } + + @NonNull + @SuppressWarnings("ConstantConditions") + @Override + public HackedMethod1 withParam(final Class type) { + final Invokable invokable = findInvokable(type); + return invokable == null ? null : new HackedMethod1(invokable); + } + + @NonNull + @SuppressWarnings("ConstantConditions") + @Override + public HackedMethod2 withParams(final Class type1, final Class type2) { + final Invokable invokable = findInvokable(type1, type2); + return invokable == null ? null : new HackedMethod2(invokable); + } + + @NonNull + @SuppressWarnings("ConstantConditions") + @Override + public HackedMethod3 withParams(final Class type1, final Class type2, final Class type3) { + final Invokable invokable = findInvokable(type1, type2, type3); + return invokable == null ? null : new HackedMethod3(invokable); + } + + @NonNull + @SuppressWarnings("ConstantConditions") + @Override + public HackedMethod4 withParams(final Class type1, final Class type2, final Class type3, final Class type4) { + final Invokable invokable = findInvokable(type1, type2, type3, type4); + return invokable == null ? null : new HackedMethod4(invokable); + } + + @NonNull + @SuppressWarnings("ConstantConditions") + @Override + public HackedMethod5 withParams(final Class type1, final Class type2, final Class type3, final Class type4, final Class type5) { + final Invokable invokable = findInvokable(type1, type2, type3, type4, type5); + return invokable == null ? null : new HackedMethod5(invokable); + } + + @NonNull + @SuppressWarnings("ConstantConditions") + @Override + public HackedMethodN withParams(final Class... types) { + final Invokable invokable = findInvokable(types); + return invokable == null ? null : new HackedMethodN(invokable); + } + + private @Nullable + Invokable findInvokable(final Class... param_types) { + if (mClass == ANY_TYPE) // AnyType as a internal indicator for class not found. + return mHasFallback ? new FallbackInvokable(mFallbackReturnValue) : null; + + final int modifiers; + Invokable invokable; + final AccessibleObject accessible; + final Class[] ex_types; + try { + if (mName != null) { + final Method candidate = mClass.getDeclaredMethod(mName, param_types); + Method method = candidate; + ex_types = candidate.getExceptionTypes(); + modifiers = method.getModifiers(); + if (Modifier.isStatic(mModifiers) != Modifier.isStatic(candidate.getModifiers())) { + fail(new AssertionException(candidate + (Modifier.isStatic(mModifiers) ? " is not static" : "is static")).setHackedMethod(method)); + method = null; + } + if (mReturnType != null && mReturnType != ANY_TYPE && !candidate.getReturnType().equals(mReturnType)) { + fail(new AssertionException("Return type mismatch: " + candidate)); + method = null; + } + if (method != null) { + invokable = new InvokableMethod<>(method); + accessible = method; + } else { + invokable = null; + accessible = null; + } + } else { + final Constructor ctor = mClass.getDeclaredConstructor(param_types); + modifiers = ctor.getModifiers(); + invokable = new InvokableConstructor<>(ctor); + accessible = ctor; + ex_types = ctor.getExceptionTypes(); + } + } catch (final NoSuchMethodException e) { + fail(new AssertionException(e).setHackedClass(mClass).setHackedMethodName(mName).setParamTypes(param_types)); + return mHasFallback ? new FallbackInvokable(mFallbackReturnValue) : null; + } + + if (mModifiers > 0 && (modifiers & mModifiers) != mModifiers) + fail(new AssertionException(invokable + " does not match modifiers: " + mModifiers).setHackedMethodName(mName)); + + if (mThrowTypes == null && ex_types.length > 0 || mThrowTypes != null && ex_types.length == 0) { + fail(new AssertionException("Checked exception(s) not match: " + invokable)); + if (ex_types.length > 0) + invokable = null; // No need to fall-back if expected checked exceptions are absent. + } else if (mThrowTypes != null) { + Arrays.sort(ex_types, CLASS_COMPARATOR); + if (!Arrays.equals(ex_types, mThrowTypes)) { // TODO: Check derived relation of exceptions + fail(new AssertionException("Checked exception(s) not match: " + invokable)); + invokable = null; + } + } + + if (invokable == null) { + if (!mHasFallback) return null; + return new FallbackInvokable<>(mFallbackReturnValue); + } + + if (!accessible.isAccessible()) accessible.setAccessible(true); + return invokable; + } + } + + private static class InvokableMethod implements Invokable { + + private final Method method; + + InvokableMethod(final Method method) { + this.method = method; + } + + public Object invoke(final C target, final Object[] args) throws IllegalAccessException, + IllegalArgumentException, InvocationTargetException { + return method.invoke(target, args); + } + + @Override + public Class getReturnType() { + return method.getReturnType(); + } + + @Override + public String toString() { + return method.toString(); + } + } + + private static class InvokableConstructor implements Invokable { + + private final Constructor constructor; + + InvokableConstructor(final Constructor method) { + this.constructor = method; + } + + public Object invoke(final C target, final Object[] args) throws InstantiationException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return constructor.newInstance(args); + } + + @Override + public Class getReturnType() { + return constructor.getDeclaringClass(); + } + + @Override + public String toString() { + return constructor.toString(); + } + } + + private static class FallbackInvokable implements Invokable { + + private final @Nullable + Object mValue; + + FallbackInvokable(final @Nullable Object value) { + mValue = value; + } + + @Override + public Object invoke(final C target, final Object[] args) throws InvocationTargetException, IllegalAccessException, InstantiationException { + return mValue; + } + + @Override + public Class getReturnType() { + return mValue == null ? Object.class : mValue.getClass(); + } + } + + public static class HackedClass { + + private final Class mClass; + + HackedClass(final Class clazz) { + mClass = clazz; + } + + public @CheckResult + MemberFieldToHack field(final @NonNull String name) { + return new MemberFieldToHack<>(mClass, name, 0); + } + + public @CheckResult + StaticFieldToHack staticField(final @NonNull String name) { + return new StaticFieldToHack<>(mClass, name, Modifier.STATIC); + } + + public @CheckResult + NonNullHackedMethod method(final String name) { + return new HackedMethodImpl<>(mClass, name, 0); + } + + public @CheckResult + NonNullHackedMethod staticMethod(final String name) { + return new HackedMethodImpl<>(mClass, name, Modifier.STATIC); + } + + public @CheckResult + NonNullHackedInvokable constructor() { + final HackedMethodImpl constructor = new HackedMethodImpl<>(mClass, null, 0); + constructor.fallbackReturning(null); // Always fallback to null. + return constructor; + } + } + + /** + * This is a simple demo for the common usage of {@link Hack} + */ + @SuppressWarnings("unused") + private static class Demo { + + static String sField; + boolean mField; + + Demo(final int flags) { + } + + static void demo() { + final Demo demo = Hacks.Demo_ctor.invoke(0).statically(); + try { + Hacks.Demo_methodThrows.invoke().on(demo); + } catch (final InterruptedException | IOException e) { // The checked exceptions declared by throwing() in hack definition. + e.printStackTrace(); + } + Hacks.Demo_staticMethod.invoke(1, "xx").statically(); + } + + static boolean staticMethod(final int a, final String c) { + return false; + } + + private void methodThrows() throws InterruptedException, IOException { + } + + @SuppressWarnings({"FieldCanBeLocal", "UnnecessarilyQualifiedStaticUsage"}) + static class Hacks { + + static HackedMethod1 Demo_ctor; + static HackedMethod0 Demo_methodThrows; + static HackedMethod2 Demo_staticMethod; + static @Nullable + HackedField Demo_mField; // Optional hack may be null if assertion failed + static @Nullable + HackedTargetField Demo_sField; + + static { + Hack.setAssertionFailureHandler(new AssertionFailureHandler() { + @Override + public void onAssertionFailure(final AssertionException failure) { + Log.w("Demo", "Partially incompatible: " + failure.getDebugInfo()); + // Report the incompatibility silently. + //... + } + }); + Demo_ctor = Hack.into(Demo.class).constructor().withParam(int.class); + // Method without fallback (will be null if absent) + Demo_methodThrows = Hack.into(Demo.class).method("methodThrows").returning(Void.class).fallbackReturning(null) + .throwing(InterruptedException.class, IOException.class).withoutParams(); + // Method with fallback (will never be null) + Demo_staticMethod = Hack.into(Demo.class).staticMethod("methodWith2Params").returning(boolean.class) + .fallbackReturning(false).withParams(int.class, String.class); + Demo_mField = Hack.into(Demo.class).field("mField").fallbackTo(false); + Demo_sField = Hack.into(Demo.class).staticField("sField").ofType(String.class); + } + } + } + + /** + * Placeholder for unchecked exception + */ + public class Unchecked extends RuntimeException { + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/light/Light.java b/app/src/main/java/org/meowcat/edxposed/manager/util/light/Light.java new file mode 100644 index 00000000..40bb0f88 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/light/Light.java @@ -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; + } + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b0fd8790..d07c9514 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -78,7 +78,7 @@ android:foreground="?attr/selectableItemBackground" app:cardBackgroundColor="#4CAF50" app:cardCornerRadius="8dp" - app:cardElevation="8dp" + app:cardElevation="4dp" app:cardPreventCornerOverlap="false"> @style/Widget.MaterialComponents.TabLayout.Colored - -