Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9562a038 authored by Romain Hunault's avatar Romain Hunault 🚴🏻
Browse files

Merge branch 'hotfix-icons' into 'master'

Fix Icons

See merge request e/apps/BlissLauncher!42
parents c76f9246 638e5da8
Loading
Loading
Loading
Loading
Loading
+55 −0
Original line number Diff line number Diff line
package foundation.e.blisslauncher;

import android.annotation.TargetApi;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.DrawableWrapper;
import android.os.Build;
import android.util.AttributeSet;

import org.xmlpull.v1.XmlPullParser;

/**
 * Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount.
 */
@TargetApi(Build.VERSION_CODES.N)
public class FixedScaleDrawable extends DrawableWrapper {

    // TODO b/33553066 use the constant defined in MaskableIconDrawable
    public static final float LEGACY_ICON_SCALE = .7f * .6667f;
    private float mScaleX, mScaleY;

    public FixedScaleDrawable() {
        super(new ColorDrawable());
        mScaleX = LEGACY_ICON_SCALE;
        mScaleY = LEGACY_ICON_SCALE;
    }

    @Override
    public void draw(Canvas canvas) {
        int saveCount = canvas.save();
        canvas.scale(mScaleX, mScaleY,
                getBounds().exactCenterX(), getBounds().exactCenterY());
        super.draw(canvas);
        canvas.restoreToCount(saveCount);
    }

    @Override
    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { }

    @Override
    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme) { }

    public void setScale(float scale) {
        float h = getIntrinsicHeight();
        float w = getIntrinsicWidth();
        mScaleX = scale * LEGACY_ICON_SCALE;
        mScaleY = scale * LEGACY_ICON_SCALE;
        if (h > w && w > 0) {
            mScaleX *= w / h;
        } else if (w > h && h > 0) {
            mScaleY *= h / w;
        }
    }
}
+242 −0
Original line number Diff line number Diff line
package foundation.e.blisslauncher.core;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.SparseIntArray;

import androidx.annotation.NonNull;
import androidx.core.graphics.ColorUtils;

import foundation.e.blisslauncher.FixedScaleDrawable;
import foundation.e.blisslauncher.core.customviews.AdaptiveIconDrawableCompat;

import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.round;

public class AdaptiveIconGenerator {

    // Average number of derived colors (based on averages with ~100 icons and performance testing)
    private static final int NUMBER_OF_COLORS_GUESSTIMATE = 45;

    // Found after some experimenting, might be improved with some more testing
    private static final float FULL_BLEED_ICON_SCALE = 1.44f;
    // Found after some experimenting, might be improved with some more testing
    private static final float NO_MIXIN_ICON_SCALE = 1.40f;
    // Icons with less than 5 colors are considered as "single color"
    private static final int SINGLE_COLOR_LIMIT = 5;
    // Minimal alpha to be considered opaque
    private static final int MIN_VISIBLE_ALPHA = 0xEF;

    private Context context;
    private Drawable icon;

    private boolean ranLoop;
    private boolean shouldWrap;
    private int backgroundColor = Color.WHITE;
    private boolean useWhiteBackground = true;
    private boolean isFullBleed;
    private boolean noMixinNeeded;
    private boolean fullBleedChecked;
    private boolean matchesMaskShape;
    private boolean isBackgroundWhite;
    private float scale;
    private int height;
    private float aHeight;
    private int width;
    private float aWidth;
    private Drawable result;

    public AdaptiveIconGenerator(Context context, @NonNull Drawable icon) {
        this.context = context;
        this.icon = icon;
    }

