new way of color modification
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user