new way of color modification

This commit is contained in:
Vologhat
2023-02-19 15:31:50 +03:00
parent c9eac43919
commit 314e4de27d
86 changed files with 6007 additions and 1185 deletions
@@ -23,8 +23,8 @@ public class OEMDetector {
static {
try {
getPropMtd = ReflectionUtils.getDeclaredMethod("android.os.SystemProperties", "get", String.class);
semPlatformIntField = ReflectionUtils.getDeclaredField(Build.VERSION.class, "SEM_PLATFORM_INT");
getPropMtd = ReflectionUtils.findMethod(Class.forName("android.os.SystemProperties"), "get", String.class);
semPlatformIntField = ReflectionUtils.findField(Build.VERSION.class, "SEM_PLATFORM_INT");
} catch (NoSuchFieldException | ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
}
@@ -89,9 +89,8 @@ public class DockBarInjector {
for (DockBarTab tab : sManager.getSelectedTabs()) {
MenuItem add = menu.add(0, tab.id, 0, tab.titleID);
Drawable drawable = getResources().getDrawable(tab.iconID);
add.setIcon(new RecoloredDrawable(drawable, getCSTDock()));
add.setIcon(new RecoloredDrawable(getResources().getDrawable(tab.iconID), getCSTDock()));
add.setTitle(getGlobalContext().getString(tab.titleID));
add.setCheckable(true);
}
@@ -1,14 +1,7 @@
package ru.vtosters.lite.hooks;
import static ru.vtosters.lite.ui.dialogs.ServerDialog.sendRequest;
import static ru.vtosters.lite.utils.CacheUtils.getInstance;
import static ru.vtosters.lite.utils.NewsFeedFiltersUtils.setupFilters;
import static ru.vtosters.lite.utils.Preferences.checkupdates;
import static ru.vtosters.lite.utils.ThemesUtils.setNeededTheme;
import android.app.Activity;
import android.os.Build;
import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.downloaders.notifications.NotificationChannels;
import ru.vtosters.lite.ssfs.UsersList;
@@ -18,6 +11,12 @@ import ru.vtosters.lite.ui.dialogs.OTADialog;
import ru.vtosters.lite.ui.dialogs.Start;
import ru.vtosters.lite.utils.DeletedMessagesHandler;
import static ru.vtosters.lite.ui.dialogs.ServerDialog.sendRequest;
import static ru.vtosters.lite.utils.CacheUtils.getInstance;
import static ru.vtosters.lite.utils.NewsFeedFiltersUtils.setupFilters;
import static ru.vtosters.lite.utils.Preferences.checkupdates;
import static ru.vtosters.lite.utils.ThemesUtils.setNeededTheme;
public class MainActivityInjector {
public static void inject(Activity activity) {
setNeededTheme(activity);
@@ -82,7 +82,7 @@ public class M3UDownloader implements ITrackDownloader {
String key = IOUtils.readAllLines(IOUtils.openStream(ts.getKeyURL()));
content = IOUtils.decodeStream(is, key);
} else {
content = IOUtils.readAllBytes(is);
content = IOUtils.readFully(is);
}
File tsDump = new File(tsesDir, ts.getName());
IOUtils.writeToFile(tsDump, content);
@@ -36,7 +36,7 @@ public class TSMerger {
for (var file : tsesList) {
if (file.getName().endsWith(".ts")) {
try {
mergedByteArrayStream.write(IOUtils.readAllBytes(file));
mergedByteArrayStream.write(IOUtils.readFully(file));
} catch (IOException e) {
e.printStackTrace();
return false;
@@ -41,7 +41,7 @@ public class ThumbnailDownloader {
private static void downloadThumbnail(String url, int res, String trackId)
throws IOException {
var data = IOUtils.readAllBytes(new URL(url).openStream());
var data = IOUtils.readFully(new URL(url).openStream());
IOUtils.writeToFile(FileCacheImplementation.getTrackThumbnail(trackId, res), data);
}
}
@@ -152,39 +152,6 @@ public class ThemesCore {
themedColors.put(R.attr.im_bubble_outgoing_highlighted, accentColor);
}
@SuppressLint("NonConstantResourceId")
public static int getThemedAttr(Context context, @IdRes int attr) {
return switch (attr) {
case R.attr.im_bubble_wallpaper_outgoing, R.attr.im_bubble_outgoing -> outgoinging_msg;
case R.attr.im_bubble_outgoing_highlighted -> outgoinging_msg_highlight;
case R.attr.newsfeed_post_title_color -> darken_color;
default -> themedColors.get(attr);
};
}
public static boolean hasThemedAttr(Context context, int attrID) {
// try {
// Log.d(TAG, "Requesting color by attr " + context.getResources().getResourceName(attrID));
// } catch (Exception e) {
// }
if (isCachedAccents()) {
if (ThemesUtils.isDarkTheme()) {
return (themedColors.get(attrID) != 0 && ACCENT_THEME_ONLY_DARK.get(attrID, true) && (
ThemesUtils.isMilkshake() ?
ACCENT_THEME_ONLY_MILK_DARK.get(attrID, true) :
ACCENT_THEME_ONLY_NOMILK_DARK.get(attrID, true)));
} else {
return (themedColors.get(attrID) != 0 && ACCENT_THEME_ONLY_LIGHT.get(attrID, true) && (
ThemesUtils.isMilkshake() ?
ACCENT_THEME_ONLY_MILK_LIGHT.get(attrID, true) :
ACCENT_THEME_ONLY_NOMILK_LIGHT.get(attrID, true)));
}
} else {
return themedColors.get(attrID) != 0;
}
}
public static void clear() {
cachedAccents = false;
themedColors.clear();
@@ -1,54 +0,0 @@
package ru.vtosters.lite.themes;
import android.content.Context;
import android.graphics.drawable.*;
import android.os.Build;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import com.vtosters.lite.R;
import ru.vtosters.lite.themes.utils.RecolorUtils;
import ru.vtosters.lite.utils.ThemesUtils;
public class ThemesHacks {
// Фиксит селектор (все/свои/архив) в профиле
public static void fixProfileSelector(View view) {
if (!ThemesUtils.isMilkshake()) return;
var id = view.getId();
if (id == R.id.profile_wall_owner_posts || id == R.id.profile_wall_all_posts || id == R.id.profile_wall_archived_posts) {
StateListDrawable states = (StateListDrawable) ((LayerDrawable) view.getBackground()).getDrawable(0);
LayerDrawable selected = (LayerDrawable) ((DrawableContainer.DrawableContainerState) states.getConstantState()).getChildren()[0];
((ColorDrawable) selected.getDrawable(0)).setColor(ThemesUtils.getAccentColor());
}
}
public static void fixThemedFeed(TextView textView, int i) {
if (i != R.layout.themed_feed_tab_2) {
return;
}
textView.setTextColor(RecolorUtils.recolorCSL(textView.getTextColors()));
((InsetDrawable) ((DrawableContainer.DrawableContainerState) textView.getBackground().getConstantState()).getChildren()[0]).getDrawable().setTint(ThemesUtils.lighten(ThemesUtils.getAccentColor(), 0.15f));
}
public static void fixDropdown(Drawable drawable) {
if (!ThemesUtils.isDarkTheme() && ThemesUtils.isMilkshake() && ThemesCore.isCachedAccents()) {
StateListDrawable stateListDrawable = (StateListDrawable) drawable;
((DrawableContainer.DrawableContainerState) stateListDrawable.getConstantState()).getChildren()[0].setTint(ThemesUtils.getAccentColor());
((DrawableContainer.DrawableContainerState) stateListDrawable.getConstantState()).getChildren()[1].setTint(ThemesUtils.halfAlpha(ThemesUtils.getAccentColor()));
}
}
public static int getHackedColor(@NonNull Context context, @ColorRes int color) {
if (ThemesCore.isCachedAccents() && ColorReferences.isAccentedColor(context.getResources().getColor(color))) {
return ThemesUtils.getAccentColor();
}
if (ThemesCore.isCachedAccents() && ColorReferences.isMutedAccentedColor(context.getResources().getColor(color))) {
return ThemesUtils.getMutedAccentColor();
}
return Build.VERSION.SDK_INT > 23 ? context.getColor(color) : context.getResources().getColor(color);
}
}
@@ -0,0 +1,241 @@
package ru.vtosters.lite.themes;
import android.app.Application;
import android.content.Context;
import android.util.Log;
import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceFile;
import com.vtosters.lite.R;
import ru.vtosters.lite.themes.loaders.ResourcesLoader;
import ru.vtosters.lite.themes.utils.ArscEditor;
import ru.vtosters.lite.utils.IOUtils;
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
public class ThemesManager {
private final static int[] ACCENT_COLORS = {
R.color.vk_sky_300,
R.color.sky_300,
R.color.viewer_retry_button_text_color,
R.color.azure_300,
R.color.picker_camera_button,
R.color.music_check_button_bg_pressed,
R.color.notification_color,
R.color.music_text_counter,
R.color.music_follow_button_bg_pressed,
R.color.music_blue_button_normal,
R.color.music_check_button_bg_normal,
R.color.vk_terms_link,
R.color.vk_blue_200,
R.color.accent_blue,
R.color.azure_A400,
R.color.vk_azure_A400,
R.color.blue,
R.color.blue_200,
R.color.vk_blue_300,
R.color.vk_blue_400,
R.color.blue_300,
R.color.blue_400,
R.color.cool_blue,
R.color.cornflower_blue,
R.color.cornflower_blue_two,
R.color.dot_unread,
R.color.fave_promo_btn_pressed,
R.color.header_blue,
R.color.colorAccent,
R.color.live_emoji_butt_hide,
R.color.music_action_button_blue,
R.color.name,
R.color.picker_blue,
R.color.picker_blue_pressed,
R.color.picker_tab_bg_selected,
R.color.picker_tab_text_selected,
R.color.sharing_blue_btn_normal,
R.color.sharing_blue_btn_pressed,
R.color.vk_sky_300,
R.color.vk_blue_A400,
R.color.blue_A400,
R.color.sky_300,
R.color.text_blue,
R.color.tip_background,
R.color.tw__blue_default,
R.color.tw__blue_pressed,
R.color.vkim_msg_sending_ic,
R.color.vkim_playing_drawable_rect,
R.color.vk_white_blue32,
R.color.date_picker_selector,
R.color.post_suggest_blue,
R.color.list_dialog_blue,
R.color.music_tab_bg_normal,
R.color.toolbar_blue_background,
R.color.toolbar_blue_statusBarColorBack,
R.color.blue_200_muted,
R.color.vk_blue_200_muted,
R.color.white_blue32,
R.color.vk_white_blue32,
R.color.header_blue_opacity16,
R.color.black_blue45_alpha10,
R.color.music_check_button_bg_checked,
R.color.vk_black_blue45_alpha10,
R.color.black_blue30_alpha66,
R.color.vk_black_blue30_alpha66,
R.color.attachpicker_item_background,
R.color.black_blue24_alpha8,
R.color.music_placeholder_artist_bg,
R.color.black_blue24_alpha24,
R.color.vk_black_blue24_alpha24,
R.color.azure_100_muted,
R.color.vk_azure_100_muted,
R.color.muted_blue,
R.color.blue_600,
R.color.light_blue,
R.color.vk_blue_600,
R.color.muted_blue_opacity40,
R.color.muted_blue_old,
R.color.darker_blue,
R.color.header_blue_opacity40
};
private final static String[] NAMES = {
"resources.arsc",
"res",
"assets"
};
// Directories
private static File mainDir;
private static File binDir;
private static File palettesDir;
// Files
// Temp archive with resources.arsc and res.zip content for loading resources
private static File resTmpZipFile = null;
// Archive with assets folder content for loading resources
private static File assetsZipFile = null;
// Archive with res folder content
private static File resZipFile = null;
// Resources table file
private static File resourcesArscFile = null;
public static void init(Application app) {
try {
Log.d("ThemesManager", "init paths");
initPaths(app);
if(hasTmpArchive()) {
ResourcesLoader.init(app);
ResourcesLoader.load(app, resTmpZipFile.getAbsolutePath(), assetsZipFile.getAbsolutePath(), false);
}
} catch(Exception e) {
Log.e("ThemesManager", e+"");
}
}
private static void initPaths(Context context) {
mainDir = new File(context.getFilesDir(), "VTLThemes");
if(!mainDir.exists()) mainDir.mkdir();
binDir = new File(mainDir, "bin");
if(!binDir.exists()) mainDir.mkdir();
palettesDir = new File(mainDir, "palettes");
if(!palettesDir.exists()) palettesDir.mkdir();
assetsZipFile = new File(binDir, "assets.zip");
resZipFile = new File(binDir, "res.zip");
resourcesArscFile = new File(binDir, "resources.arsc");
resTmpZipFile = new File(binDir, "res.tmp.zip");
}
public static void generateBins(Context context)
throws IOException {
try(final var apk = new ZipFile(context.getApplicationInfo().sourceDir)) {
for(var filename : NAMES) {
final var bin = new File(binDir, filename);
if(!bin.exists())
if(filename.contains(".")) extractFile(apk, filename, bin);
else extractAndCompressDirectory(apk, filename, new File(binDir, filename + ".zip"));
}
}
}
private static void extractFile(ZipFile apk, String name, File out)
throws IOException {
final var is = apk.getInputStream(apk.getEntry(name));
if(!out.exists()) out.getParentFile().mkdirs();
IOUtils.copy(is, out);
}
private static void extractAndCompressDirectory(ZipFile apk, String dirName, File out)
throws IOException {
final var zos = new ZipOutputStream(new FileOutputStream(out));
try(zos) {
final var entries = apk.entries();
while(entries.hasMoreElements()) {
final var entry = entries.nextElement();
if(!entry.getName().startsWith(dirName + "/")) continue;
zos.putNextEntry(new ZipEntry(entry.getName()));
final var zis = apk.getInputStream(entry);
IOUtils.copy(zis, zos);
zis.close();
zos.closeEntry();
}
}
}
public static boolean hasBins() {
return assetsZipFile.exists() && resZipFile.exists() && resourcesArscFile.exists();
}
public static boolean hasTmpArchive() {
return resTmpZipFile.exists();
}
public static void generateTempResArchive(int accentColor)
throws IOException {
final var resZip = new ZipFile(resZipFile);
final var zos = new ZipOutputStream(new FileOutputStream(resTmpZipFile));
try(zos) {
final var entries = resZip.entries();
while(entries.hasMoreElements()) {
final var entry = entries.nextElement();
final var is = resZip.getInputStream(entry);
zos.putNextEntry(new ZipEntry(entry.getName()));
IOUtils.copy(is, zos);
is.close();
zos.closeEntry();
}
final var arsc = new BinaryResourceFile(IOUtils.readFully(resourcesArscFile));
if(!ArscEditor.changeColors(arsc, ACCENT_COLORS, accentColor)) {
resZip.close();
resTmpZipFile.delete();
throw new IOException("Error while generating res.tmp.res");
}
zos.putNextEntry(new ZipEntry("resources.arsc"));
IOUtils.copy(new ByteArrayInputStream(arsc.toByteArray()), zos);
}
}
public static void deleteBins() {
assetsZipFile.delete();
resZipFile.delete();
resourcesArscFile.delete();
}
public static void deleteTmpArchive() {
resTmpZipFile.delete();
}
}
@@ -1,92 +0,0 @@
package ru.vtosters.lite.themes;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import ru.vtosters.lite.utils.ThemesUtils;
public class VTLResources extends Resources {
private static final String TAG = "VTLResources";
private final Context context;
private static Field typedArrayField;
private static final List<Integer> attributesToTheme = new ArrayList<>();
static {
try {
typedArrayField = TypedArray.class.getDeclaredField("mData");
typedArrayField.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
attributesToTheme.add(16843041);
attributesToTheme.add(android.R.attr.backgroundTint);
attributesToTheme.add(16843173);
}
public VTLResources(Context context, Resources parent) {
super(parent.getAssets(), parent.getDisplayMetrics(), parent.getConfiguration());
this.context = context;
// Log.d(TAG, "VTLResources: init");
}
@Override
public TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
var typedArray = super.obtainAttributes(set, attrs);
var data = getArrayData(typedArray);
for (int i = 0; i < attrs.length; i++) {
try {
int attrID = attrs[i];
if (attributesToTheme.contains(attrID)) {
int type = data[i*6];
int cnt = data[(i*6)+1];
if (type == TypedValue.TYPE_ATTRIBUTE && isAttrThemeable(cnt)) {
data[i*6] = TypedValue.TYPE_INT_COLOR_RGB8;
data[(i*6)+1] = ThemesUtils.getAccentColor();
data[(i*6)+2] = 0; // clear reference content
}
}
} catch (Exception e) {
Log.e(TAG, "TAVzlom failed! (obtainAttributes)");
e.printStackTrace();
}
}
return typedArray;
}
@Override
public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException {
var drawable = super.getDrawable(id, theme);
if (id == com.vtosters.lite.R.drawable.newsfeed_tab_dropdown_16) {
ThemesHacks.fixDropdown(drawable);
}
return drawable;
}
private static boolean isAttrThemeable(int attrID) {
return !ThemesUtils.isDarkTheme() && attrID == com.vtosters.lite.R.attr.button_primary_background;
}
private static int[] getArrayData(TypedArray arr) {
try {
return (int[]) typedArrayField.get(arr);
} catch (IllegalAccessException e) {
Log.e(TAG, "getArrayData: ", e);
e.printStackTrace();
return new int[0];
}
}
}
@@ -1,43 +0,0 @@
package ru.vtosters.lite.themes;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import ru.vtosters.lite.themes.hooks.BaseHook;
import ru.vtosters.lite.themes.hooks.ButtonHook;
import ru.vtosters.lite.themes.hooks.ImageViewHook;
import ru.vtosters.lite.themes.hooks.ProgressWheelHook;
import ru.vtosters.lite.themes.hooks.TabLayoutHook;
import ru.vtosters.lite.themes.hooks.TextViewHook;
import ru.vtosters.lite.themes.hooks.ToolbarHook;
import ru.vtosters.lite.themes.hooks.VKImageViewHook;
public class ViewInjector {
public static ArrayList<BaseHook> hooks = new ArrayList<>();
static {
hooks.add(new ButtonHook());
hooks.add(new ImageViewHook());
hooks.add(new TabLayoutHook());
hooks.add(new TextViewHook());
hooks.add(new ToolbarHook());
hooks.add(new VKImageViewHook());
hooks.add(new ProgressWheelHook());
}
public static View inject(View view, int i, boolean z) {
for (BaseHook hook : hooks) {
hook.inject(view, i, z);
}
if (view instanceof ViewGroup) {
var viewGroup = (ViewGroup) view;
for (int i2 = 0; i2 < viewGroup.getChildCount(); i2++) {
inject(viewGroup.getChildAt(i2), i, false);
}
}
return view;
}
}
@@ -1,7 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.view.View;
public interface BaseHook {
void inject(View view, int i, boolean z);
}
@@ -1,29 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.content.res.ColorStateList;
import android.view.View;
import android.widget.Button;
import ru.vtosters.lite.themes.ColorReferences;
import ru.vtosters.lite.utils.ThemesUtils;
public class ButtonHook implements BaseHook {
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof Button) {
var button = (Button) view;
if (button.getBackgroundTintList() != null && ColorReferences.isMutedAccentedColor(button.getBackgroundTintList())) {
button.setBackgroundTintList(ColorStateList.valueOf(ThemesUtils.getMutedAccentColor()));
}
if (button.getBackgroundTintList() != null && ColorReferences.isAccentedColor(button.getBackgroundTintList())) {
button.setBackgroundTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
if (TextViewHook.isPositiveButton(button) || TextViewHook.isVkUiButton(view)) {
button.setBackgroundTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
}
}
}
@@ -1,24 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.content.res.ColorStateList;
import android.view.View;
import android.widget.ImageView;
import ru.vtosters.lite.themes.ColorReferences;
import ru.vtosters.lite.themes.ThemesCore;
import ru.vtosters.lite.utils.ThemesUtils;
public class ImageViewHook implements BaseHook {
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof ImageView) {
if (ThemesCore.isCachedAccents()) {
var imageView = (ImageView) view;
if (imageView.getImageTintList() == null) return;
if (ColorReferences.isCslNeedToBeThemed(imageView.getImageTintList())) {
imageView.setImageTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
}
}
}
}
@@ -1,21 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.view.View;
import com.pnikosis.materialishprogress.ProgressWheel;
import ru.vtosters.lite.themes.ThemesCore;
import ru.vtosters.lite.utils.ThemesUtils;
public class ProgressWheelHook implements BaseHook {
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof ProgressWheel) {
if (ThemesCore.isCachedAccents()) {
var progressWheel = (ProgressWheel) view;
progressWheel.setBarColor(ThemesUtils.getAccentColor());
}
}
}
}
@@ -1,21 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.content.res.ColorStateList;
import android.view.View;
import com.vk.core.view.VKTabLayout;
import ru.vtosters.lite.utils.ThemesUtils;
public class TabLayoutHook implements BaseHook {
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof VKTabLayout) {
var tabLayout = (VKTabLayout) view;
tabLayout.setSelectedTabIndicatorColor(ThemesUtils.getAccentColor());
tabLayout.setTabRippleColor(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
}
}
@@ -1,136 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.annotation.SuppressLint;
import android.content.res.ColorStateList;
import android.os.Build;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.widget.AppCompatCheckedTextView;
import com.vk.core.extensions.EditTextExt;
import com.vk.core.view.OverlayTextView;
import com.vk.core.view.TintTextView;
import com.vk.core.view.links.LinkedTextView;
import com.vtosters.lite.R;
import ru.vtosters.lite.themes.ColorReferences;
import ru.vtosters.lite.themes.ThemesHacks;
import ru.vtosters.lite.themes.utils.VkUiThemer;
import ru.vtosters.lite.utils.ThemesUtils;
public class TextViewHook implements BaseHook {
private static final String TAG = "TextViewHook";
@SuppressLint("RestrictedApi")
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof TextView) {
var textView = (TextView) view;
if (ColorReferences.isAccentedColor(textView.getCurrentTextColor())) {
textView.setTextColor(ThemesUtils.getAccentColor());
}
if (ColorReferences.isMutedAccentedColor(textView.getCurrentTextColor())) {
textView.setTextColor(ThemesUtils.getMutedAccentColor());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && textView.getCompoundDrawableTintList() != null) {
if (ColorReferences.isAccentedColor(textView.getCompoundDrawableTintList())) {
textView.setCompoundDrawableTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
if (ColorReferences.isMutedAccentedColor(textView.getCompoundDrawableTintList())) {
textView.setCompoundDrawableTintList(ColorStateList.valueOf(ThemesUtils.getMutedAccentColor()));
}
}
if (textView.getLinkTextColors() != null && ColorReferences.isAccentedColor(textView.getLinkTextColors())) {
textView.setLinkTextColor(ThemesUtils.getAccentColor());
}
if (textView.getLinkTextColors() != null && ColorReferences.isMutedAccentedColor(textView.getLinkTextColors())) {
textView.setLinkTextColor(ThemesUtils.getMutedAccentColor());
}
if (isPositiveButton(textView) || isVkUiButton(textView)) {
view.setBackgroundTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
if (view instanceof OverlayTextView) {
if (view.getId() == android.R.id.button1) {
view.setBackground(ThemesUtils.recolorDrawable(view.getBackground()));
}
if (ColorReferences.isAccentedColor(((OverlayTextView) view).getTextColors().getDefaultColor())) {
((OverlayTextView) view).setTextColor(ThemesUtils.getAccentColor());
}
}
if (view instanceof TintTextView) {
if (eligibleForStartTinting((TintTextView) view)) {
((TintTextView) view).setDrawableStartTint(ThemesUtils.getAccentColor());
}
if (eligibleForStartTintingMuted((TintTextView) view)) {
((TintTextView) view).setDrawableStartTint(ThemesUtils.getMutedAccentColor());
}
if (eligibleForTinting(view.getId())) {
((TintTextView) view).setDrawableTint(ThemesUtils.getAccentColor());
}
if (eligibleForTinting(view.getId())) {
((TintTextView) view).setDrawableStartTint(ThemesUtils.getAccentColor());
}
}
if (view instanceof LinkedTextView) {
((LinkedTextView) view).setLinkTextColor(ThemesUtils.getAccentColor());
}
if (view instanceof AppCompatCheckedTextView) {
var check = (AppCompatCheckedTextView) view;
if (ColorReferences.isAccentedColor(check.getCheckMarkTintList())) {
check.setCheckMarkTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
if (ColorReferences.isMutedAccentedColor(check.getCheckMarkTintList())) {
check.setCheckMarkTintList(ColorStateList.valueOf(ThemesUtils.getMutedAccentColor()));
}
}
if (view instanceof EditText) {
ThemesUtils.setCursorColor((EditText) view);
}
ThemesUtils.colorHandles(textView);
VkUiThemer.autoThemeVkuiButtons(textView);
ThemesHacks.fixProfileSelector(textView);
ThemesHacks.fixThemedFeed(textView, i);
}
}
public static boolean eligibleForStartTinting(TintTextView view) {
return ColorReferences.isAccentedColor(view.getTextColors()) || view.getTextColors().getDefaultColor() == ThemesUtils.getAccentColor();
}
public static boolean eligibleForStartTintingMuted(TintTextView view) {
return ColorReferences.isMutedAccentedColor(view.getTextColors()) || view.getTextColors().getDefaultColor() == ThemesUtils.getMutedAccentColor();
}
public static boolean eligibleForTinting(int id) {
return !ThemesUtils.isDarkTheme() && (id == R.id.attach_title || id == R.id.post_retweet_name);
}
public static boolean isPositiveButton(View view) {
return !ThemesUtils.isDarkTheme() && (view.getId() == R.id.positive || view.getId() == R.id.positive_button);
}
public static boolean isVkUiButton(View view) {
return !ThemesUtils.isDarkTheme() && (view.getId() == R.id.empty_button || view.getId() == R.id.button);
}
}
@@ -1,29 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import com.vk.core.drawable.RecoloredDrawable;
import ru.vtosters.lite.utils.ThemesUtils;
public class ToolbarHook implements BaseHook {
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof Toolbar) {
var color = (ThemesUtils.isMilkshake() && !ThemesUtils.isDarkTheme()) ? ThemesUtils.getAccentColor() : ThemesUtils.getHeaderText();
var menu = ((Toolbar) view).getMenu();
for (int j = 0; j < menu.size(); ++j) {
var item = menu.getItem(j);
var icon = item.getIcon();
if (icon != null) {
if (icon instanceof RecoloredDrawable)
((RecoloredDrawable) icon).a(color);
else
icon = new RecoloredDrawable(icon, color);
item.setIcon(icon);
}
}
}
}
}
@@ -1,30 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.view.View;
import com.vk.imageloader.view.VKImageView;
import ru.vtosters.lite.themes.utils.RecolorUtils;
import ru.vtosters.lite.utils.ThemesUtils;
public class VKImageViewHook implements BaseHook {
private static Paint paint = new Paint();
static {
paint.setColorFilter(new PorterDuffColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_IN));
}
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof VKImageView) {
var imageView = (VKImageView) view;
if (imageView.getImageTintList() != null) {
imageView.setImageTintList(RecolorUtils.recolorCSL(imageView.getImageTintList()));
imageView.setColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_IN);
}
}
}
}
@@ -0,0 +1,58 @@
package ru.vtosters.lite.themes.loaders;
import android.content.res.AssetManager;
import ru.vtosters.lite.utils.ReflectionUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class AssetManagerHelper {
// methods
private final static Method getAssetPathMtd;
private final static Method getApkAssetsMtd;
private final static Method addAssetPathMtd;
private final static Method addAssetPathAsSharedLibraryMtd;
private final static Method addOverlayPathMtd;
static {
try {
getAssetPathMtd = ReflectionUtils.findMethod(Class.forName("android.content.res.ApkAssets"), "getAssetPath");
getApkAssetsMtd = ReflectionUtils.findMethod(AssetManager.class, "getApkAssets");
addAssetPathMtd = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPathAsSharedLibraryMtd = AssetManager.class.getMethod("addAssetPathAsSharedLibrary", String.class);
addOverlayPathMtd = AssetManager.class.getMethod("addOverlayPath", String.class);
} catch(ClassNotFoundException | NoSuchMethodException e) {
throw new RuntimeException();
}
}
public static String getApkPath(Object assetManager, String packagename)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Object[] apkAssetsArr = (Object[]) getAssetPathMtd.invoke(assetManager);
// By default, base apk is the last element of array if is not, for execution speed, we should iterate array from the end to the beginning
for(int i = apkAssetsArr.length - 1; i >= 0; i--) {
final String pathname = (String) getApkAssetsMtd.invoke(apkAssetsArr[i]);
if(pathname.contains(packagename)) return pathname;
}
throw new NullPointerException();
}
//region Uses for adding own assets or getting existing cookies
public static int addAssetPath(AssetManager manager, String path)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return (int) addAssetPathMtd.invoke(manager, path);
}
public static int addAssetPathAsSharedLibrary(AssetManager manager, String path)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return (int) addAssetPathAsSharedLibraryMtd.invoke(manager, path);
}
public static int addOverlayPath(AssetManager manager, String path)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return (int) addOverlayPathMtd.invoke(manager, path);
}
//endregion
}
@@ -0,0 +1,380 @@
package ru.vtosters.lite.themes.loaders;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.ArrayMap;
import android.util.Log;
import ru.vtosters.lite.utils.ReflectionUtils;
import java.io.File;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class ResourcesLoader {
private static Collection<WeakReference<Resources>> sResourceReferences = null;
private static Object sCurrentActivityThread = null;
private static AssetManager sAssetManager = null;
// methods
private static Method ensureStringBlocksMtd = null;
// fields
private static Field assetsFld = null;
private static Field resourcesImplFld = null;
private static Field resDirFld = null;
private static Field packagesFld = null;
private static Field resourcePackagesFld = null;
private static Field publicSourceDirField = null;
private static Field stringBlocksFld = null;
private static long storedPatchedResModifiedTime = 0L;
static {
try {
// API version 8 has PackageInfo, 10 has LoadedApk. Needs to check 9
final Class<?> activityThreadClz = Class.forName("android.app.ActivityThread");
Class<?> loadedApkClz;
try {
loadedApkClz = Class.forName("android.app.LoadedApk");
} catch(ClassNotFoundException ignored) {
loadedApkClz = Class.forName("android.app.ActivityThread$PackagenInfo");
}
resDirFld = ReflectionUtils.findField(loadedApkClz, "mResDir");
packagesFld = ReflectionUtils.findField(activityThreadClz, "mPackages");
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1)
resourcePackagesFld = ReflectionUtils.findField(activityThreadClz, "mResourcePackages");
// fix jianGuo pro has private field 'mAssets' with Resource
// try use mResourcesImpl first
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
try {
// N moved the mAssets inside an mResourcesImpl field
resourcesImplFld = ReflectionUtils.findField(Resources.class, "mResourcesImpl");
} catch(Throwable ignored) {
// for safety
assetsFld = ReflectionUtils.findField(Resources.class, "mAssets");
}
else
assetsFld = ReflectionUtils.findField(Resources.class, "mAssets");
try {
publicSourceDirField = ReflectionUtils.findField(ApplicationInfo.class, "publicSourceDir");
} catch (NoSuchFieldException ignored) {
// Ignored.
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
try {
stringBlocksFld = ReflectionUtils.findField(AssetManager.class, "mStringBlocks");
ensureStringBlocksMtd = ReflectionUtils.findMethod(AssetManager.class, "ensureStringBlocks");
} catch (Throwable ignored) {
// Ignored.
}
} catch(Throwable e) {
Log.d("WTF", e+"");
}
}
public static void init(Context context)
throws Exception {
sCurrentActivityThread = ReflectionUtils.getActivityThread(context, null);
//pre-N
// Find the singleton instance of ResourcesManager
final Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
final Method mGetInstance = ReflectionUtils.findMethod(resourcesManagerClass, "getInstance");
final Object resourcesManager = mGetInstance.invoke(null);
try {
Field fMActiveResources = ReflectionUtils.findField(resourcesManagerClass, "mActiveResources");
final ArrayMap<?, WeakReference<Resources>> activeResources19 =
(ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
sResourceReferences = activeResources19.values();
} catch (NoSuchFieldException ignore) {
// N moved the resources to mResourceReferences
final Field mResourceReferences = ReflectionUtils.findField(resourcesManagerClass, "mResourceReferences");
sResourceReferences = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
}
}
public static void load(Context context, String resPath, String assetsPath, boolean reinject)
throws Exception {
final ApplicationInfo appInfo = context.getApplicationInfo();
final Field[] packagesFlds = Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1
? new Field[] { packagesFld, resourcePackagesFld }
: new Field[] { packagesFld };
for(var fld : packagesFlds) {
final var value = fld.get(sCurrentActivityThread);
for(var entry
: ((Map<String, WeakReference<?>>) value).entrySet()) {
final var loadedApk = entry.getValue().get();
if(loadedApk == null) continue;
final var resDirPath = (String) resDirFld.get(loadedApk);
if(appInfo.sourceDir.equals(resDirPath))
resDirFld.set(loadedApk, resPath);
}
}
if (reinject) {
Log.i("ResourcesLoader", "Re-injecting, skip rest logic.");
return;
}
sAssetManager = AssetManager.class.newInstance();
// Create a new AssetManager instance and point it to the resources installed under
if(AssetManagerHelper.addAssetPath(sAssetManager, resPath) == 0
|| AssetManagerHelper.addAssetPath(sAssetManager, assetsPath) == 0)
throw new IllegalStateException("Could not create new AssetManager");
// Add SharedLibraries to AssetManager for resolve system resources not found issue
// This influence SharedLibrary Package ID
if(shouldAddSharedLibraryAssets(appInfo)) {
for(var sharedLibrary : appInfo.sharedLibraryFiles) {
if(sharedLibrary.endsWith(".apk")) continue;
if(AssetManagerHelper.addAssetPathAsSharedLibrary(sAssetManager, sharedLibrary) == 0)
throw new IllegalStateException("AssetManager add SharedLibrary Fail");
Log.i("ResourcesLoader", "addAssetPathAsSharedLibrary " + sharedLibrary);
}
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
if (stringBlocksFld != null && ensureStringBlocksMtd != null) {
stringBlocksFld.set(sAssetManager, null);
ensureStringBlocksMtd.invoke(sAssetManager);
}
for(var wr : sResourceReferences) {
final var resources = wr.get();
if(resources == null) continue;
// Set the AssetManager of the Resources instance to our brand new one
try {
//pre-N
assetsFld.set(resources, sAssetManager);
} catch (Throwable ignore) {
// N
final var resourceImpl = resourcesImplFld.get(resources);
// for Huawei HwResourcesImpl
final Field implAssets = ReflectionUtils.findField(resourceImpl, "mAssets");
implAssets.set(resourceImpl, sAssetManager);
}
clearPreloadTypedArrayIssue(resources);
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
// Handle issues caused by WebView on Android N.
// Issue: On Android N, if an activity contains a webview, when screen rotates
// our resource patch may lost effects.
// for 5.x/6.x, we found Couldn't expand RemoteView for StatusBarNotification Exception
if (Build.VERSION.SDK_INT >= 24) {
try {
if (publicSourceDirField != null) {
publicSourceDirField.set(context.getApplicationInfo(), resPath);
}
} catch (Throwable ignore) {
// Ignored.
}
}
installResourceInsuranceHacks(context, resPath, assetsPath);
}
private static void installResourceInsuranceHacks(Context context, String patchedResApkPath, String patchedAssetsApkPath) {
try {
final Object activityThread = ReflectionUtils.getActivityThread(context, null);
final Field mHField = ReflectionUtils.findField(activityThread, "mH");
final Handler mH = (Handler) mHField.get(activityThread);
final Field mCallbackField = ReflectionUtils.findField(Handler.class, "mCallback");
final Handler.Callback originCallback = (Handler.Callback) mCallbackField.get(mH);
if (!(originCallback instanceof ResourceInsuranceHandlerCallback)) {
final ResourceInsuranceHandlerCallback hackCallback = new ResourceInsuranceHandlerCallback(
context, patchedResApkPath, patchedAssetsApkPath, originCallback, mH.getClass());
mCallbackField.set(mH, hackCallback);
} else {
Log.w("ResourcesLoader", "installResourceInsuranceHacks: already installed, skip rest logic.");
}
} catch (Throwable thr) {
thr.printStackTrace();
}
}
private static final class ResourceInsuranceHandlerCallback implements Handler.Callback {
private static final String LAUNCH_ACTIVITY_LIFECYCLE_ITEM_CLASSNAME = "android.app.servertransaction.LaunchActivityItem";
private final Context mContext;
private final String mPatchResApkPath;
private final String mPatchedAssetsApkPath;
private final Handler.Callback mOriginalCallback;
private final int LAUNCH_ACTIVITY;
private final int RELAUNCH_ACTIVITY;
private final int EXECUTE_TRANSACTION;
private Method mGetCallbacksMethod = null;
private boolean mSkipInterceptExecuteTransaction = false;
ResourceInsuranceHandlerCallback(Context context, String patchResApkPath, String patchedAssetsApkPath, Handler.Callback original, Class<?> hClazz) {
Context appContext = context.getApplicationContext();
mContext = (appContext != null ? appContext : context);
mPatchResApkPath = patchResApkPath;
mPatchedAssetsApkPath = patchedAssetsApkPath;
mOriginalCallback = original;
LAUNCH_ACTIVITY = fetchMessageId(hClazz, "LAUNCH_ACTIVITY", 100);
RELAUNCH_ACTIVITY = fetchMessageId(hClazz, "RELAUNCH_ACTIVITY", 126);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
EXECUTE_TRANSACTION = fetchMessageId(hClazz, "EXECUTE_TRANSACTION ", 159);
} else {
EXECUTE_TRANSACTION = -1;
}
}
private int fetchMessageId(Class<?> hClazz, String name, int defVal) {
int value;
try {
value = ReflectionUtils.findField(hClazz, name).getInt(null);
} catch (Throwable e) {
value = defVal;
}
return value;
}
@Override
public boolean handleMessage(Message msg) {
boolean consume = false;
if (hackMessage(msg)) {
consume = true;
} else if (mOriginalCallback != null) {
consume = mOriginalCallback.handleMessage(msg);
}
return consume;
}
@SuppressWarnings("unchecked")
private boolean hackMessage(Message msg) {
boolean shouldReInjectPatchedResources = false;
if (!isPatchedResModifiedAfterLastLoad(mPatchResApkPath)) {
shouldReInjectPatchedResources = false;
} else {
if (msg.what == LAUNCH_ACTIVITY || msg.what == RELAUNCH_ACTIVITY) {
shouldReInjectPatchedResources = true;
} else if (msg.what == EXECUTE_TRANSACTION) {
do {
if (mSkipInterceptExecuteTransaction) {
break;
}
final Object transaction = msg.obj;
if (transaction == null) {
Log.w("ResourcesLoader", "transaction is null, skip rest insurance logic.");
break;
}
if (mGetCallbacksMethod == null) {
try {
mGetCallbacksMethod = ReflectionUtils.findMethod(transaction, "getCallbacks");
} catch (Throwable ignored) {
// Ignored.
}
}
if (mGetCallbacksMethod == null) {
Log.e("ResourcesLoader", "fail to find getLifecycleStateRequest method, skip rest insurance logic.");
mSkipInterceptExecuteTransaction = true;
break;
}
try {
final List<Object> req = (List<Object>) mGetCallbacksMethod.invoke(transaction);
if (req != null && req.size() > 0) {
final Object cb = req.get(0);
shouldReInjectPatchedResources = cb != null && cb.getClass().getName().equals(LAUNCH_ACTIVITY_LIFECYCLE_ITEM_CLASSNAME);
}
} catch (Throwable ignored) {
Log.e("ResourcesLoader", "fail to call getLifecycleStateRequest method, skip rest insurance logic.");
}
} while (false);
}
}
if (shouldReInjectPatchedResources) {
try {
load(mContext, mPatchResApkPath, mPatchedAssetsApkPath, true);
} catch (Throwable thr) {
thr.printStackTrace();
}
}
return false;
}
}
private static boolean isPatchedResModifiedAfterLastLoad(String patchedResPath) {
long patchedResModifiedTime;
try {
patchedResModifiedTime = new File(patchedResPath).lastModified();
} catch (Throwable thr) {
thr.printStackTrace();
patchedResModifiedTime = 0L;
}
if (patchedResModifiedTime == 0) {
return false;
}
if (patchedResModifiedTime == storedPatchedResModifiedTime) {
return false;
}
return true;
}
private static void recordCurrentPatchedResModifiedTime(String patchedResPath) {
try {
storedPatchedResModifiedTime = new File(patchedResPath).lastModified();
} catch (Throwable thr) {
thr.printStackTrace();
storedPatchedResModifiedTime = 0L;
}
}
/**
* Why must I do these?
* Resource has mTypedArrayPool field, which just like Message Poll to reduce gc
* MiuiResource change TypedArray to MiuiTypedArray, but it get string block from offset instead of assetManager
*/
private static void clearPreloadTypedArrayIssue(Resources resources) {
// Perform this trick not only in Miui system since we can't predict if any other
// manufacturer would do the same modification to Android.
// if (!isMiuiSystem) {
// return;
// }
Log.w("ResourcesLoader", "try to clear typedArray cache!");
// Clear typedArray cache.
try {
final Field typedArrayPoolField = ReflectionUtils.findField(Resources.class, "mTypedArrayPool");
final Object origTypedArrayPool = typedArrayPoolField.get(resources);
final Method acquireMethod = ReflectionUtils.findMethod(origTypedArrayPool, "acquire");
while (true) {
if (acquireMethod.invoke(origTypedArrayPool) == null) {
break;
}
}
} catch (Throwable ignored) {
Log.e("ResourcesLoader", "clearPreloadTypedArrayIssue failed, ignore error: " + ignored);
}
}
private static boolean shouldAddSharedLibraryAssets(ApplicationInfo applicationInfo) {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && applicationInfo != null &&
applicationInfo.sharedLibraryFiles != null;
}
}
@@ -64,7 +64,7 @@ public class PalettesManager {
if (assets == null || assets.length == 0) return false;
for (var asset : assets) {
try {
IOUtils.copyFile(manager.open(ASSETS_DIR + asset), new File(PALETTES_DIR, asset));
IOUtils.copy(manager.open(ASSETS_DIR + asset), new File(PALETTES_DIR, asset));
} catch (IOException e) {
e.printStackTrace();
}
@@ -0,0 +1,37 @@
package ru.vtosters.lite.themes.utils;
import com.google.devrel.gmscore.tools.apk.arsc.*;
public class ArscEditor {
public static boolean changeColors(BinaryResourceFile arsc, int[] colorIds, int color /*int[] colors*/) {
int q = 0;
for(var chunk : arsc.getChunks()) {
final var resourceTableChunk = (ResourceTableChunk) chunk;
for(PackageChunk packageChunk : resourceTableChunk.getPackages()) {
for(TypeSpecChunk typeSpecChunk : packageChunk.getTypeSpecChunks()) {
for(TypeChunk typeChunk : packageChunk.getTypeChunks(typeSpecChunk.getId())) {
if(!"color".equals(typeChunk.getTypeName())) continue;
for(var entry : typeChunk.getEntries().entrySet()) {
if(entry.getValue().isComplex()) continue;
final var id = BinaryResourceIdentifier.create(packageChunk.getId(), typeSpecChunk.getId(), entry.getKey());
for(int i = 0; i < colorIds.length; ++i) {
if(colorIds[i] != id.resId()) continue;
final var value = entry.getValue();
value.updateValue(new BinaryResourceValue(8, value.value().type(), color /*colors[i]*/));
++q;
}
}
}
}
}
}
return q == colorIds.length;
}
}
@@ -12,18 +12,6 @@ import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.ThemesUtils;
public class RecolorUtils {
public static Drawable recolorDrawableToolbar(Drawable drawable) {
if (drawable == null) return null;
return new RecoloredDrawable(drawable, ThemesUtils.getHeaderText());
} // Recolor toolbar drawable to accent color
@SuppressLint("UseCompatLoadingForDrawables")
public static Drawable recolorDrawableInt(int drawable) {
@SuppressLint("UseCompatLoadingForDrawables") Drawable res = AndroidUtils.getResources().getDrawable(drawable);
return new RecoloredDrawable(res, ThemesUtils.getAccentColor());
} // Get res drawable via id and coloring to accent
public static int recolorHexColor(int i){
var accented = ColorReferences.isAccentedColor(i);
@@ -31,12 +19,6 @@ public class RecolorUtils {
return (accented || mutedaccented) ? (accented ? ThemesUtils.getAccentColor() : ThemesUtils.getMutedAccentColor()) : i;
}
public static ColorStateList recolorCSL(ColorStateList colorStateList) {
if (colorStateList == null) return null;
return ColorStateList.valueOf(ThemesUtils.getAccentColor());
} // Recolor ColorStateList to accent color
@SuppressLint("UseCompatLoadingForColorStateLists")
public static ColorStateList themeCSL(Context context, int color) {
if (ColorReferences.isColorRefAccented(color)) {
@@ -1,85 +0,0 @@
package ru.vtosters.lite.themes.utils;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableContainer;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.RippleDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
import android.widget.TextView;
import ru.vtosters.lite.themes.ColorReferences;
import ru.vtosters.lite.utils.ThemesUtils;
public class VkUiThemer {
public static void autoThemeVkuiButtons(TextView view) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;
if (ThemesUtils.isDarkTheme())
return; // no need to theme b/w buttons, well it should skip, but I do it for extra performance
try {
if (!(view.getBackground() instanceof StateListDrawable)) return;
StateListDrawable states = ((StateListDrawable) view.getBackground());
if (states.getConstantState() == null) return;
DrawableContainer.DrawableContainerState dStates = (DrawableContainer.DrawableContainerState) states.getConstantState();
if (dStates.getChildCount() == 2) {
Drawable target = dStates.getChildren()[0];
if (!(target instanceof RippleDrawable)) return;
RippleDrawable ripple = (RippleDrawable) target;
for (int i = 0; i < ripple.getNumberOfLayers(); i++) {
if (ripple.getId(i) == android.R.id.mask) continue;
Drawable currentTarget = ripple.getDrawable(i);
if (!(currentTarget instanceof GradientDrawable)) continue;
//if (TSColorRefs.TS_DEBUG_MODE) Log.d("TSCore", "[tsh] finally, obamium = ["+currentTarget.toString()+"]");
GradientDrawable spd = (GradientDrawable) currentTarget;
if (spd.getShape() != GradientDrawable.RECTANGLE) continue;
// Log.d("TSCore", "[tsh] got a color!");
ColorStateList csl = spd.getColor();
// if (csl != null) {
// Log.d("TSCore", "[tsh] csl2 <def: " + csl.getDefaultColor() + " = " + ThemesUtils.hex(csl.getDefaultColor()) + "> | " + csl);
// }
if (ColorReferences.isAccentedColor(spd.getColor())) {
// Log.d("TSCore", "[tsh] theming CSL");
spd.setTint(ThemesUtils.getAccentColor());
}
}
Drawable draw2 = dStates.getChildren()[0];
if (!(draw2 instanceof GradientDrawable)) return;
GradientDrawable spd = (GradientDrawable) draw2;
if (spd.getShape() != GradientDrawable.RECTANGLE) return;
// Log.d("TSCore", "[tsh:disabled] got a color!");
ColorStateList csl = spd.getColor();
// if (csl != null) {
// Log.d("TSCore", "[tsh:disabled] csl2 <def: " + csl.getDefaultColor() + " = " + ThemesUtils.hex(csl.getDefaultColor()) + "> | " + csl);
// }
if (ColorReferences.isAccentedColor(spd.getColor())) {
// Log.d("TSCore", "[tsh:disabled] theming CSL");
spd.setTint(ThemesUtils.getAccentColor());
}
if (ColorReferences.isMutedAccentedColor(spd.getColor())) {
// Log.d("TSCore", "[tsh:disabled] theming CSL");
spd.setTint(ThemesUtils.getMutedAccentColor());
}
}
} catch (Exception e) {
// This must go wrong, so we ignore any exceptions that may occur
e.printStackTrace(); // REMOVE THIS!
}
}
}
@@ -21,7 +21,6 @@ import androidx.recyclerview.widget.RecyclerView;
import com.vtosters.lite.R;
import ru.vtosters.lite.themes.hooks.TextViewHook;
import ru.vtosters.lite.ui.adapters.CategorizedAdapter;
import ru.vtosters.lite.ui.components.DockBarEditorManager;
import ru.vtosters.lite.ui.components.ItemMovingCallback;
@@ -58,8 +57,6 @@ public class DockBarEditorFragment extends BaseToolbarFragment {
restartApplication();
});
new TextViewHook().inject(save, 0, false);
var saveParams = LayoutUtils.createLinear(0, -2);
saveParams.weight = 1.0f;
buttonsContainer.addView(save, saveParams);
@@ -74,8 +71,6 @@ public class DockBarEditorFragment extends BaseToolbarFragment {
restartApplication();
});
new TextViewHook().inject(reset, 0, false);
var resetParams = LayoutUtils.createLinear(0, -2);
resetParams.weight = 1.0f;
buttonsContainer.addView(reset, resetParams);
@@ -15,7 +15,6 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.vtosters.lite.R;
import ru.vtosters.lite.themes.hooks.TextViewHook;
import ru.vtosters.lite.ui.adapters.CategorizedAdapter;
import ru.vtosters.lite.ui.components.ItemMovingCallback;
import ru.vtosters.lite.ui.components.SuperAppEditorManager;
@@ -67,8 +66,6 @@ public class SuperAppEditorFragment extends BaseToolbarFragment {
restartApplication();
});
new TextViewHook().inject(save, 0, false);
var saveParams = LayoutUtils.createLinear(0, -2);
saveParams.weight = 1.0f;
buttonsContainer.addView(save, saveParams);
@@ -83,8 +80,6 @@ public class SuperAppEditorFragment extends BaseToolbarFragment {
restartApplication();
});
new TextViewHook().inject(reset, 0, false);
var resetParams = LayoutUtils.createLinear(0, -2);
resetParams.weight = 1.0f;
buttonsContainer.addView(reset, resetParams);
@@ -1,18 +1,17 @@
package ru.vtosters.lite.ui.fragments;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.WindowManager;
import androidx.preference.PreferenceCategory;
import b.h.g.k.VKProgressDialog;
import com.vk.core.dialogs.alert.VkAlertDialog;
import com.vk.core.fragments.FragmentImpl;
import com.vk.navigation.Navigator;
import com.vtosters.lite.R;
import com.vtosters.lite.general.fragments.MaterialPreferenceToolbarFragment;
import ru.vtosters.lite.themes.ThemesCore;
import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.themes.ThemesManager;
import ru.vtosters.lite.themes.palettes.PalettesManager;
import ru.vtosters.lite.ui.components.DockBarEditorManager;
import ru.vtosters.lite.ui.dialogs.PalettesBottomSheetDialog;
@@ -20,14 +19,22 @@ import ru.vtosters.lite.ui.views.rarepebble.ColorPickerView;
import ru.vtosters.lite.ui.wallpapers.WallpaperMenuFragment;
import ru.vtosters.lite.utils.*;
import java.io.IOException;
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
import static ru.vtosters.lite.utils.ThemesUtils.recolorDrawable;
public class ThemesFragment extends MaterialPreferenceToolbarFragment {
private VKProgressDialog mProgressDialog;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
mProgressDialog = new VKProgressDialog(requireContext());
mProgressDialog.setCancelable(false);
addPreferencesFromResource(R.xml.preferences_themes);
initPreferences();
}
@@ -47,6 +54,22 @@ public class ThemesFragment extends MaterialPreferenceToolbarFragment {
return true;
});
var invalidateThemeCache = findPreference("invalidate_theme_cache");
invalidateThemeCache.setOnPreferenceClickListener(preference -> {
mProgressDialog.setMessage("Инвалидация кэша...");
mProgressDialog.show();
VTExecutors.getIoExecutor().execute(() -> {
ThemesManager.deleteBins();
try {
ThemesManager.generateBins(requireContext());
requireActivity().runOnUiThread(this::restart);
} catch(IOException e) {
requireActivity().runOnUiThread(() -> showErrorDialog("Ошибка при инвалидации кэша"));
}
});
return false;
});
var navBarPreference = findPreference("navbar");
navBarPreference.setOnPreferenceClickListener(preference -> {
restart();
@@ -147,7 +170,7 @@ public class ThemesFragment extends MaterialPreferenceToolbarFragment {
}
})
.setNegativeButton(R.string.reset, (dialog, which) -> {
ThemesUtils.setCustomAccentColor(0, false);
ThemesManager.deleteTmpArchive();
LifecycleUtils.restartApplicationWithTimer();
})
.setPositiveButton(R.string.cancel, null)
@@ -193,9 +216,47 @@ public class ThemesFragment extends MaterialPreferenceToolbarFragment {
}
void setAccentColor(int color) {
ThemesUtils.setCustomAccentColor(color, false);
ThemesCore.setThemedColors(color);
restart();
String message;
Runnable task;
if(!ThemesManager.hasBins()) {
message = "Генерация файлов...";
task = () -> {
try {
ThemesManager.generateBins(requireContext());
requireActivity().runOnUiThread(() -> setAccentColor(color));
} catch(IOException e) {
Log.e("ThemesFragment", e+"");
ThemesManager.deleteBins();
requireActivity().runOnUiThread(() -> showErrorDialog("Ошибка при генерации файлов"));
}
};
} else {
message = "Применение акцента...";
task = () -> {
try {
ThemesManager.generateTempResArchive(color);
requireActivity().runOnUiThread(this::restart);
} catch(IOException e) {
Log.e("ThemesFragment", e+"");
ThemesManager.deleteTmpArchive();
requireActivity().runOnUiThread(() -> showErrorDialog("Ошибка при применении акцента"));
}
};
}
mProgressDialog.setMessage(message);
mProgressDialog.show();
VTExecutors.getIoExecutor().execute(task);
}
void showErrorDialog(String msg) {
mProgressDialog.dismiss();
new VkAlertDialog.Builder(requireContext())
.setTitle("Ошибка")
.setMessage(msg)
.setPositiveButton("OK", null)
.show();
}
void restart() {
@@ -21,6 +21,7 @@ import com.vk.core.util.ToastUtils;
import com.vtosters.lite.general.fragments.WebViewFragment;
import ru.vtosters.lite.hooks.DateHook;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
@@ -56,7 +57,8 @@ public class AndroidUtils {
@NonNull
public static Context getGlobalContext() {
try {
return ReflectionUtils.invokeStaticMethod("android.app.AppGlobals", "getInitialApplication");
final Method getInitialApplicationMtd = ReflectionUtils.findMethod(Class.forName("android.app.AppGlobals"), "getInitialApplication");
return (Context) getInitialApplicationMtd.invoke(null);
} catch (Exception e) {
Log.d("GlobalContext", "Error while fetching context via refl");
}
@@ -1,11 +1,6 @@
package ru.vtosters.lite.utils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
@@ -24,12 +19,14 @@ import javax.crypto.spec.SecretKeySpec;
public class IOUtils {
public static final int BUFFER_SIZE = 0x800;
public static byte[] decodeStream(InputStream encIs, String keyURL) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IOException {
public static byte[] decodeStream(InputStream encIs, String keyURL)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IOException {
CipherInputStream cip = new CipherInputStream(encIs, getCipher(keyURL));
return readAllBytes(cip);
return readFully(cip);
}
public static Cipher getCipher(String key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
public static Cipher getCipher(String key)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Key keySpec = new SecretKeySpec(key.getBytes(), "AES");
// bypassing ecb
@@ -40,66 +37,72 @@ public class IOUtils {
return cipher;
}
public static InputStream openStream(String url) throws IOException {
public static InputStream openStream(String url)
throws IOException {
URLConnection connection = new URL(url).openConnection();
return connection.getInputStream();
}
public static String readAllLines(File file) throws IOException {
public static String readAllLines(File file)
throws IOException {
return readAllLines(new FileInputStream(file));
}
public static String readAllLines(InputStream is) throws IOException {
return new String(readAllBytes(is), "utf-8");
public static String readAllLines(InputStream is)
throws IOException {
return new String(readFully(is), StandardCharsets.UTF_8);
}
public static byte[] readAllBytes(File file) throws IOException {
return readAllBytes(new FileInputStream(file));
public static byte[] readFully(final InputStream is)
throws IOException {
final var bos = new ByteArrayOutputStream();
try(bos) {
final byte[] buff = new byte[BUFFER_SIZE];
int len;
while((len = is.read(buff)) > 0)
bos.write(buff, 0, len);
return bos.toByteArray();
}
}
public static byte[] readAllBytes(InputStream is) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[BUFFER_SIZE];
int len;
while ((len = is.read(buffer)) > 0)
bos.write(buffer, 0, len);
is.close();
bos.close();
return bos.toByteArray();
public static byte[] readFully(final File in)
throws IOException {
return readFully(new FileInputStream(in));
}
public static void writeToFile(File file, String content) throws IOException {
public static void writeToFile(File file, String content)
throws IOException {
writeToFile(file, content.getBytes(StandardCharsets.UTF_8));
}
public static void writeToFile(File file, byte[] content) throws IOException {
if (!file.exists())
file.createNewFile();
public static void writeToFile(File file, byte[] content)
throws IOException {
var fos = new FileOutputStream(file);
fos.write(content);
fos.close();
}
public static void copyFileToDirectory(File dest, File targetDir) throws IOException {
if (!targetDir.exists())
targetDir.mkdirs();
var path = dest.getAbsolutePath();
var target = new File(targetDir, path.substring(path.lastIndexOf("/")));
copyFile(dest, target);
public static void copy(final InputStream is, final OutputStream os)
throws IOException {
final byte[] buff = new byte[BUFFER_SIZE];
int len;
while((len = is.read(buff)) > 0)
os.write(buff, 0, len);
}
public static void copyFile(File dest, File target) throws IOException {
var parent = target.getParentFile();
if (!parent.exists())
parent.getParentFile().mkdirs();
writeToFile(target, readAllBytes(dest));
public static void copy(final byte[] buffer, final File out)
throws IOException {
copy(new ByteArrayInputStream(buffer), new FileOutputStream(out));
}
public static void copyFile(InputStream is, File target) throws IOException {
var parent = target.getParentFile();
if (!(parent != null ? parent.exists() : false))
parent.getParentFile().mkdirs();
writeToFile(target, readAllBytes(is));
public static void copy(final InputStream is, final File out)
throws IOException {
copy(is, new FileOutputStream(out));
}
public static void copy(final File in, final File out)
throws IOException {
copy(new FileInputStream(in), new FileOutputStream(out));
}
public static void deleteRecursive(File f) {
@@ -1,9 +1,9 @@
package ru.vtosters.lite.utils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import android.content.Context;
import com.vk.im.ui.views.Corners;
import java.lang.reflect.*;
public class ReflectionUtils {
@@ -11,244 +11,91 @@ public class ReflectionUtils {
* Gets a class with a specific name.
* Supports smali type (Lpackage/Class;)
*/
public static Class<?> getClass(String name)
public static Class<?> findClass(String name)
throws ClassNotFoundException {
if (name.contains(";"))
if (name.endsWith(";"))
name = name.substring(1, name.length() - 1).replace("/", ".");
return Class.forName(name);
}
public static Field getDeclaredField(Class<?> clz, String fieldName)
public static Field findField(Class<?> clz, String fieldName)
throws NoSuchFieldException {
Field field = clz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
}
public static Field getDeclaredField(String className, String fieldName)
throws NoSuchFieldException, ClassNotFoundException {
return getDeclaredField(getClass(className), fieldName);
}
public static Field getDeclaredFieldRecursive(Class<?> clz, String fieldName)
throws NoSuchFieldException {
Field field = null;
Class<?> clazz = clz;
while (clazz != null) {
NoSuchFieldException ex = null;
Field fld = null;
for(; clz != null; clz = clz.getSuperclass()) {
try {
field = getDeclaredField(clazz, fieldName);
fld = clz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clz.getSuperclass();
} catch(NoSuchFieldException e) {
ex = e;
}
}
if (field != null)
return field;
throw new NoSuchFieldException(fieldName);
if(fld == null) throw ex;
fld.setAccessible(true);
return fld;
}
public static Field getDeclaredFieldRecursive(String className, String fieldName)
throws ClassNotFoundException, NoSuchFieldException {
return getDeclaredFieldRecursive(getClass(className), fieldName);
public static Field findField(Object instance, String fieldName)
throws NoSuchFieldException {
return findField(instance.getClass(), fieldName);
}
public static Field getField(Class<?> clz, String fieldName)
throws NoSuchFieldException {
Field field = clz.getField(fieldName);
field.setAccessible(true);
return field;
}
public static Field getField(String className, String fieldName)
throws NoSuchFieldException, ClassNotFoundException {
return getField(getClass(className), fieldName);
}
public static Field getFieldRecursive(Class<?> clz, String fieldName)
throws NoSuchFieldException {
Field field = null;
Class<?> clazz = clz;
while (clazz != null) {
public static Constructor<?> findConstructor(Class<?> clz, Class<?>... params)
throws NoSuchMethodException {
Constructor<?> cctor = null;
for(; clz != null; clz = clz.getSuperclass()) {
try {
field = getField(clazz, fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clz.getSuperclass();
cctor = clz.getDeclaredConstructor(params);
} catch(NoSuchMethodException ignored) {
}
}
if (field != null)
return field;
throw new NoSuchFieldException(fieldName);
if(cctor == null) throw new NoSuchMethodException("");
cctor.setAccessible(true);
return cctor;
}
public static Field getFieldRecursive(String className, String fieldName)
throws ClassNotFoundException, NoSuchFieldException {
return getFieldRecursive(getClass(className), fieldName);
public static Constructor<?> findConstructor(Object instance, Class<?>... params)
throws NoSuchMethodException {
return findConstructor(instance.getClass(), params);
}
public static <T> T getObjectField(Class<?> clz, String fieldName, Object instance)
throws NoSuchFieldException, IllegalAccessException {
Field field;
public static Method findMethod(Class<?> clz, String methodName, Class<?>... params)
throws NoSuchMethodException {
Method mtd = null;
for(; clz != null; clz = clz.getSuperclass()) {
try {
mtd = clz.getDeclaredMethod(methodName, params);
} catch(NoSuchMethodException ignored) {
}
}
if(mtd == null) throw new NoSuchMethodException("methodName == " + methodName);
mtd.setAccessible(true);
return mtd;
}
public static Method findMethod(Object instance, String methodName, Class<?>... params)
throws NoSuchMethodException {
return findMethod(instance.getClass(), methodName, params);
}
public static Object getActivityThread(Context context, Class<?> clz)
throws InvocationTargetException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException {
try {
field = getField(clz, fieldName);
} catch (NoSuchFieldException e) {
field = getDeclaredField(clz, fieldName);
if(clz == null) clz = findClass("android.app.ActivityThread");
final Method currentActivityThreadMtd = findMethod(clz, "currentActivityThread");
Object currentActivityThread = currentActivityThreadMtd.invoke(null);
if (currentActivityThread == null && context != null) {
// In older versions of Android (prior to frameworks/base 66a017b63461a22842)
// the currentActivityThread was built on thread locals, so we'll need to try
// even harder
final Field loadedApkFld = findField(context, "mLoadedApk");
final Object apk = loadedApkFld.get(context);
final Field activityThreadFld = findField(apk, "mActivityThread");
currentActivityThread = activityThreadFld.get(apk);
}
return currentActivityThread;
} catch(Throwable th) {
return null;
}
return (T) field.get(instance);
}
public static <T> T getObjectField(String className, String fieldName, Object instance)
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return getObjectField(getClass(className), fieldName, instance);
}
public static void setObjectField(Class<?> clz, String fieldName, Object instance, Object value)
throws NoSuchFieldException, IllegalAccessException {
Field field;
try {
field = getField(clz, fieldName);
} catch (NoSuchFieldException e) {
field = getDeclaredField(clz, fieldName);
}
unfinalField(field);
field.set(instance, value);
}
public static void setObjectField(String className, String fieldName, Object instance, Object value)
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
setObjectField(getClass(className), fieldName, instance, value);
}
public static <T> T getObjectStaticField(Class<?> clz, String fieldName)
throws NoSuchFieldException, IllegalAccessException {
return getObjectField(clz, fieldName, null);
}
public static <T> T getObjectStaticField(String className, String fieldName)
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return getObjectField(getClass(className), fieldName, null);
}
public static void setObjectStaticField(Class<?> clz, String fieldName, Object value)
throws NoSuchFieldException, IllegalAccessException {
setObjectField(clz, fieldName, null, value);
}
public static void setObjectStaticField(String className, String fieldName, Object value)
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
setObjectStaticField(getClass(className), fieldName, value);
}
public static void unfinalField(Field field) throws NoSuchFieldException {
if (!Modifier.isFinal(field.getModifiers())) return;
Field accessFlagsFld = getDeclaredField(Field.class, "accessFlags");
try {
int accessFlags = accessFlagsFld.getInt(field);
accessFlagsFld.setInt(field, ~Modifier.FINAL & accessFlags);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static Method getMethod(Class<?> clz, String methodName, Class<?>... parameterTypes)
throws NoSuchMethodException {
Method method = clz.getMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
}
public static Method getMethod(String className, String methodName, Class<?>... parameterTypes)
throws ClassNotFoundException, NoSuchMethodException {
return getMethod(getClass(className), methodName, parameterTypes);
}
public static Method getMethod(Class<?> clz, String methodName, Object... parameters)
throws NoSuchMethodException {
Class<?>[] parameterTypes = new Class<?>[parameters.length];
for (int i = 0; i < parameters.length; i++) {
parameterTypes[i] = parameters[i].getClass();
}
return getMethod(clz, methodName, parameterTypes);
}
public static Method getMethod(String className, String methodName, Object... parameters)
throws ClassNotFoundException, NoSuchMethodException {
return getMethod(getClass(className), methodName, parameters);
}
public static Method getDeclaredMethod(Class<?> clz, String methodName, Class<?>... parameterTypes)
throws NoSuchMethodException {
Method method = clz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
}
public static Method getDeclaredMethod(String className, String methodName, Class<?>... parameterTypes)
throws ClassNotFoundException, NoSuchMethodException {
return getDeclaredMethod(getClass(className), methodName, parameterTypes);
}
public static Method getDeclaredMethod(Class<?> clz, String methodName, Object... parameters)
throws NoSuchMethodException {
Class<?>[] parameterTypes = new Class<?>[parameters.length];
for (int i = 0; i < parameters.length; i++) {
parameterTypes[i] = parameters[i].getClass();
}
return getDeclaredMethod(clz, methodName, parameterTypes);
}
public static Method getDeclaredMethod(String className, String methodName, Object... parameters)
throws ClassNotFoundException, NoSuchMethodException {
return getDeclaredMethod(getClass(className), methodName, parameters);
}
public static <T> T invokeMethod(Class<?> clz, String methodName, Object instance, Class<?>[] parameterTypes, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method method;
try {
method = getMethod(clz, methodName, parameterTypes);
} catch (NoSuchMethodException e) {
method = getDeclaredMethod(clz, methodName, parameters);
}
return (T) method.invoke(instance, parameters);
}
public static <T> T invokeMethod(String className, String methodName, Object instance, Class<?>[] parameterTypes, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
return invokeMethod(getClass(className), methodName, instance, parameterTypes, parameters);
}
public static <T> T invokeMethod(Class<?> clz, String methodName, Object instance, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?>[] parameterTypes = new Class<?>[parameters.length];
for (int i = 0; i < parameters.length; i++) {
parameterTypes[i] = parameters[i].getClass();
}
return invokeMethod(clz, methodName, instance, parameterTypes, parameters);
}
public static <T> T invokeMethod(String className, String methodName, Object instance, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
return invokeMethod(getClass(className), methodName, instance, parameters);
}
public static <T> T invokeStaticMethod(Class<?> clz, String methodName, Class<?>[] parameterTypes, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return invokeMethod(clz, methodName, null, parameterTypes, parameters);
}
public static <T> T invokeStaticMethod(String className, String methodName, Class<?>[] parameterTypes, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
return invokeStaticMethod(getClass(className), methodName, parameterTypes, parameters);
}
public static <T> T invokeStaticMethod(Class<?> clz, String methodName, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return invokeMethod(clz, methodName, null, parameters);
}
public static <T> T invokeStaticMethod(String className, String methodName, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
return invokeMethod(getClass(className), methodName, null, parameters);
}
}
@@ -1,23 +1,17 @@
package ru.vtosters.lite.utils;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.webkit.WebView;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import com.vk.articles.preload.WebCachePreloader;
import com.vk.core.drawable.RecoloredDrawable;
@@ -27,13 +21,9 @@ import com.vk.core.ui.themes.VKTheme;
import com.vk.core.ui.themes.VKThemeHelper;
import com.vtosters.lite.R;
import com.vtosters.lite.data.ThemeTracker;
import ru.vtosters.lite.deviceinfo.OEMDetector;
import ru.vtosters.lite.hooks.VKUIHook;
import ru.vtosters.lite.themes.ThemesHacks;
import ru.vtosters.lite.ui.wallpapers.WallpapersHooks;
import java.lang.reflect.Field;
import static ru.vtosters.lite.utils.AndroidUtils.*;
import static ru.vtosters.lite.utils.Preferences.*;
@@ -50,14 +40,6 @@ public class ThemesUtils {
return ColorStateList.valueOf(getAccentColor());
}
public static int getSystemModeTheme() {
return AppCompatDelegate.getDefaultNightMode();
}
public static void setSystemModeTheme(int theme) {
AppCompatDelegate.setDefaultNightMode(theme);
}
public static void setTheme(VKTheme theme, Activity activity) {
if (activity == null) {
activity = LifecycleUtils.getCurrentActivity();
@@ -69,17 +51,6 @@ public class ThemesUtils {
WebCachePreloader.e();
} // apply changed theme
public static void setThemeFL(VKTheme theme, Activity activity, float[] fl) {
if (activity == null) {
activity = LifecycleUtils.getCurrentActivity();
}
VKThemeHelper.theme(theme, activity, fl);
ThemeTracker.a();
VKUIHook.isLoaded = false;
new WebView(activity).clearCache(true);
WebCachePreloader.e();
}
public static boolean isDarkTheme() {
return VKThemeHelper.r();
}
@@ -93,111 +64,17 @@ public class ThemesUtils {
}
public static int getAccentColor() {
var accent = AndroidUtils.getPreferences().getInt("accent_color", getColorFromAttr(R.attr.accent));
return (accent == 0 || !isMilkshake()) || isMonetTheme() ? getColorFromAttr(R.attr.accent) : accent;
return getColorFromAttr(R.attr.accent);
} // Color accent
public static int getMutedAccentColor() {
return getMutedColor(getAccentColor());
}
@SuppressLint("DiscouragedPrivateApi")
public static void setCursorColor(EditText view) {
try {
view.setHighlightColor(ThemesUtils.getMutedAccentColor());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
view.getTextCursorDrawable().setColorFilter(new PorterDuffColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_IN));
} else if (!OEMDetector.isMIUI()) {
Field field = TextView.class.getDeclaredField("mCursorDrawableRes");
field.setAccessible(true);
int drawableResId = field.getInt(view);
field = TextView.class.getDeclaredField("mEditor");
field.setAccessible(true);
Object editor = field.get(view);
if (editor != null) {
Drawable drawable = ContextCompat.getDrawable(view.getContext(), drawableResId);
drawable.setColorFilter(getAccentColor(), PorterDuff.Mode.SRC_IN);
Drawable[] drawables = {drawable, drawable};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
field = editor.getClass().getDeclaredField("mCursorDrawable");
} else {
field = editor.getClass().getDeclaredField("mDrawableForCursor");
}
field.setAccessible(true);
field.set(editor, drawables);
}
}
} catch (Exception e) {
Log.e("ThemesUtils", "setCursorColor: ", e);
}
}
@SuppressLint("DiscouragedPrivateApi")
public static void colorHandles(TextView view) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
PorterDuffColorFilter filter = new PorterDuffColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_IN);
view.getTextSelectHandle().setColorFilter(filter);
view.getTextSelectHandleRight().setColorFilter(filter);
view.getTextSelectHandleLeft().setColorFilter(filter);
} else if (!OEMDetector.isMIUI()) {
Field editorField = TextView.class.getDeclaredField("mEditor");
if (!editorField.isAccessible()) editorField.setAccessible(true);
Object editor = editorField.get(view);
if (editor != null) {
Class<?> editorClass;
editorClass = editor.getClass();
String[] handleNames = {"mSelectHandleLeft", "mSelectHandleRight", "mSelectHandleCenter"};
String[] resNames = {"mTextSelectHandleLeftRes", "mTextSelectHandleRightRes", "mTextSelectHandleRes"};
for (int i = 0; i < handleNames.length; i++) {
Field handleField = editorClass.getDeclaredField(handleNames[i]);
if (!handleField.isAccessible()) {
handleField.setAccessible(true);
}
Drawable handleDrawable = (Drawable) handleField.get(editor);
if (handleDrawable == null) {
Field resField = TextView.class.getDeclaredField(resNames[i]);
if (!resField.isAccessible()) {
resField.setAccessible(true);
}
int resId = resField.getInt(view);
handleDrawable = view.getResources().getDrawable(resId);
}
if (handleDrawable != null) {
Drawable drawable = handleDrawable.mutate();
drawable.setColorFilter(getAccentColor(), PorterDuff.Mode.SRC_IN);
handleField.set(editor, drawable);
}
}
}
}
} catch (Exception e) {
Log.e("ThemesUtils", "colorHandles: ", e);
}
}
public static int getMutedColor(int color) {
return ColorUtils.blendARGB(color, (isDarkTheme() ? Color.BLACK : Color.WHITE), (isMilkshake() ? 0.5f : isDarkTheme() ? 0.32f : 0.1f));
}
public static void setCustomAccentColor(int newColor, boolean async) {
var editor = AndroidUtils.edit().putInt("accent_color", newColor);
if (async) editor.apply();
else editor.commit();
}
public static int getTextAttr() {
return getColorFromAttr(R.attr.text_primary);
} // Text Primary color
@@ -304,10 +181,6 @@ public class ThemesUtils {
tw.getBackground().setColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_OVER);
}
public static void recolorBGView(View view) {
view.getBackground().setColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_OVER);
}
public static int darken(int color, float by) {
float[] hsl = new float[3];
ColorUtils.colorToHSL(color, hsl);
@@ -335,8 +208,8 @@ public class ThemesUtils {
return ColorUtils.setAlphaComponent(src, 169);
}
public static int getColor(int i) {
return ThemesHacks.getHackedColor(getGlobalContext(), i);
public static int getColor(int color) {
return Build.VERSION.SDK_INT > 23 ? getGlobalContext().getColor(color) : getGlobalContext().getResources().getColor(color);
} // Android Support color injector + accent color checker
public static int getAlertStyle() {