    private void loop() {
        Drawable extractee = icon;

        if (extractee == null) {
            Log.e("AdaptiveIconGenerator", "extractee is null, skipping.");
            onExitLoop();
            return;
        }

        RectF bounds = new RectF();

        scale = 1.0f;

        if (extractee instanceof ColorDrawable) {
            isFullBleed = true;
            fullBleedChecked = true;
        }

        width = extractee.getIntrinsicWidth();
        height = extractee.getIntrinsicHeight();
        aWidth = width * (1 - (bounds.left + bounds.right));
        aHeight = height * (1 - (bounds.top + bounds.bottom));

        // Check if the icon is squarish
        final float ratio = aHeight / aWidth;
        boolean isSquarish = 0.999 < ratio && ratio < 1.0001;
        boolean almostSquarish = isSquarish || (0.97 < ratio && ratio < 1.005);
        if (!isSquarish) {
            isFullBleed = false;
            fullBleedChecked = true;
        }

        final Bitmap bitmap = Utilities.drawableToBitmap(extractee);
        if (bitmap == null) {
            onExitLoop();
            return;
        }

        if (!bitmap.hasAlpha()) {
            isFullBleed = true;
            fullBleedChecked = true;
        }

        final int size = height * width;
        SparseIntArray rgbScoreHistogram = new SparseIntArray(NUMBER_OF_COLORS_GUESSTIMATE);
        final int[] pixels = new int[size];
        bitmap.getPixels(pixels, 0, width, 0, 0, width, height);

        /*
         *   Calculate the number of padding pixels around the actual icon (i)
         *   +----------------+
         *   |      top       |
         *   +---+--------+---+
         *   |   |        |   |
         *   | l |    i   | r |
         *   |   |        |   |
         *   +---+--------+---+
         *   |     bottom     |
         *   +----------------+
         */
        float adjHeight = height - bounds.top - bounds.bottom;
        float l = bounds.left * width * adjHeight;
        float top = bounds.top * height * width;
        float r = bounds.right * width * adjHeight;
        float bottom = bounds.bottom * height * width;
        int addPixels = round(l + top + r + bottom);

        // Any icon with less than 10% transparent pixels (padding excluded) is considered "full-bleed-ish"
        final int maxTransparent = (int) (round(size * .10) + addPixels);
        // Any icon with less than 27% transparent pixels (padding excluded) doesn't need a color mix-in
        final int noMixinScore = (int) (round(size * .27) + addPixels);

        int highScore = 0;
        int bestRGB = 0;
        int transparentScore = 0;
        for (int pixel : pixels) {
            int alpha = 0xFF & (pixel >> 24);
            if (alpha < MIN_VISIBLE_ALPHA) {
                // Drop mostly-transparent pixels.
                transparentScore++;
                if (transparentScore > maxTransparent) {
                    isFullBleed = false;
                    fullBleedChecked = true;
                }
                continue;
            }
            // Reduce color complexity.
            int rgb = ColorExtractor.posterize(pixel);
            if (rgb < 0) {
                // Defensively avoid array bounds violations.
                continue;
            }
            int currentScore = rgbScoreHistogram.get(rgb) + 1;
            rgbScoreHistogram.append(rgb, currentScore);
            if (currentScore > highScore) {
                highScore = currentScore;
                bestRGB = rgb;
            }
        }
        // add back the alpha channel
        bestRGB |= 0xff << 24;

        // not yet checked = not set to false = has to be full bleed, isBackgroundWhite = true = is adaptive
        isFullBleed |= !fullBleedChecked && !isBackgroundWhite;

        // return early if a mix-in isnt needed
        noMixinNeeded = !isFullBleed && !isBackgroundWhite && almostSquarish && transparentScore <= noMixinScore;

        // Currently, it's set to true so a white background is used for all the icons.
        if(useWhiteBackground) {
            //backgroundColor = Color.WHITE;
            backgroundColor = Color.WHITE & 0x80FFFFFF;
            onExitLoop();
            return;
        }

        if (isFullBleed || noMixinNeeded) {
            backgroundColor = bestRGB;
            onExitLoop();
            return;
        }

        // "single color"
        final int numColors = rgbScoreHistogram.size();
        boolean singleColor = numColors <= SINGLE_COLOR_LIMIT;

        // Convert to HSL to get the lightness and adjust the color
        final float[] hsl = new float[3];
        ColorUtils.colorToHSL(bestRGB, hsl);
        float lightness = hsl[2];

        boolean light = lightness > .5;
        // Apply dark background to mostly white icons
        boolean veryLight = lightness > .75 && singleColor;
        // Apply light background to mostly dark icons
        boolean veryDark = lightness < .35 && singleColor;

        // Adjust color to reach suitable contrast depending on the relationship between the colors
        final int opaqueSize = size - transparentScore;
        final float pxPerColor = opaqueSize / (float) numColors;
        float mixRatio = min(max(pxPerColor / highScore, .15f), .7f);

        // Vary color mix-in based on lightness and amount of colors
        int fill = (light && !veryLight) || veryDark ? 0xFFFFFFFF : 0xFF333333;
        backgroundColor = ColorUtils.blendARGB(bestRGB, fill, mixRatio);
        onExitLoop();
    }

