diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 93e1b8878..d2e517b51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,6 +37,8 @@ jobs: sudo apt install zipalign chmod +x scripts/apktool.jar chmod +x scripts/apksigner.jar + rustup target add armv7-linux-androideabi + rustup target add aarch64-linux-android - name: Build VTLite run: | diff --git a/app/build.gradle b/app/build.gradle index f7e6ee2cb..5f8f766c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,6 +7,16 @@ import java.util.zip.ZipInputStream plugins { id 'com.android.application' id 'net.nemerosa.versioning' version '2.15.1' + id 'org.mozilla.rust-android-gradle.rust-android' +} + +cargo { + profile = 'release' + module = '../native-effects' + libname = 'native-effects' + targets = ["arm", "arm64"] + prebuiltToolchains = true + pythonCommand = "python3" } def smaliFolder = projectDir.toPath().resolve("../smali").normalize() @@ -100,6 +110,7 @@ android { targetCompatibility JavaVersion.VERSION_14 } namespace 'ru.vtosters.lite' + ndkVersion '25.2.9519653' } dependencies { @@ -112,6 +123,8 @@ dependencies { task prepareDexForMerge(type: Copy) { dependsOn('exportDex') + // Remove comment for native lib build + dependsOn('cargoBuild') into smaliFolder.toString() from(projectDir) { include 'classes6.dex' @@ -119,7 +132,16 @@ task prepareDexForMerge(type: Copy) { } from(projectDir) { include 'version.properties' - into("assets") + into 'assets' + } + // Remove comment for native lib build + from('../native-effects/target/aarch64-linux-android/release'){ + include 'libnative_effects.so' + into 'lib/arm64-v8a' + } + from('../native-effects/target/armv7-linux-androideabi/release'){ + include 'libnative_effects.so' + into 'lib/armeabi-v7a' } doLast { new File(projectDir, "classes6.dex").delete() diff --git a/app/src/main/java/ru/vtosters/lite/ui/wallpapers/ImageEffect.java b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/ImageEffect.java new file mode 100644 index 000000000..ac8cb3656 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/ImageEffect.java @@ -0,0 +1,245 @@ +package ru.vtosters.lite.ui.wallpapers; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.vtosters.lite.R; +import ru.vtosters.lite.utils.AndroidUtils; + +import java.nio.ByteBuffer; + +import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext; +import static ru.vtosters.lite.utils.AndroidUtils.getPreferences; + +enum ImageEffects { + Blur(true, + AndroidUtils.getArray(R.array.filter_types), + new String[]{"disabled", "low", "med", "high"}, + getGlobalContext().getString(R.string.filter_blur) + ) { + @Override + public void applyEffect(ByteBuffer bitmap, int height, int width) { + int radius; + switch (getPreferences().getString(ImageEffects.Blur.toString(), "disabled")) { + case "low": + radius = 8; + break; + case "med": + radius = 14; + break; + case "high": + radius = 20; + break; + default: + return; + } + NativeEffects.gaussian(bitmap, height, width, radius); + } + + @Override + public String getSummary() { + switch (getPreferences().getString(ImageEffects.Blur.toString(), "disabled")) { + case "low": + return AndroidUtils.getString(R.string.wallpapers_low); + case "med": + return AndroidUtils.getString(R.string.wallpapers_med); + case "high": + return AndroidUtils.getString(R.string.wallpapers_high); + default: + return AndroidUtils.getString(R.string.wallpapers_disabled); + } + } + }, + Dim(true, + AndroidUtils.getArray(R.array.filter_dim_types), + new String[]{"disabled", "dim_black", "dim_white"}, + getGlobalContext().getString(R.string.filter_dim)) { + @Override + public void applyEffect(ByteBuffer bitmap, int height, int width) { + var delta = 0; + switch (getPreferences().getString(ImageEffects.Dim.toString(), "disabled")) { + case "dim_black": + delta = -50; break; + case "dim_white": + delta = 50; break; + default: + return; + } + NativeEffects.dim(bitmap, height, width, delta); + } + + @Override + public String getSummary() { + switch (getPreferences().getString(ImageEffects.Dim.toString(), "disabled")) { + case "dim_black": + return AndroidUtils.getString(R.string.wallpapers_dim_black); + case "dim_white": + return AndroidUtils.getString(R.string.wallpapers_dim_white); + default: + return AndroidUtils.getString(R.string.wallpapers_disabled); + } + } + }, + Mosaic(true, + AndroidUtils.getArray(R.array.filter_types), + new String[]{"disabled", "low", "med", "high"}, + getGlobalContext().getString(R.string.filter_mosaic)) { + @Override + public void applyEffect(ByteBuffer bitmap, int height, int width) { + var scale = 0; + switch (getPreferences().getString(ImageEffects.Mosaic.toString(), "disabled")) { + case "high": + scale = 50; break; + case "med": + scale = 25; break; + case "low": + scale = 10; break; + default: + return; + } + NativeEffects.mosaic(bitmap, height, width, scale); + } + + @Override + public String getSummary() { + switch (getPreferences().getString(ImageEffects.Mosaic.toString(), "disabled")) { + case "low": + return AndroidUtils.getString(R.string.wallpapers_low); + case "med": + return AndroidUtils.getString(R.string.wallpapers_med); + case "high": + return AndroidUtils.getString(R.string.wallpapers_high); + default: + return AndroidUtils.getString(R.string.wallpapers_disabled); + } + } + }, + Monochrome(false, null, null, + getGlobalContext().getString(R.string.filter_monochrome)) { + @Override + public void applyEffect(ByteBuffer bitmap, int height, int width) { + if (isApplied()) { + NativeEffects.monochrome(bitmap); + } + } + }, + Invert(false, null, null, + getGlobalContext().getString(R.string.filter_invert_colors)) { + @Override + public void applyEffect(ByteBuffer bitmap, int height, int width) { + if (isApplied()) { + NativeEffects.invert(bitmap); + } + } + }, + Sepia(false, null, null, + getGlobalContext().getString(R.string.filter_sepia)) { + @Override + public void applyEffect(ByteBuffer bitmap, int height, int width) { + if (isApplied()) { + NativeEffects.sepia(bitmap); + } + } + }, + Emboss(false, null, null, + getGlobalContext().getString(R.string.filter_emboss)) { + @Override + public String getSummary() { + return getGlobalContext().getString(R.string.filter_maybe_lag); + } + + @Override + public void applyEffect(ByteBuffer bitmap, int height, int width) { + if (isApplied()) { + NativeEffects.emboss(bitmap, height, width); + } + } + }, + Engrave(false, null, null, + getGlobalContext().getString(R.string.filter_engrave)) { + @Override + public String getSummary() { + return getGlobalContext().getString(R.string.filter_maybe_lag); + } + + @Override + public void applyEffect(ByteBuffer bitmap, int height, int width) { + if (isApplied()) { + NativeEffects.engrave(bitmap, height, width); + } + } + }, + Flea(false, null, null, + getGlobalContext().getString(R.string.filter_flea)) { + @Override + public void applyEffect(ByteBuffer bitmap, int height, int width) { + if (isApplied()) { + NativeEffects.flea(bitmap); + } + } + }, + Snow(false, null, null, + getGlobalContext().getString(R.string.filter_snow)) { + @Override + public void applyEffect(ByteBuffer bitmap, int height, int width) { + if (isApplied()){ + NativeEffects.snow(bitmap); + } + } + }; + + private final boolean free; + private final String title; + @Nullable + private final String[] entryValues; + @Nullable + private final String[] entries; + + ImageEffects(boolean free, @Nullable String[] entries, @Nullable String[] entryValues, String title) { + this.free = free; + this.entries = entries; + this.entryValues = entryValues; + this.title = title; + } + + public String getTitle() { + return title; + } + + public Boolean isSwitch() { + return entryValues == null || entryValues.length == 0; + } + + public Boolean isList() { + return entryValues != null && entryValues.length > 0; + } + + public boolean isFree() { + return free; + } + + @Nullable + public String[] getEntries() { + return entries; + } + + @Nullable + public String[] getEntryValues() { + return entryValues; + } + + public String getSummary() { + return ""; + } + + public abstract void applyEffect(ByteBuffer bitmap, int height, int width); + + protected boolean isApplied() { + return getPreferences().getBoolean(this.toString(), false); + } + + @NonNull + @Override + public String toString() { + return "msg_" + super.toString(); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/ui/wallpapers/ImageFilters.java b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/ImageFilters.java index a41079e7c..165560054 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/wallpapers/ImageFilters.java +++ b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/ImageFilters.java @@ -1,351 +1,37 @@ package ru.vtosters.lite.ui.wallpapers; -import static ru.vtosters.lite.utils.AndroidUtils.getPreferences; import static ru.vtosters.lite.utils.AndroidUtils.getResources; -import static ru.vtosters.lite.utils.Preferences.hasVerification; import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.LightingColorFilter; -import android.graphics.Matrix; -import android.graphics.Paint; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.util.Log; -import com.vk.medianative.MediaNative; - -import java.util.Random; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; public class ImageFilters { - public static final int COLOR_MAX = 0xFF; public static Drawable getFilteredDrawable(Drawable orig) { if (orig == null) return null; if (!(orig instanceof BitmapDrawable)) return orig; - if (!getPreferences().getString("msg_blur_radius", "disabled").equals("disabled")) { - orig = getBlurredWallpaper(orig, getRadius()); + Bitmap bitmap = ((BitmapDrawable) orig).getBitmap(); + if (bitmap.getConfig() != Bitmap.Config.ARGB_8888) { + bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true); } - if (!getPreferences().getString("msg_dim", "off").equals("off")) { - orig = getDimmed(orig); + var directBuff = ByteBuffer.allocateDirect(bitmap.getByteCount()); + directBuff.order(ByteOrder.nativeOrder()); + bitmap.copyPixelsToBuffer(directBuff); + + // FIXME: preserve order of filters + // e.g. user turned invert first then mosaic then monochrome + // invocation here happens regardless of that order + for (var type : ImageEffects.values()) { + type.applyEffect(directBuff, bitmap.getHeight(), bitmap.getWidth()); } - - if (!getPreferences().getString("msg_mosaic", "disabled").equals("disabled")) { - orig = getMosaic(orig); - } - - if (!hasVerification()) { - return orig; - } - - if (getPreferences().getBoolean("msg_monochrome", false)) { - orig = getMonochrome(orig); - } - - Bitmap bm; - - if (orig != null) { - bm = ((BitmapDrawable) orig).getBitmap(); - } else { - return null; - } - - Bitmap instance = bm.copy(Bitmap.Config.ARGB_8888, true); - - if (getPreferences().getBoolean("msg_invert", false)) { - instance = applyInvertEffect(instance); - } - - if (getPreferences().getBoolean("msg_sepia", false)) { - instance = applySepiaEffect(instance); - } - - if (getPreferences().getBoolean("msg_emboss", false)) { - instance = applyEmbossEffect(instance); - } - - if (getPreferences().getBoolean("msg_engrave", false)) { - instance = applyEngraveEffect(instance); - } - - if (getPreferences().getBoolean("msg_flea", false)) { - instance = applyFleaEffect(instance); - } - - if (getPreferences().getBoolean("msg_snow", false)) { - instance = applySnowEffect(instance); - } - - return new BitmapDrawable(getResources(), instance); - } - - private static int getRadius() { - String radius = getPreferences().getString("msg_blur_radius", "disabled"); - return switch (radius) { - case "low" -> 8; - case "med" -> 14; - default -> 20; - }; - } - - public static Drawable getBlurredWallpaper(Drawable orig, int radius) { - try { - if (orig == null) return null; - if (!(orig instanceof BitmapDrawable)) return orig; // we need only BitmapDrawable - Bitmap bm = ((BitmapDrawable) orig).getBitmap(); - - Bitmap instance = bm.copy(Bitmap.Config.ARGB_8888, true); - - MediaNative.blurBitmap(instance, radius); - return new BitmapDrawable(getResources(), instance); - } catch (Exception e) { - e.printStackTrace(); - return orig; - } - } - - public static Drawable getMosaic(Drawable orig) { - int scale = 100; - - if (orig == null) return null; - if (!(orig instanceof BitmapDrawable)) return orig; - Bitmap bm = ((BitmapDrawable) orig).getBitmap(); - - - Bitmap instance = bm.copy(Bitmap.Config.ARGB_8888, true); - - - String radius = getPreferences().getString("msg_mosaic", "disabled"); - - switch (radius) { - case "high" -> scale = 25; - case "med" -> scale = 50; - case "low" -> scale = 75; - } - - Bitmap temp = Bitmap.createScaledBitmap(instance, scale, scale, false); - Bitmap mosaicBitmap = Bitmap.createScaledBitmap(temp, instance.getWidth(), instance.getHeight(), false); - - return new BitmapDrawable(getResources(), mosaicBitmap); - } - - public static Drawable getDimmed(Drawable orig) { - if (orig == null) return null; - if (!(orig instanceof BitmapDrawable)) return orig; - Bitmap bm = ((BitmapDrawable) orig).getBitmap(); - - Bitmap instance = bm.copy(Bitmap.Config.ARGB_8888, true); - - String radius = getPreferences().getString("msg_dim", "disabled"); - switch (radius) { - case "dim_black": - darken(instance); - case "dim_white": - lighten(instance); - } - return new BitmapDrawable(getResources(), instance); - } - - public static Drawable getMonochrome(Drawable orig) { - if (orig == null) return null; - if (!(orig instanceof BitmapDrawable)) return orig; - Bitmap bm = ((BitmapDrawable) orig).getBitmap(); - - Bitmap instance = bm.copy(Bitmap.Config.ARGB_8888, true); - - Bitmap bwBitmap = Bitmap.createBitmap(instance.getWidth(), instance.getHeight(), Bitmap.Config.RGB_565); - float[] hsv = new float[3]; - for (int col = 0; col < instance.getWidth(); col++) { - for (int row = 0; row < instance.getHeight(); row++) { - Color.colorToHSV(instance.getPixel(col, row), hsv); - if (hsv[2] > 0.5f) { - bwBitmap.setPixel(col, row, 0xffffffff); - } else { - bwBitmap.setPixel(col, row, 0xff000000); - } - } - } - - return new BitmapDrawable(getResources(), bwBitmap); - } - - private static void darken(Bitmap bm) { - try { - Canvas canvas = new Canvas(bm); - Paint p = new Paint(Color.RED); - ColorFilter filter = new LightingColorFilter(0xFF7F7F7F, 0x00000000); // darken - p.setColorFilter(filter); - canvas.drawBitmap(bm, new Matrix(), p); - } catch (Exception e) { - Log.d("WallpaperD", e.getMessage()); - } - } - - private static void lighten(Bitmap bm) { - try { - Canvas canvas = new Canvas(bm); - Paint p = new Paint(Color.RED); - ColorFilter filter = new LightingColorFilter(0xFFFFFFFF, 0x00222222); // lighten - p.setColorFilter(filter); - canvas.drawBitmap(bm, new Matrix(), p); - } catch (Exception e) { - Log.d("WallpaperL", e.getMessage()); - } - } - - public static Bitmap applyInvertEffect(Bitmap src) { - // create new bitmap with the same settings as source bitmap - Bitmap bmOut = Bitmap.createBitmap(src.getWidth(), src.getHeight(), src.getConfig()); - // color info - int A, R, G, B; - int pixelColor; - // image size - int height = src.getHeight(); - int width = src.getWidth(); - - // scan through every pixel - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - // get one pixel - pixelColor = src.getPixel(x, y); - // saving alpha channel - A = Color.alpha(pixelColor); - // inverting byte for each R/G/B channel - R = 255 - Color.red(pixelColor); - G = 255 - Color.green(pixelColor); - B = 255 - Color.blue(pixelColor); - // set newly-inverted pixel to output image - bmOut.setPixel(x, y, Color.argb(A, R, G, B)); - } - } - - // return final bitmap - return bmOut; - } - - public static Bitmap applySepiaEffect(Bitmap src) { - // image size - int width = src.getWidth(); - int height = src.getHeight(); - // create output bitmap - Bitmap bmOut = Bitmap.createBitmap(width, height, src.getConfig()); - // constant grayscale - final double GS_RED = 0.3; - final double GS_GREEN = 0.59; - final double GS_BLUE = 0.11; - // color information - int A, R, G, B; - int pixel; - - // scan through all pixels - for (int x = 0; x < width; ++x) { - for (int y = 0; y < height; ++y) { - // get pixel color - pixel = src.getPixel(x, y); - // get color on each channel - A = Color.alpha(pixel); - R = Color.red(pixel); - G = Color.green(pixel); - B = Color.blue(pixel); - // apply grayscale sample - B = (int) (GS_RED * R + GS_GREEN * G + GS_BLUE * B); - // apply intensity level for sepia tone - R = G = B = (int) (B * 0.9); - // set new pixel color to output bitmap - bmOut.setPixel(x, y, Color.argb(A, R, G, B)); - } - } - - // return final image - return bmOut; - } - - public static Bitmap applyEmbossEffect(Bitmap src) { - double[][] EmbossConfig = new double[][]{ - {-1, 0, -1}, - {0, 4, 0}, - {-1, 0, -1} - }; - ConvolutionMatrix convMatrix = new ConvolutionMatrix(3); - convMatrix.applyConfig(EmbossConfig); - convMatrix.Factor = 1; - convMatrix.Offset = 127; - return ConvolutionMatrix.computeConvolution3x3(src, convMatrix); - } - - public static Bitmap applyEngraveEffect(Bitmap src) { - ConvolutionMatrix convMatrix = new ConvolutionMatrix(3); - convMatrix.setAll(0); - convMatrix.Matrix[0][0] = -2; - convMatrix.Matrix[1][1] = 2; - convMatrix.Factor = 1; - convMatrix.Offset = 95; - return ConvolutionMatrix.computeConvolution3x3(src, convMatrix); - } - - public static Bitmap applyFleaEffect(Bitmap source) { - // get image size - int width = source.getWidth(); - int height = source.getHeight(); - int[] pixels = new int[width * height]; - // get pixel array from source - source.getPixels(pixels, 0, width, 0, 0, width, height); - // a random object - Random random = new Random(); - - int index; - // iteration through pixels - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { - // get current index in 2D-matrix - index = y * width + x; - // get random color - int randColor = Color.rgb(random.nextInt(COLOR_MAX), - random.nextInt(COLOR_MAX), random.nextInt(COLOR_MAX)); - // OR - pixels[index] |= randColor; - } - } - // output bitmap - Bitmap bmOut = Bitmap.createBitmap(width, height, source.getConfig()); - bmOut.setPixels(pixels, 0, width, 0, 0, width, height); - return bmOut; - } - - public static Bitmap applySnowEffect(Bitmap source) { - // get image size - int width = source.getWidth(); - int height = source.getHeight(); - int[] pixels = new int[width * height]; - // get pixel array from source - source.getPixels(pixels, 0, width, 0, 0, width, height); - // random object - Random random = new Random(); - - int R, G, B, index, thresHold; - // iteration through pixels - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { - // get current index in 2D-matrix - index = y * width + x; - // get color - R = Color.red(pixels[index]); - G = Color.green(pixels[index]); - B = Color.blue(pixels[index]); - // generate threshold - thresHold = random.nextInt(COLOR_MAX); - if (R > thresHold && G > thresHold && B > thresHold) { - pixels[index] = Color.rgb(COLOR_MAX, COLOR_MAX, COLOR_MAX); - } - } - } - // output bitmap - Bitmap bmOut = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); - bmOut.setPixels(pixels, 0, width, 0, 0, width, height); - return bmOut; + directBuff.flip(); + bitmap.copyPixelsFromBuffer(directBuff); + return new BitmapDrawable(getResources(), bitmap); } } diff --git a/app/src/main/java/ru/vtosters/lite/ui/wallpapers/NativeEffects.java b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/NativeEffects.java new file mode 100644 index 000000000..1ae99fdcd --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/NativeEffects.java @@ -0,0 +1,22 @@ +package ru.vtosters.lite.ui.wallpapers; + +import java.nio.ByteBuffer; + +public class NativeEffects { + + static { + System.loadLibrary("native_effects"); + } + + public static native String monochrome(ByteBuffer input); + public static native String invert(ByteBuffer input); + public static native String sepia(ByteBuffer input); + public static native String emboss(ByteBuffer input, int height, int width); + public static native String engrave(ByteBuffer input, int height, int width); + public static native String flea(ByteBuffer input); + public static native String snow(ByteBuffer input); + // FIXME: slow when comparing with MediaNative.blurBitmap + public static native String gaussian(ByteBuffer input, int height, int width, float radius); + public static native String dim(ByteBuffer input, int height, int width, int delta); + public static native String mosaic(ByteBuffer input, int height, int width, int scale); +} diff --git a/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpaperMenuFragment.java b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpaperMenuFragment.java index eac75f554..022553d55 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpaperMenuFragment.java +++ b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpaperMenuFragment.java @@ -7,17 +7,16 @@ import android.net.Uri; import android.os.Bundle; import android.widget.Toast; import androidx.annotation.Nullable; -import androidx.preference.ListPreference; import com.vtosters.lite.R; import com.vtosters.lite.general.fragments.MaterialPreferenceToolbarFragment; import com.vtosters.lite.im.ImEngineProvider; import ru.vtosters.lite.ui.PreferenceFragmentUtils; -import ru.vtosters.lite.utils.AndroidUtils; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Arrays; import static ru.vtosters.lite.ui.wallpapers.WallpapersHooks.*; import static ru.vtosters.lite.utils.AndroidUtils.edit; @@ -29,7 +28,6 @@ public class WallpaperMenuFragment extends MaterialPreferenceToolbarFragment { private WallpaperPreferences mWPPreviewPref; public static void copyStream(InputStream copyFrom, OutputStream copyTo) throws IOException { - byte[] buffer = new byte[1024]; int length; while ((length = copyFrom.read(buffer)) > 0) { @@ -47,146 +45,117 @@ public class WallpaperMenuFragment extends MaterialPreferenceToolbarFragment { addPreferencesFromResource(R.xml.empty); - PreferenceFragmentUtils.addPreferenceCategory(getPreferenceScreen(), requireContext().getString(R.string.vkim_settings_appearance_chat_preview_title)); + PreferenceFragmentUtils.addPreferenceCategory( + getPreferenceScreen(), + requireContext().getString(R.string.vkim_settings_appearance_chat_preview_title) + ); mWPPreviewPref = new WallpaperPreferences(getContext()); mWPPreviewPref.setIcon(null); mWPPreviewPref.setIconSpaceReserved(false); getPreferenceScreen().addPreference(mWPPreviewPref); - PreferenceFragmentUtils.addPreference(getPreferenceScreen(), "wp_set", requireContext().getString(R.string.wallpaper_select), requireContext().getString(R.string.wallpaper_from_gallery), R.drawable.ic_picture_outline_28, preference -> { - startActivityForResult(new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI), 1488); - removeStickCache(); - return true; - }); + PreferenceFragmentUtils.addPreference( + getPreferenceScreen(), + "wp_set", + requireContext().getString(R.string.wallpaper_select), + requireContext().getString(R.string.wallpaper_from_gallery), + R.drawable.ic_picture_outline_28, + preference -> { + startActivityForResult( + new Intent( + Intent.ACTION_PICK, + android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI + ), + 1488 + ); + removeStickCache(); + return true; + }); - PreferenceFragmentUtils.addPreference(getPreferenceScreen(), "wp_clear", requireContext().getString(R.string.wallpaper_remove), "", R.drawable.ic_delete_outline_28, preference -> { - removeWallpaper(); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - removeStickCache(); - return true; - }); + PreferenceFragmentUtils.addPreference( + getPreferenceScreen(), + "wp_clear", + requireContext().getString(R.string.wallpaper_remove), + "", + R.drawable.ic_delete_outline_28, + preference -> { + removeWallpaper(); + requestUpdateWallpaper(); + mWPPreviewPref.redraw(); + removeStickCache(); + return true; + }); PreferenceFragmentUtils.addPreferenceCategory(getPreferenceScreen(), requireContext().getString(R.string.vtlfilters)); if (!hasVerification() && !getBoolValue("dialogrecomm", false)) { - PreferenceFragmentUtils.addPreference(getPreferenceScreen(), "", requireContext().getString(R.string.filters_warning), requireContext().getString(R.string.icons_warning_info), 0, preference -> { - requireContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse("https://vtosters.app/donate/"))); - return false; - }); + PreferenceFragmentUtils.addPreference( + getPreferenceScreen(), + "", + requireContext().getString(R.string.filters_warning), + requireContext().getString(R.string.icons_warning_info), + 0, + preference -> { + requireContext().startActivity( + new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse("https://vtosters.app/donate/")) + ); + return false; + }); } - PreferenceFragmentUtils.addListPreference(getPreferenceScreen(), "msg_blur_radius", "disabled", requireContext().getString(R.string.filter_blur), AndroidUtils.getArray(R.array.filter_types), new String[]{ - "disabled", "low", "med", "high" - }); + Arrays.stream(ImageEffects.values()) + .filter(imageEffects -> imageEffects.isFree() || hasVerification()) + .forEach(it -> { + if (it.isList()) { + var pref = PreferenceFragmentUtils.addListPreference( + getPreferenceScreen(), + it.toString(), + "disabled", + it.getTitle(), + it.getEntries(), + it.getEntryValues() + ); + pref.setSummary(it.getSummary()); + pref.setOnPreferenceChangeListener((preference, o) -> { + edit().putString(it.toString(), (String) o).apply(); + preference.setSummary(it.getSummary()); + requestUpdateWallpaper(); + mWPPreviewPref.redraw(); + return true; + }); + } else if (it.isSwitch()) { + PreferenceFragmentUtils.addMaterialSwitchPreference( + getPreferenceScreen(), + it.toString(), + it.getTitle(), + it.getSummary(), + 0, + false, + (preference, o) -> { + edit().putBoolean(it.toString(), (Boolean) o).apply(); + requestUpdateWallpaper(); + mWPPreviewPref.redraw(); + return true; + }); + } + }); - PreferenceFragmentUtils.addListPreference(getPreferenceScreen(), "msg_dim", "off", requireContext().getString(R.string.filter_dim), AndroidUtils.getArray(R.array.filter_dim_types), new String[]{ - "off", "dim_black", "dim_white" - }); - - - PreferenceFragmentUtils.addListPreference(getPreferenceScreen(), "msg_mosaic", "disabled", requireContext().getString(R.string.filter_mosaic), AndroidUtils.getArray(R.array.filter_types), new String[]{ - "disabled", "low", "med", "high" - }); - - if (hasVerification()) { - PreferenceFragmentUtils.addMaterialSwitchPreference(getPreferenceScreen(), "msg_monochrome", requireContext().getString(R.string.filter_monochrome), "", 0, false, (preference, o) -> { - boolean value = (boolean) o; - edit().putBoolean("msg_monochrome", value).commit(); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); - - PreferenceFragmentUtils.addMaterialSwitchPreference(getPreferenceScreen(), "msg_invert", requireContext().getString(R.string.filter_invert_colors), "", 0, false, (preference, o) -> { - boolean value = (boolean) o; - edit().putBoolean("msg_invert", value).commit(); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); - - PreferenceFragmentUtils.addMaterialSwitchPreference(getPreferenceScreen(), "msg_sepia", requireContext().getString(R.string.filter_sepia), "", 0, false, (preference, o) -> { - boolean value = (boolean) o; - edit().putBoolean("msg_sepia", value).commit(); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); - - PreferenceFragmentUtils.addMaterialSwitchPreference(getPreferenceScreen(), "msg_emboss", requireContext().getString(R.string.filter_emboss), requireContext().getString(R.string.filter_maybe_lag), 0, false, (preference, o) -> { - boolean value = (boolean) o; - edit().putBoolean("msg_emboss", value).commit(); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); - - PreferenceFragmentUtils.addMaterialSwitchPreference(getPreferenceScreen(), "msg_engrave", requireContext().getString(R.string.filter_engrave), requireContext().getString(R.string.filter_maybe_lag), 0, false, (preference, o) -> { - boolean value = (boolean) o; - edit().putBoolean("msg_engrave", value).commit(); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); - - PreferenceFragmentUtils.addMaterialSwitchPreference(getPreferenceScreen(), "msg_flea", requireContext().getString(R.string.filter_flea), "", 0, false, (preference, o) -> { - boolean value = (boolean) o; - edit().putBoolean("msg_flea", value).commit(); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); - - PreferenceFragmentUtils.addMaterialSwitchPreference(getPreferenceScreen(), "msg_snow", requireContext().getString(R.string.filter_flea), "", 0, false, (preference, o) -> { - boolean value = (boolean) o; - edit().putBoolean("msg_snow", value).commit(); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); - } - - PreferenceFragmentUtils.addMaterialSwitchPreference(getPreferenceScreen(), "compresswp", requireContext().getString(R.string.compress_wallpaper_title), requireContext().getString(R.string.compress_wallpaper_summ), 0, true, (preference, o) -> { - boolean value = (boolean) o; - edit().putBoolean("compresswp", value).commit(); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); - - ListPreference blur = (ListPreference) findPreference("msg_blur_radius"); - ListPreference dim = (ListPreference) findPreference("msg_dim"); - ListPreference mosaic = (ListPreference) findPreference("msg_mosaic"); - - blur.setSummary(getRadiusSummary()); - dim.setSummary(getDimmingSummary()); - mosaic.setSummary(getMosaicSummary()); - - blur.setOnPreferenceChangeListener((preference, o) -> { - edit().putString("msg_blur_radius", (String) o).apply(); - preference.setSummary(getRadiusSummary()); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); - - dim.setOnPreferenceChangeListener((preference, o) -> { - edit().putString("msg_dim", (String) o).apply(); - preference.setSummary(getDimmingSummary()); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); - - mosaic.setOnPreferenceChangeListener((preference, o) -> { - edit().putString("msg_mosaic", (String) o).apply(); - preference.setSummary(getMosaicSummary()); - requestUpdateWallpaper(); - mWPPreviewPref.redraw(); - return true; - }); + PreferenceFragmentUtils.addMaterialSwitchPreference( + getPreferenceScreen(), + "compresswp", + requireContext().getString(R.string.compress_wallpaper_title), + requireContext().getString(R.string.compress_wallpaper_summ), + 0, + true, + (preference, o) -> { + boolean value = (boolean) o; + edit().putBoolean("compresswp", value).commit(); + requestUpdateWallpaper(); + mWPPreviewPref.redraw(); + return true; + }); } @Override diff --git a/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpaperPreferences.java b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpaperPreferences.java index e2970e6c7..f098503e9 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpaperPreferences.java +++ b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpaperPreferences.java @@ -1,7 +1,6 @@ package ru.vtosters.lite.ui.wallpapers; -import static ru.vtosters.lite.ui.wallpapers.WallpapersHooks.getFilteredFile; -import static ru.vtosters.lite.ui.wallpapers.WallpapersHooks.getWallpaperFile; +import static ru.vtosters.lite.ui.wallpapers.WallpapersHooks.*; import static ru.vtosters.lite.utils.AndroidUtils.dp2px; import android.annotation.SuppressLint; @@ -59,12 +58,8 @@ public class WallpaperPreferences extends Preference { } private void requestBg() { - final Drawable drawable = getFilteredFile(); - - if (drawable != null && getWallpaperFile() != null) - mChatBackground.setImageDrawable(drawable); - else - mChatBackground.setImageDrawable(null); + final Drawable drawable = getWallpaper(); + mChatBackground.setImageDrawable(drawable); } @SuppressLint("NotifyDataSetChanged") diff --git a/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpapersHooks.java b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpapersHooks.java index 80bbba440..423f47604 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpapersHooks.java +++ b/app/src/main/java/ru/vtosters/lite/ui/wallpapers/WallpapersHooks.java @@ -1,78 +1,97 @@ package ru.vtosters.lite.ui.wallpapers; -import static com.vk.im.engine.h.im_bg_chat; -import static ru.vtosters.lite.ui.wallpapers.ImageFilters.getFilteredDrawable; -import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext; -import static ru.vtosters.lite.utils.AndroidUtils.getPreferences; -import static ru.vtosters.lite.utils.AndroidUtils.sendToast; -import static ru.vtosters.lite.utils.Preferences.getBoolValue; - import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.ImageView; - import com.vtosters.lite.R; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; - import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.ThemesUtils; import vigo.sdk.Log; +import java.io.File; +import java.io.FileOutputStream; +import java.util.function.Supplier; + +import static com.vk.im.engine.h.im_bg_chat; +import static ru.vtosters.lite.ui.wallpapers.ImageFilters.getFilteredDrawable; +import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext; +import static ru.vtosters.lite.utils.AndroidUtils.sendToast; +import static ru.vtosters.lite.utils.Preferences.getBoolValue; + public class WallpapersHooks { - private static final boolean compress = getBoolValue("compresswp", true); - private static File mWallpaperFile; + // TODO: force compress by default not asking user about that? + private static final Supplier compress = () -> getBoolValue("compresswp", true); + private static final File originalWp = new File(getGlobalContext().getFilesDir(), "wallpaper.jpeg"); + private static final File compressedWp = new File(getGlobalContext().getFilesDir(), "compressedwp.jpeg"); + // TODO: cache to file? private static Drawable mWallpaper; private static boolean mUpdateWallpaperRequested = true; - private static boolean mUpdateWallpaperFileRequested = true; - public static File getWallpaperFile() { - if (mWallpaperFile == null) { - mWallpaperFile = new File(getGlobalContext().getFilesDir(), "wallpaper.webp"); + // TODO: respect blur, when there is blur/mosaic effect applied these two can be even less, just HD (1280x720) for e.g. + private static final int MAX_WP_WIDTH = 1080; + private static final int MAX_WP_HEIGHT = 1920; + + + public static void setBg(View view) { + if (hasWallpapers()) { + ((ImageView) view).setImageDrawable(getWallpaper()); // set picture to background + } else { + view.setBackgroundColor(ThemesUtils.getColorFromAttr(im_bg_chat)); // set default bg color } - - return mWallpaperFile; } - public static Drawable getFilteredFile() { - File file = null; - File wp = mWallpaperFile; - String temp = getGlobalContext().getFilesDir() + File.separator + "filteredwp.webp"; - - if (wp != null && mUpdateWallpaperFileRequested) { - try { - file = new File(temp); - file.createNewFile(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ((BitmapDrawable) getFilteredDrawable(Drawable.createFromPath(wp.getAbsolutePath()))).getBitmap().compress(Bitmap.CompressFormat.WEBP, (compress ? 80 : 95), bos); - byte[] bitmapdata = bos.toByteArray(); - - FileOutputStream fos = new FileOutputStream(file); - fos.write(bitmapdata); - fos.flush(); - fos.close(); - - } catch (Exception e) { - Log.d("Wallpapers", e.getMessage()); - } - - mUpdateWallpaperFileRequested = false; - } - - try { - return Drawable.createFromPath(temp); - } catch (Exception e) { - if (file != null) { - return Drawable.createFromPath(file.getAbsolutePath()); - } else { + public static Drawable getWallpaper() { + if (mWallpaper == null || mUpdateWallpaperRequested) { + if (!originalWp.exists()) { return null; } + Drawable drawable; + if (compress.get()) { + if (!compressedWp.exists() && !prepareCompressed()) { + return null; + } + drawable = Drawable.createFromPath(compressedWp.getAbsolutePath()); + } else { + drawable = Drawable.createFromPath(originalWp.getAbsolutePath()); + } + + if (drawable == null) { + return null; + } + mWallpaper = getFilteredDrawable(drawable); + mUpdateWallpaperRequested = false; } + + return mWallpaper; + } + + private static boolean prepareCompressed() { + // TODO remove this check? with "compress" enabled it should be fine + if (!eligibleWallpaperFile(originalWp)) { + return false; + } + try (var f = new FileOutputStream(compressedWp)) { + var bitmap = BitmapFactory.decodeFile(originalWp.getAbsolutePath()); + + if (bitmap.getHeight() > MAX_WP_HEIGHT || bitmap.getWidth() > MAX_WP_WIDTH) { + bitmap = resize(bitmap, MAX_WP_WIDTH, MAX_WP_HEIGHT); + } + var drawable = new BitmapDrawable(null, bitmap); + // TODO: remove duplication without changes when provided image already fits required params + drawable.getBitmap().compress(Bitmap.CompressFormat.WEBP, 90, f); + } catch (Exception e) { + Log.d("Wallpapers", e.getMessage()); + return false; + } + return true; + } + + + public static File getWallpaperFile() { + return originalWp; } private static Boolean eligibleWallpaperFile(File file) { @@ -85,94 +104,40 @@ public class WallpapersHooks { } } - public static void setBg(View view) { - if (hasWallpapers()) { - ((ImageView) view).setImageDrawable(getFilteredFile()); // set picture to background - } else { - view.setBackgroundColor(ThemesUtils.getColorFromAttr(im_bg_chat)); // set default bg color - } - } - - public static String getWallpaperUrl() { - var file = getWallpaperFile(); - - if (file.exists() && eligibleWallpaperFile(file)) { - return file.getAbsolutePath(); - } - - return "default"; - } - public static boolean hasWallpapers() { return getWallpaper() != null; } - public static Drawable getWallpaper() { - if (mWallpaper == null || mUpdateWallpaperRequested) { - String url = getWallpaperUrl(); - - if (url.equals("default") || url.isEmpty()) { - return null; - } - - Drawable drawable = Drawable.createFromPath(getWallpaperUrl()); - - if (drawable == null) { - return null; - } - - mWallpaper = drawable; - - mWallpaper = getFilteredDrawable(mWallpaper); - - mUpdateWallpaperRequested = false; - } - - return mWallpaper; - } - public static void requestUpdateWallpaper() { mUpdateWallpaperRequested = true; - mUpdateWallpaperFileRequested = true; } - public static String getRadiusSummary() { - String radius = getPreferences().getString("msg_blur_radius", "disabled"); - return switch (radius) { - case "low" -> AndroidUtils.getString(R.string.wallpapers_low); - case "med" -> AndroidUtils.getString(R.string.wallpapers_med); - case "high" -> AndroidUtils.getString(R.string.wallpapers_high); - default -> AndroidUtils.getString(R.string.wallpapers_disabled); - }; - } - - public static String getDimmingSummary() { - String radius = getPreferences().getString("msg_dim", "disabled"); - return switch (radius) { - case "dim_black" -> AndroidUtils.getString(R.string.wallpapers_dim_black); - case "dim_white" -> AndroidUtils.getString(R.string.wallpapers_dim_white); - default -> AndroidUtils.getString(R.string.wallpapers_disabled); - }; - } - - public static String getMosaicSummary() { - String radius = getPreferences().getString("msg_mosaic", "disabled"); - return switch (radius) { - case "low" -> AndroidUtils.getString(R.string.wallpapers_low); - case "med" -> AndroidUtils.getString(R.string.wallpapers_med); - case "high" -> AndroidUtils.getString(R.string.wallpapers_high); - default -> AndroidUtils.getString(R.string.wallpapers_disabled); - }; - } - - public static void removeWallpaper() { try { - new File(getGlobalContext().getFilesDir(), "wallpaper.webp").delete(); - new File(getGlobalContext().getFilesDir(), "filteredwp.webp").delete(); - mWallpaperFile = null; + mWallpaper = null; + originalWp.delete(); + compressedWp.delete(); } catch (Exception e) { Log.d("Wallpapers", e.getMessage()); } } + + private static Bitmap resize(Bitmap image, int maxWidth, int maxHeight) { + if (maxHeight > 0 && maxWidth > 0) { + int width = image.getWidth(); + int height = image.getHeight(); + float ratioBitmap = (float) width / (float) height; + float ratioMax = (float) maxWidth / (float) maxHeight; + + int finalWidth = maxWidth; + int finalHeight = maxHeight; + if (ratioMax > ratioBitmap) { + finalWidth = (int) ((float) maxHeight * ratioBitmap); + } else { + finalHeight = (int) ((float) maxWidth / ratioBitmap); + } + image = Bitmap.createScaledBitmap(image, finalWidth, finalHeight, true); + } + return image; + } } diff --git a/build.gradle b/build.gradle index 868de8026..61a22649a 100644 --- a/build.gradle +++ b/build.gradle @@ -3,9 +3,13 @@ buildscript { repositories { google() mavenCentral() + maven { + url "https://plugins.gradle.org/m2/" + } } dependencies { classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'org.mozilla.rust-android-gradle:plugin:0.9.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/native-effects/.gitignore b/native-effects/.gitignore new file mode 100644 index 000000000..96ef6c0b9 --- /dev/null +++ b/native-effects/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/native-effects/Cargo.toml b/native-effects/Cargo.toml new file mode 100644 index 000000000..b4f69397c --- /dev/null +++ b/native-effects/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "native-effects" +version = "0.1.0" +edition = "2021" + +[profile.release] +opt-level = 3 +strip = true +lto = true +codegen-units = 1 + +[lib] +crate-type = ["cdylib"] + +[dependencies] +jni = "0.20.0" +rand = "0.8.5" +image = "0.24.5" diff --git a/native-effects/src/lib.rs b/native-effects/src/lib.rs new file mode 100644 index 000000000..340a66ec7 --- /dev/null +++ b/native-effects/src/lib.rs @@ -0,0 +1,365 @@ +use jni::objects::JByteBuffer; +use jni::objects::JClass; +use jni::sys::jfloat; +use jni::sys::jint; +use jni::sys::jstring; +use jni::JNIEnv; +use rand::Rng; +use std::cmp::max; + +// We assume that input ByteBuffer is always in ARGB_8888 format +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct Pixel { + r: u8, + g: u8, + b: u8, + a: u8, +} + +impl Pixel { + fn set_color(&mut self, r: u8, g: u8, b: u8) { + self.r = r; + self.g = g; + self.b = b; + } + + fn invert(&mut self) { + self.r = 255 - self.r; + self.g = 255 - self.g; + self.b = 255 - self.b; + } +} + +fn resolve_pixel_slice<'a>(env: &JNIEnv<'a>, bitmap_buffer: &JByteBuffer<'a>) -> &'a mut [Pixel] { + let start = env + .get_direct_buffer_address(*bitmap_buffer) + .expect("Unable to resolve DirectBuffer address"); + let capacity = env + .get_direct_buffer_capacity(*bitmap_buffer) + .expect("Unable to resolve DirectBuffer size"); + let s = start as *mut Pixel; + unsafe { std::slice::from_raw_parts_mut(s, capacity / 4) } +} + +#[no_mangle] +pub extern "C" fn Java_ru_vtosters_lite_ui_wallpapers_NativeEffects_monochrome( + env: JNIEnv, + _: JClass, + bitmap_buffer: JByteBuffer, +) -> jstring { + let pixels = resolve_pixel_slice(&env, &bitmap_buffer); + pixels.iter_mut().for_each(|p| { + // value (V) from HSV + let value = max(p.r, max(p.g, p.b)); + if value > 127 { + p.set_color(255, 255, 255); + } else { + p.set_color(0, 0, 0); + } + }); + + let output = env + .new_string("Applied Monochrome effect!") + .expect("Couldn't create java string!"); + return **output; +} + +#[no_mangle] +pub extern "C" fn Java_ru_vtosters_lite_ui_wallpapers_NativeEffects_invert( + env: JNIEnv, + _: JClass, + bitmap_buffer: JByteBuffer, +) -> jstring { + let pixels = resolve_pixel_slice(&env, &bitmap_buffer); + + pixels.iter_mut().for_each(|p| p.invert()); + + let output = env + .new_string("Applied Invert effect!") + .expect("Couldn't create java string!"); + return **output; +} + +#[no_mangle] +pub extern "C" fn Java_ru_vtosters_lite_ui_wallpapers_NativeEffects_sepia( + env: JNIEnv, + _: JClass, + bitmap_buffer: JByteBuffer, +) -> jstring { + let pixels = resolve_pixel_slice(&env, &bitmap_buffer); + + pixels.iter_mut().for_each(|p| { + let r = p.r as f32; + let g = p.g as f32; + let b = p.b as f32; + + let new_red = 0.393 * r + 0.769 * g + 0.189 * b; + let new_green = 0.349 * r + 0.686 * g + 0.168 * b; + let new_blue = 0.272 * r + 0.534 * g + 0.131 * b; + + p.set_color(new_red as u8, new_green as u8, new_blue as u8); + }); + + let output = env + .new_string("Applied Sepia effect!") + .expect("Couldn't create java string!"); + return **output; +} + +#[no_mangle] +pub extern "C" fn Java_ru_vtosters_lite_ui_wallpapers_NativeEffects_emboss( + env: JNIEnv, + _: JClass, + bitmap_buffer: JByteBuffer, + height: jint, + width: jint, +) -> jstring { + let pixels = resolve_pixel_slice(&env, &bitmap_buffer); + let kernel = [-1.0, 0.0, -1.0, 0.0, 4.0, 0.0, -1.0, 0.0, -1.0]; + let out: &mut Vec = &mut Vec::new(); + for (y, row) in pixels.chunks(width as usize).enumerate() { + for (x, pixel) in row.iter().enumerate() { + if y == 0 || x == 0 || y == (height - 1) as usize || x == (width - 1) as usize { + out.push(*pixel); + continue; + } + let start = y * width as usize + x - 1; + let end = start + 3; + let matrix = [ + &pixels[start - width as usize..end - width as usize], + &pixels[start..end], + &pixels[start + width as usize..end + width as usize], + ] + .concat(); + let mut acc = [64.0, 64.0, 64.0]; + for (p, k) in matrix.iter().zip(kernel.iter()) { + acc[0] += p.r as f32 * k; + acc[1] += p.g as f32 * k; + acc[2] += p.b as f32 * k; + } + + out.push(Pixel { + r: acc[0] as u8, + g: acc[1] as u8, + b: acc[2] as u8, + a: pixel.a, + }); + } + } + + pixels.copy_from_slice(&out); + + let output = env + .new_string("Applied Emboss effect!") + .expect("Couldn't create java string!"); + return **output; +} + +#[no_mangle] +pub extern "C" fn Java_ru_vtosters_lite_ui_wallpapers_NativeEffects_engrave( + env: JNIEnv, + _: JClass, + bitmap_buffer: JByteBuffer, + height: jint, + width: jint, +) -> jstring { + let pixels = resolve_pixel_slice(&env, &bitmap_buffer); + let kernel = [-2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0]; + let out: &mut Vec = &mut Vec::new(); + for (y, row) in pixels.chunks(width as usize).enumerate() { + for (x, pixel) in row.iter().enumerate() { + if y == 0 || x == 0 || y == (height - 1) as usize || x == (width - 1) as usize { + out.push(*pixel); + continue; + } + let start = y * width as usize + x - 1; + let end = start + 3; + let matrix = [ + &pixels[start - width as usize..end - width as usize], + &pixels[start..end], + &pixels[start + width as usize..end + width as usize], + ] + .concat(); + let mut acc = [95.0, 95.0, 95.0]; + for (p, k) in matrix.iter().zip(kernel.iter()) { + acc[0] += p.r as f32 * k; + acc[1] += p.g as f32 * k; + acc[2] += p.b as f32 * k; + } + + out.push(Pixel { + r: acc[0] as u8, + g: acc[1] as u8, + b: acc[2] as u8, + a: pixel.a, + }); + } + } + + pixels.copy_from_slice(&out); + + let output = env + .new_string("Applied Engrave effect!") + .expect("Couldn't create java string!"); + return **output; +} + +#[no_mangle] +pub extern "C" fn Java_ru_vtosters_lite_ui_wallpapers_NativeEffects_flea( + env: JNIEnv, + _: JClass, + bitmap_buffer: JByteBuffer, +) -> jstring { + let mut rng = rand::thread_rng(); + let pixels = resolve_pixel_slice(&env, &bitmap_buffer); + for p in pixels { + p.set_color( + p.r | rng.gen::(), + p.g | rng.gen::(), + p.b | rng.gen::(), + ) + } + + let output = env + .new_string("Applied Flea effect!") + .expect("Couldn't create java string!"); + return **output; +} + +#[no_mangle] +pub extern "C" fn Java_ru_vtosters_lite_ui_wallpapers_NativeEffects_snow( + env: JNIEnv, + _: JClass, + bitmap_buffer: JByteBuffer, +) -> jstring { + let mut rng = rand::thread_rng(); + let pixels = resolve_pixel_slice(&env, &bitmap_buffer); + for p in pixels { + let threshold: u8 = rng.gen(); + if p.r > threshold && p.g > threshold && p.b > threshold { + p.set_color(255, 255, 255); + } + } + + let output = env + .new_string("Applied Snow effect!") + .expect("Couldn't create java string!"); + return **output; +} + +#[no_mangle] +pub extern "C" fn Java_ru_vtosters_lite_ui_wallpapers_NativeEffects_gaussian( + env: JNIEnv, + _: JClass, + bitmap_buffer: JByteBuffer, + height: jint, + width: jint, + radius: jfloat, +) -> jstring { + let start = env + .get_direct_buffer_address(bitmap_buffer) + .expect("Unable to resolve DirectBuffer address"); + let capacity = env + .get_direct_buffer_capacity(bitmap_buffer) + .expect("Unable to resolve DirectBuffer size"); + let s = start as *mut u8; + let s = unsafe { std::slice::from_raw_parts_mut(s, capacity) }; + + let mut img = image::ImageBuffer::, Vec>::from_raw( + width as u32, + height as u32, + s.to_vec(), + ) + .unwrap(); + + let img = image::imageops::blur(&mut img, radius); + + s.copy_from_slice(img.as_raw()); + + let output = env + .new_string("Applied Gaussian effect!") + .expect("Couldn't create java string!"); + return **output; +} + +#[no_mangle] +pub extern "C" fn Java_ru_vtosters_lite_ui_wallpapers_NativeEffects_dim( + env: JNIEnv, + _: JClass, + bitmap_buffer: JByteBuffer, + height: jint, + width: jint, + delta: jint, +) -> jstring { + let start = env + .get_direct_buffer_address(bitmap_buffer) + .expect("Unable to resolve DirectBuffer address"); + let capacity = env + .get_direct_buffer_capacity(bitmap_buffer) + .expect("Unable to resolve DirectBuffer size"); + let s = start as *mut u8; + let s = unsafe { std::slice::from_raw_parts_mut(s, capacity) }; + + let img = image::ImageBuffer::, Vec>::from_raw( + width as u32, + height as u32, + s.to_vec(), + ) + .unwrap(); + + let img = image::imageops::brighten(&img, delta); + + s.copy_from_slice(img.as_raw()); + + let output = env + .new_string("Applied Dim effect!") + .expect("Couldn't create java string!"); + return **output; +} + +#[no_mangle] +pub extern "C" fn Java_ru_vtosters_lite_ui_wallpapers_NativeEffects_mosaic( + env: JNIEnv, + _: JClass, + bitmap_buffer: JByteBuffer, + height: jint, + width: jint, + scale: jint, +) -> jstring { + let start = env + .get_direct_buffer_address(bitmap_buffer) + .expect("Unable to resolve DirectBuffer address"); + let capacity = env + .get_direct_buffer_capacity(bitmap_buffer) + .expect("Unable to resolve DirectBuffer size"); + let s = start as *mut u8; + let s = unsafe { std::slice::from_raw_parts_mut(s, capacity) }; + + let mut img = image::ImageBuffer::, Vec>::from_raw( + width as u32, + height as u32, + s.to_vec(), + ) + .unwrap(); + + img = image::imageops::resize( + &img, + (width / scale) as u32, + (height / scale) as u32, + image::imageops::FilterType::Nearest, + ); + img = image::imageops::resize( + &img, + width as u32, + height as u32, + image::imageops::FilterType::Nearest, + ); + + s.copy_from_slice(img.as_raw()); + + let output = env + .new_string("Applied Mosaic effect!") + .expect("Couldn't create java string!"); + return **output; +} diff --git a/smali/lib/arm64-v8a/libnative_effects.so b/smali/lib/arm64-v8a/libnative_effects.so new file mode 100755 index 000000000..a14eda9a5 Binary files /dev/null and b/smali/lib/arm64-v8a/libnative_effects.so differ diff --git a/smali/lib/armeabi-v7a/libnative_effects.so b/smali/lib/armeabi-v7a/libnative_effects.so new file mode 100755 index 000000000..14efeae11 Binary files /dev/null and b/smali/lib/armeabi-v7a/libnative_effects.so differ diff --git a/smali/smali_classes3/com/vk/im/ui/ImUiPrefs.smali b/smali/smali_classes3/com/vk/im/ui/ImUiPrefs.smali index 0ce36872c..1815e92fd 100644 --- a/smali/smali_classes3/com/vk/im/ui/ImUiPrefs.smali +++ b/smali/smali_classes3/com/vk/im/ui/ImUiPrefs.smali @@ -424,38 +424,38 @@ return v0 .end method -.method public final c()Ljava/lang/String; - .locals 3 +# .method public final c()Ljava/lang/String; +# .locals 3 - invoke-static {}, Lru/vtosters/lite/ui/wallpapers/WallpapersHooks;->getWallpaperUrl()Ljava/lang/String; +# invoke-static {}, Lru/vtosters/lite/ui/wallpapers/WallpapersHooks;->getWallpaperUrl()Ljava/lang/String; - move-result-object v0 +# move-result-object v0 - return-object v0 +# return-object v0 - .line 1 - invoke-direct {p0}, Lcom/vk/im/ui/ImUiPrefs;->k()Landroid/content/SharedPreferences; +# .line 1 +# invoke-direct {p0}, Lcom/vk/im/ui/ImUiPrefs;->k()Landroid/content/SharedPreferences; - move-result-object v0 +# move-result-object v0 - const-string v1, "pref_dialog_background_uri" +# const-string v1, "pref_dialog_background_uri" - const-string v2, "default" +# const-string v2, "default" - invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +# invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; - move-result-object v0 +# move-result-object v0 - if-eqz v0, :cond_0 +# if-eqz v0, :cond_0 - goto :goto_0 +# goto :goto_0 - :cond_0 - const-string v0, "" +# :cond_0 +# const-string v0, "" - :goto_0 - return-object v0 -.end method +# :goto_0 +# return-object v0 +# .end method .method public final d()J .locals 4 diff --git a/smali/smali_classes3/com/vk/im/ui/fragments/ChatFragment.smali b/smali/smali_classes3/com/vk/im/ui/fragments/ChatFragment.smali index 101c61a3c..83279a023 100644 --- a/smali/smali_classes3/com/vk/im/ui/fragments/ChatFragment.smali +++ b/smali/smali_classes3/com/vk/im/ui/fragments/ChatFragment.smali @@ -772,180 +772,180 @@ return-void - .line 1 - sget-object v0, Lcom/vk/im/ui/ImUiPrefs;->g:Lcom/vk/im/ui/ImUiPrefs; + # .line 1 + # sget-object v0, Lcom/vk/im/ui/ImUiPrefs;->g:Lcom/vk/im/ui/ImUiPrefs; - invoke-virtual {v0}, Lcom/vk/im/ui/ImUiPrefs;->c()Ljava/lang/String; + # invoke-virtual {v0}, Lcom/vk/im/ui/ImUiPrefs;->c()Ljava/lang/String; - move-result-object v0 + # move-result-object v0 - .line 2 - invoke-interface {v0}, Ljava/lang/CharSequence;->length()I + # .line 2 + # invoke-interface {v0}, Ljava/lang/CharSequence;->length()I - move-result v1 + # move-result v1 - const/4 v2, 0x1 + # const/4 v2, 0x1 - const/4 v3, 0x0 + # const/4 v3, 0x0 - if-nez v1, :cond_0 + # if-nez v1, :cond_0 - const/4 v1, 0x1 + # const/4 v1, 0x1 - goto :goto_0 + # goto :goto_0 - :cond_0 - const/4 v1, 0x0 + # :cond_0 + # const/4 v1, 0x0 - :goto_0 - const-string v4, "msgListComponent" + # :goto_0 + # const-string v4, "msgListComponent" - const-string v5, "dialogBackgroundView" + # const-string v5, "dialogBackgroundView" - const/4 v6, 0x0 + # const/4 v6, 0x0 - if-nez v1, :cond_6 + # if-nez v1, :cond_6 - const-string v1, "default" + # const-string v1, "default" - invoke-static {v0, v1}, Lkotlin/jvm/internal/Intrinsics;->a(Ljava/lang/Object;Ljava/lang/Object;)Z + # invoke-static {v0, v1}, Lkotlin/jvm/internal/Intrinsics;->a(Ljava/lang/Object;Ljava/lang/Object;)Z - move-result v1 + # move-result v1 - if-nez v1, :cond_6 + # if-nez v1, :cond_6 - iget-object v1, p0, Lcom/vk/im/ui/fragments/ChatFragment;->E:Lcom/vk/im/engine/models/dialogs/DialogExt; + # iget-object v1, p0, Lcom/vk/im/ui/fragments/ChatFragment;->E:Lcom/vk/im/engine/models/dialogs/DialogExt; - if-eqz v1, :cond_5 + # if-eqz v1, :cond_5 - invoke-virtual {v1}, Lcom/vk/im/engine/models/dialogs/DialogExt;->t1()Lcom/vk/im/engine/models/dialogs/Dialog; + # invoke-virtual {v1}, Lcom/vk/im/engine/models/dialogs/DialogExt;->t1()Lcom/vk/im/engine/models/dialogs/Dialog; - move-result-object v1 + # move-result-object v1 - invoke-virtual {v1}, Lcom/vk/im/engine/models/dialogs/Dialog;->U1()Z + # invoke-virtual {v1}, Lcom/vk/im/engine/models/dialogs/Dialog;->U1()Z - move-result v1 + # move-result v1 - if-eqz v1, :cond_1 + # if-eqz v1, :cond_1 - goto :goto_1 + # goto :goto_1 - .line 3 - :cond_1 - iget-object v1, p0, Lcom/vk/im/ui/fragments/ChatFragment;->k0:Lcom/vk/im/ui/views/FrescoImageView; + # .line 3 + # :cond_1 + # iget-object v1, p0, Lcom/vk/im/ui/fragments/ChatFragment;->k0:Lcom/vk/im/ui/views/FrescoImageView; - if-eqz v1, :cond_4 + # if-eqz v1, :cond_4 - new-instance v3, Lcom/vk/im/engine/models/Image; + # new-instance v3, Lcom/vk/im/engine/models/Image; - invoke-direct {v3, v0}, Lcom/vk/im/engine/models/Image;->(Ljava/lang/String;)V + # invoke-direct {v3, v0}, Lcom/vk/im/engine/models/Image;->(Ljava/lang/String;)V - invoke-virtual {v1, v3}, Lcom/vk/im/ui/views/FrescoImageView;->setLocalImage(Lcom/vk/im/engine/models/Image;)V + # invoke-virtual {v1, v3}, Lcom/vk/im/ui/views/FrescoImageView;->setLocalImage(Lcom/vk/im/engine/models/Image;)V - .line 4 - iget-object v0, p0, Lcom/vk/im/ui/fragments/ChatFragment;->k0:Lcom/vk/im/ui/views/FrescoImageView; + # .line 4 + # iget-object v0, p0, Lcom/vk/im/ui/fragments/ChatFragment;->k0:Lcom/vk/im/ui/views/FrescoImageView; - if-eqz v0, :cond_3 + # if-eqz v0, :cond_3 - invoke-virtual {v0, v6}, Lcom/vk/im/ui/views/FrescoImageView;->setBackgroundImage(Landroid/graphics/drawable/Drawable;)V + # invoke-virtual {v0, v6}, Lcom/vk/im/ui/views/FrescoImageView;->setBackgroundImage(Landroid/graphics/drawable/Drawable;)V - .line 5 - iget-object v0, p0, Lcom/vk/im/ui/fragments/ChatFragment;->d0:Lcom/vk/im/ui/components/msg_list/MsgListComponent; + # .line 5 + # iget-object v0, p0, Lcom/vk/im/ui/fragments/ChatFragment;->d0:Lcom/vk/im/ui/components/msg_list/MsgListComponent; - if-eqz v0, :cond_2 + # if-eqz v0, :cond_2 - invoke-virtual {v0, v2}, Lcom/vk/im/ui/components/msg_list/MsgListComponent;->b(Z)V + # invoke-virtual {v0, v2}, Lcom/vk/im/ui/components/msg_list/MsgListComponent;->b(Z)V - goto :goto_2 + # goto :goto_2 - :cond_2 - invoke-static {v4}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V + # :cond_2 + # invoke-static {v4}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V - throw v6 + # throw v6 - .line 6 - :cond_3 - invoke-static {v5}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V + # .line 6 + # :cond_3 + # invoke-static {v5}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V - throw v6 + # throw v6 - .line 7 - :cond_4 - invoke-static {v5}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V + # .line 7 + # :cond_4 + # invoke-static {v5}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V - throw v6 + # throw v6 - :cond_5 - const-string v0, "dialogExt" + # :cond_5 + # const-string v0, "dialogExt" - .line 8 - invoke-static {v0}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V + # .line 8 + # invoke-static {v0}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V - throw v6 + # throw v6 - .line 9 - :cond_6 - :goto_1 - iget-object v0, p0, Lcom/vk/im/ui/fragments/ChatFragment;->k0:Lcom/vk/im/ui/views/FrescoImageView; + # .line 9 + # :cond_6 + # :goto_1 + # iget-object v0, p0, Lcom/vk/im/ui/fragments/ChatFragment;->k0:Lcom/vk/im/ui/views/FrescoImageView; - if-eqz v0, :cond_a + # if-eqz v0, :cond_a - invoke-virtual {v0, v6}, Lcom/vk/im/ui/views/FrescoImageView;->setLocalImage(Lcom/vk/im/engine/models/Image;)V + # invoke-virtual {v0, v6}, Lcom/vk/im/ui/views/FrescoImageView;->setLocalImage(Lcom/vk/im/engine/models/Image;)V - .line 10 - iget-object v0, p0, Lcom/vk/im/ui/fragments/ChatFragment;->k0:Lcom/vk/im/ui/views/FrescoImageView; + # .line 10 + # iget-object v0, p0, Lcom/vk/im/ui/fragments/ChatFragment;->k0:Lcom/vk/im/ui/views/FrescoImageView; - if-eqz v0, :cond_9 + # if-eqz v0, :cond_9 - new-instance v1, Landroid/graphics/drawable/ColorDrawable; + # new-instance v1, Landroid/graphics/drawable/ColorDrawable; - iget-object v2, p0, Lcom/vk/im/ui/fragments/ChatFragment;->L:Landroid/content/Context; + # iget-object v2, p0, Lcom/vk/im/ui/fragments/ChatFragment;->L:Landroid/content/Context; - if-eqz v2, :cond_8 + # if-eqz v2, :cond_8 - sget v5, Lcom/vk/im/ui/c;->im_bg_chat:I + # sget v5, Lcom/vk/im/ui/c;->im_bg_chat:I - invoke-static {v2, v5}, Lcom/vk/core/util/ContextExtKt;->h(Landroid/content/Context;I)I + # invoke-static {v2, v5}, Lcom/vk/core/util/ContextExtKt;->h(Landroid/content/Context;I)I - move-result v2 + # move-result v2 - invoke-direct {v1, v2}, Landroid/graphics/drawable/ColorDrawable;->(I)V + # invoke-direct {v1, v2}, Landroid/graphics/drawable/ColorDrawable;->(I)V - invoke-virtual {v0, v1}, Lcom/vk/im/ui/views/FrescoImageView;->setBackgroundImage(Landroid/graphics/drawable/Drawable;)V + # invoke-virtual {v0, v1}, Lcom/vk/im/ui/views/FrescoImageView;->setBackgroundImage(Landroid/graphics/drawable/Drawable;)V - .line 11 - iget-object v0, p0, Lcom/vk/im/ui/fragments/ChatFragment;->d0:Lcom/vk/im/ui/components/msg_list/MsgListComponent; + # .line 11 + # iget-object v0, p0, Lcom/vk/im/ui/fragments/ChatFragment;->d0:Lcom/vk/im/ui/components/msg_list/MsgListComponent; - if-eqz v0, :cond_7 + # if-eqz v0, :cond_7 - invoke-virtual {v0, v3}, Lcom/vk/im/ui/components/msg_list/MsgListComponent;->b(Z)V + # invoke-virtual {v0, v3}, Lcom/vk/im/ui/components/msg_list/MsgListComponent;->b(Z)V - :goto_2 - return-void + # :goto_2 + # return-void - :cond_7 - invoke-static {v4}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V + # :cond_7 + # invoke-static {v4}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V - throw v6 + # throw v6 - :cond_8 - const-string v0, "themedContext" + # :cond_8 + # const-string v0, "themedContext" - .line 12 - invoke-static {v0}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V + # .line 12 + # invoke-static {v0}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V - throw v6 + # throw v6 - :cond_9 - invoke-static {v5}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V + # :cond_9 + # invoke-static {v5}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V - throw v6 + # throw v6 - .line 13 - :cond_a - invoke-static {v5}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V + # .line 13 + # :cond_a + # invoke-static {v5}, Lkotlin/jvm/internal/Intrinsics;->b(Ljava/lang/String;)V - throw v6 + # throw v6 .end method .method private final Y4()V