    private void onExitLoop() {
        ranLoop = true;
        result = genResult();
    }

    private Drawable genResult() {
        AdaptiveIconDrawableCompat tmp = new AdaptiveIconDrawableCompat(
                new ColorDrawable(),
                new FixedScaleDrawable()
        );
        ((FixedScaleDrawable) tmp.getForeground()).setDrawable(icon);
        if (isFullBleed || noMixinNeeded) {
            float scale;
            if (noMixinNeeded) {
                float upScale = min(width / aWidth, height / aHeight);
                scale = NO_MIXIN_ICON_SCALE * upScale;
            } else {
                float upScale = max(width / aWidth, height / aHeight);
                scale = FULL_BLEED_ICON_SCALE * upScale;
            }
            ((FixedScaleDrawable) tmp.getForeground()).setScale(scale);
        } else {
            ((FixedScaleDrawable) tmp.getForeground()).setScale(scale);
        }
        ((ColorDrawable) tmp.getBackground()).setColor(backgroundColor);
        return tmp;
    }

    public Drawable getResult() {
        if (!ranLoop) {
            loop();
        }
        return result;
    }
}
+24 −10
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ public class AdaptiveIconProvider {
                    "Loader.with(Context) must be called before loading an icon.");
        }


        PackageManager packageManager = context.getPackageManager();
        Drawable background = null, foreground = null;

@@ -53,15 +54,20 @@ public class AdaptiveIconProvider {
                while ((eventType = manifestParser.nextToken()) != XmlPullParser.END_DOCUMENT) {
                    if (eventType == XmlPullParser.START_TAG && manifestParser.getName().equals(
                            matcher)) {
                        Log.d(TAG, "Manifest Parser Count: " + manifestParser.getAttributeCount());

                        for (int i = 0; i < manifestParser.getAttributeCount(); i++) {
                            Log.d(TAG, "Icon parser: " + manifestParser.getAttributeName(i));
                            if (manifestParser.getAttributeName(i).equalsIgnoreCase("icon")) {
                                iconId = Integer.parseInt(
                                        manifestParser.getAttributeValue(i).substring(1));
                                Log.d(TAG, "Iconid:" + iconId);
                                break;
                            }
                        }
                        if (iconId != 0) {
                            iconName = resources.getResourceName(iconId);
                            Log.d("AdaptiveIcon", "Iconname: " + iconName);
                            if (iconName.contains("/")) {
                                iconName = iconName.split("/")[1];
                            }
@@ -77,13 +83,18 @@ public class AdaptiveIconProvider {

            XmlResourceParser parser = null;
            if (iconId != 0) {
                try {
                    parser = resources.getXml(iconId);
                } catch (Resources.NotFoundException e) {
                    e.printStackTrace();
                    parser = null;
                }
            }

            for (int dir = 0; dir < IC_DIRS.length && parser == null; dir++) {
            /*for (int dir = 0; dir < IC_DIRS.length && parser == null; dir++) {
                for (int config = 0; config < IC_CONFIGS.length && parser == null; config++) {
                    for (String name : iconName != null && !iconName.equals("ic_launcher")
                            ? new String[]{iconName, "ic_launcher"} : new String[]{"ic_launcher"}) {
                    for (String name : (iconName != null && !iconName.equals("ic_launcher"))
                            ? new String[]{iconName, "ic_launcher", "ic_launcher_round"} : new String[]{"ic_launcher", "ic_launcher_round"}) {
                        try {
                            String path = "res/" + IC_DIRS[dir] + IC_CONFIGS[config] + "/" + name
                                    + ".xml";
@@ -91,7 +102,6 @@ public class AdaptiveIconProvider {
                            parser = assetManager.openXmlResourceParser(path);
                        } catch (Exception e) {
                            e.printStackTrace();
                            continue;
                        }

                        if (parser != null) {
@@ -99,7 +109,7 @@ public class AdaptiveIconProvider {
                        }
                    }
                }
            }
            }*/

            int backgroundRes = -1, foregroundRes = -1;
            if (parser != null) {
@@ -146,10 +156,13 @@ public class AdaptiveIconProvider {
            }

            if (backgroundRes != -1) {
                Log.d(TAG, "BackgroundRes: " + backgroundRes);
                Log.d(TAG, "BackgroundResName: " + resources.getResourceName(backgroundRes));
                try {
                    background = ResourcesCompat.getDrawable(resources, backgroundRes, theme);
                } catch (Resources.NotFoundException e) {
                    try {
                    e.printStackTrace();
                    /*try {
                        background = ResourcesCompat.getDrawable(resources,
                                resources.getIdentifier("ic_launcher_background", "mipmap",
                                        packageName), theme);
@@ -160,7 +173,7 @@ public class AdaptiveIconProvider {
                                            packageName), theme);
                        } catch (Resources.NotFoundException ignored) {
                        }
                    }
                    }*/
                }
            }

@@ -168,7 +181,8 @@ public class AdaptiveIconProvider {
                try {
                    foreground = ResourcesCompat.getDrawable(resources, foregroundRes, theme);
                } catch (Resources.NotFoundException e) {
                    try {
                    e.printStackTrace();
                    /*try {
                        foreground = ResourcesCompat.getDrawable(resources,
                                resources.getIdentifier("ic_launcher_foreground", "mipmap",
                                        packageName), theme);
@@ -179,7 +193,7 @@ public class AdaptiveIconProvider {
                                            packageName), theme);
                        } catch (Resources.NotFoundException ignored) {
                        }
                    }
                    }*/
                }
            }
        } catch (Exception e) {
+178 −0
Original line number Diff line number Diff line
package foundation.e.blisslauncher.core;


/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.SparseArray;

import java.util.HashSet;
import java.util.Set;

import kotlin.collections.ArraysKt;

/**
 * Utility class for extracting colors from a bitmap.
 */
public class ColorExtractor {

    private static final String TAG = "ColorExtractor";

    public static int findDominantColorByHue(Bitmap bitmap) {
        return findDominantColorByHue(bitmap, 20);
    }

    /**
     * This picks a dominant color, looking for high-saturation, high-value, repeated hues.
     *
     * @param bitmap The bitmap to scan
     * @param samples The approximate max number of samples to use.
     */
    public static int findDominantColorByHue(Bitmap bitmap, int samples) {
        final int height = bitmap.getHeight();
        final int width = bitmap.getWidth();
        int sampleStride = (int) Math.sqrt((height * width) / samples);
        if (sampleStride < 1) {
            sampleStride = 1;
        }

        // This is an out-param, for getting the hsv values for an rgb
        float[] hsv = new float[3];

        // First get the best hue, by creating a histogram over 360 hue buckets,
        // where each pixel contributes a score weighted by saturation, value, and alpha.
        float[] hueScoreHistogram = new float[360];
        float highScore = -1;
        int bestHue = -1;

        int[] pixels = new int[samples];
        int pixelCount = 0;

        for (int y = 0; y < height; y += sampleStride) {
            for (int x = 0; x < width; x += sampleStride) {
                int argb = bitmap.getPixel(x, y);
                int alpha = 0xFF & (argb >> 24);
                if (alpha < 0x80) {
                    // Drop mostly-transparent pixels.
                    continue;
                }
                // Remove the alpha channel.
                int rgb = argb | 0xFF000000;
                Color.colorToHSV(rgb, hsv);
                // Bucket colors by the 360 integer hues.
                int hue = (int) hsv[0];
                if (hue < 0 || hue >= hueScoreHistogram.length) {
                    // Defensively avoid array bounds violations.
                    continue;
                }
                if (pixelCount < samples) {
                    pixels[pixelCount++] = rgb;
                }
                float score = hsv[1] * hsv[2];
                hueScoreHistogram[hue] += score;
                if (hueScoreHistogram[hue] > highScore) {
                    highScore = hueScoreHistogram[hue];
                    bestHue = hue;
                }
            }
        }

        SparseArray<Float> rgbScores = new SparseArray<>();
        int bestColor = 0xff000000;
        highScore = -1;
        // Go back over the RGB colors that match the winning hue,
        // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets.
        // The highest-scoring RGB color wins.
        for (int i = 0; i < pixelCount; i++) {
            int rgb = pixels[i];
            Color.colorToHSV(rgb, hsv);
            int hue = (int) hsv[0];
            if (hue == bestHue) {
                float s = hsv[1];
                float v = hsv[2];
                int bucket = (int) (s * 100) + (int) (v * 10000);
                // Score by cumulative saturation * value.
                float score = s * v;
                Float oldTotal = rgbScores.get(bucket);
                float newTotal = oldTotal == null ? score : oldTotal + score;
                rgbScores.put(bucket, newTotal);
                if (newTotal > highScore) {
                    highScore = newTotal;
                    // All the colors in the winning bucket are very similar. Last in wins.
                    bestColor = rgb;
                }
            }
        }
        return bestColor;
    }

    public static boolean isSingleColor(Drawable drawable, int color) {
        if (drawable == null) return true;
        final int testColor = posterize(color);
        if (drawable instanceof ColorDrawable) {
            return posterize(((ColorDrawable) drawable).getColor()) == testColor;
        }
        final Bitmap bitmap = Utilities.drawableToBitmap(drawable);
        if (bitmap == null) {
            return false;
        }
        final int height = bitmap.getHeight();
        final int width = bitmap.getWidth();

        int[] pixels = new int[height * width];
        bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
        Set<Integer> set = new HashSet<>(ArraysKt.asList(pixels));
        Integer[] distinctPixels = new Integer[set.size()];
        set.toArray(distinctPixels);

        for (int pixel : distinctPixels) {
            if (testColor != posterize(pixel)) {
                return false;
            }
        }
        return true;
    }

    private static final int MAGIC_NUMBER = 25;

    /*
     * References:
     * https://www.cs.umb.edu/~jreyes/csit114-fall-2007/project4/filters.html#posterize
     * https://github.com/gitgraghu/image-processing/blob/master/src/Effects/Posterize.java
     */
    public static int posterize(int rgb) {
        int red = (0xff & (rgb >> 16));
        int green = (0xff & (rgb >> 8));
        int blue = (0xff & rgb);
        red -= red % MAGIC_NUMBER;
        green -= green % MAGIC_NUMBER;
        blue -= blue % MAGIC_NUMBER;
        if (red < 0) {
            red = 0;
        }
        if (green < 0) {
            green = 0;
        }
        if (blue < 0) {
            blue = 0;
        }
        return red << 16 | green << 8 | blue;
    }
}
+5 −4
Original line number Diff line number Diff line
@@ -184,14 +184,16 @@ public class IconsHandler {
            systemIcon = new AdaptiveIconDrawableCompat(
                    ((AdaptiveIconDrawable) systemIcon).getBackground(),
                    ((AdaptiveIconDrawable) systemIcon).getForeground());
            return systemIcon;
        } else {
            // Icon is not adaptive, try to load using reflection.
            Drawable adaptiveIcon = new AdaptiveIconProvider().load(ctx,
                    componentName.getPackageName());
            if (adaptiveIcon != null) {
                systemIcon = adaptiveIcon;
            } else {
                systemIcon = graphicsUtil.convertToRoundedCorner(ctx,
                        graphicsUtil.addBackground(systemIcon, false));
                // Failed to load adaptive icon, Generate an adaptive icon from app default icon.
                systemIcon = new AdaptiveIconGenerator(ctx, getDefaultAppDrawable(activityInfo, userHandle)).getResult();
            }
        }

@@ -317,8 +319,7 @@ public class IconsHandler {
    }

    public Drawable convertIcon(Drawable icon) {
        return graphicsUtil.convertToRoundedCorner(ctx,
                graphicsUtil.addBackground(icon, false));
        return new AdaptiveIconGenerator(ctx, icon).getResult();
    }

    /**
Loading