Просмотр исходного кода

- use display name for avatar generation
- use new server algorithm
Ref: https://github.com/nextcloud/nextcloud-vue/blob/master/src/functions/usernameToColor/usernameToColor.js

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>

tobiasKaminsky 4 лет назад
Родитель
Сommit
e07c2d169a

BIN
screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer.png


BIN
screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open.png


BIN
screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open.png


BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png


+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-326
+320

+ 79 - 0
src/androidTest/java/com/owncloud/android/utils/BitmapUtilsIT.kt

@@ -0,0 +1,79 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.utils
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+
+class BitmapUtilsIT {
+    @Test
+    @Suppress("MagicNumber")
+    fun usernameToColor() {
+        assertEquals(BitmapUtils.Color(0, 0, 0), BitmapUtils.Color(0, 0, 0))
+        assertEquals(BitmapUtils.Color(221, 203, 85), BitmapUtils.usernameToColor("User"))
+        assertEquals(BitmapUtils.Color(208, 158, 109), BitmapUtils.usernameToColor("Admin"))
+        assertEquals(BitmapUtils.Color(0, 130, 201), BitmapUtils.usernameToColor(""))
+        assertEquals(BitmapUtils.Color(201, 136, 121), BitmapUtils.usernameToColor("68b329da9893e34099c7d8ad5cb9c940"))
+
+        // tests from server
+        assertEquals(BitmapUtils.Color(208, 158, 109), BitmapUtils.usernameToColor("Alishia Ann Lowry"))
+        assertEquals(BitmapUtils.Color(0, 130, 201), BitmapUtils.usernameToColor("Arham Johnson"))
+        assertEquals(BitmapUtils.Color(208, 158, 109), BitmapUtils.usernameToColor("Brayden Truong"))
+        assertEquals(BitmapUtils.Color(151, 80, 164), BitmapUtils.usernameToColor("Daphne Roy"))
+        assertEquals(BitmapUtils.Color(195, 114, 133), BitmapUtils.usernameToColor("Ellena Wright Frederic Conway"))
+        assertEquals(BitmapUtils.Color(214, 180, 97), BitmapUtils.usernameToColor("Gianluca Hills"))
+        assertEquals(BitmapUtils.Color(214, 180, 97), BitmapUtils.usernameToColor("Haseeb Stephens"))
+        assertEquals(BitmapUtils.Color(151, 80, 164), BitmapUtils.usernameToColor("Idris Mac"))
+        assertEquals(BitmapUtils.Color(0, 130, 201), BitmapUtils.usernameToColor("Kristi Fisher"))
+        assertEquals(BitmapUtils.Color(188, 92, 145), BitmapUtils.usernameToColor("Lillian Wall"))
+        assertEquals(BitmapUtils.Color(221, 203, 85), BitmapUtils.usernameToColor("Lorelai Taylor"))
+        assertEquals(BitmapUtils.Color(151, 80, 164), BitmapUtils.usernameToColor("Madina Knight"))
+        assertEquals(BitmapUtils.Color(121, 90, 171), BitmapUtils.usernameToColor("Rae Hope"))
+        assertEquals(BitmapUtils.Color(188, 92, 145), BitmapUtils.usernameToColor("Santiago Singleton"))
+        assertEquals(BitmapUtils.Color(208, 158, 109), BitmapUtils.usernameToColor("Sid Combs"))
+        assertEquals(BitmapUtils.Color(30, 120, 193), BitmapUtils.usernameToColor("Vivienne Jacobs"))
+        assertEquals(BitmapUtils.Color(110, 166, 143), BitmapUtils.usernameToColor("Zaki Cortes"))
+        assertEquals(BitmapUtils.Color(91, 100, 179), BitmapUtils.usernameToColor("a user"))
+        assertEquals(BitmapUtils.Color(208, 158, 109), BitmapUtils.usernameToColor("admin"))
+        assertEquals(BitmapUtils.Color(151, 80, 164), BitmapUtils.usernameToColor("admin@cloud.example.com"))
+        assertEquals(BitmapUtils.Color(221, 203, 85), BitmapUtils.usernameToColor("another user"))
+        assertEquals(BitmapUtils.Color(36, 142, 181), BitmapUtils.usernameToColor("asd"))
+        assertEquals(BitmapUtils.Color(0, 130, 201), BitmapUtils.usernameToColor("bar"))
+        assertEquals(BitmapUtils.Color(208, 158, 109), BitmapUtils.usernameToColor("foo"))
+        assertEquals(BitmapUtils.Color(182, 70, 157), BitmapUtils.usernameToColor("wasd"))
+    }
+
+    @Test
+    @Suppress("MagicNumber")
+    fun checkEqual() {
+        assertEquals(BitmapUtils.Color(208, 158, 109), BitmapUtils.Color(208, 158, 109))
+        assertNotEquals(BitmapUtils.Color(208, 158, 109), BitmapUtils.Color(208, 158, 100))
+    }
+
+    @Test
+    @Suppress("MagicNumber")
+    fun checkHashCode() {
+        assertEquals(BitmapUtils.Color(208, 158, 109).hashCode(), BitmapUtils.Color(208, 158, 109).hashCode())
+        assertNotEquals(BitmapUtils.Color(208, 158, 109).hashCode(), BitmapUtils.Color(208, 158, 100).hashCode())
+    }
+}

+ 13 - 3
src/main/java/com/nextcloud/client/account/UserAccountManager.java

@@ -20,12 +20,15 @@
 package com.nextcloud.client.account;
 
 import android.accounts.Account;
+import android.accounts.AccountManager;
 import android.app.Activity;
 import android.content.Intent;
 
 import com.nextcloud.java.util.Optional;
+import com.owncloud.android.MainApp;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.accounts.AccountUtils;
 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
 
 import java.util.List;
@@ -154,13 +157,14 @@ public interface UserAccountManager extends CurrentAccountProvider {
 
     /**
      * Extract username from account.
-     *
+     * <p>
      * Full account name is in form of "username@nextcloud.domain".
      *
      * @param account Account instance
      * @return User name (without domain) or null, if name cannot be extracted.
      */
-    static String getUsername(Account account) {
+    static @Nullable
+    String getUsername(Account account) {
         if (account != null && account.name != null) {
             return account.name.substring(0, account.name.lastIndexOf('@'));
         } else {
@@ -168,9 +172,15 @@ public interface UserAccountManager extends CurrentAccountProvider {
         }
     }
 
+    static @Nullable
+    String getDisplayName(Account account) {
+        return AccountManager.get(MainApp.getAppContext()).getUserData(account,
+                                                                       AccountUtils.Constants.KEY_DISPLAY_NAME);
+    }
+
     /**
      * Launch account registration activity.
-     *
+     * <p>
      * This method returns immediately. Authenticator activity will be launched asynchronously.
      *
      * @param activity Activity used to launch authenticator flow via {@link Activity#startActivity(Intent)}

+ 1 - 4
src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java

@@ -1136,13 +1136,10 @@ public final class ThumbnailsCacheManager {
     }
 
     public static class AsyncMediaThumbnailDrawable extends BitmapDrawable {
-        private final WeakReference<MediaThumbnailGenerationTask> bitmapWorkerTaskReference;
 
-        public AsyncMediaThumbnailDrawable(Resources res, Bitmap bitmap,
-                                           MediaThumbnailGenerationTask bitmapWorkerTask) {
+        public AsyncMediaThumbnailDrawable(Resources res, Bitmap bitmap) {
 
             super(res, bitmap);
-            bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
         }
     }
 

+ 8 - 12
src/main/java/com/owncloud/android/ui/TextDrawable.java

@@ -69,19 +69,17 @@ public class TextDrawable extends Drawable {
      * Create a TextDrawable with the given radius.
      *
      * @param text   the text to be rendered
-     * @param r      rgb red value
-     * @param g      rgb green value
-     * @param b      rgb blue value
+     * @param color  color
      * @param radius circle radius
      */
-    public TextDrawable(String text, int r, int g, int b, float radius) {
+    public TextDrawable(String text, BitmapUtils.Color color, float radius) {
         mRadius = radius;
         mText = text;
 
         mBackground = new Paint();
         mBackground.setStyle(Paint.Style.FILL);
         mBackground.setAntiAlias(true);
-        mBackground.setColor(Color.rgb(r, g, b));
+        mBackground.setColor(Color.rgb(color.r, color.g, color.b));
 
         mTextPaint = new Paint();
         mTextPaint.setColor(Color.WHITE);
@@ -101,8 +99,9 @@ public class TextDrawable extends Drawable {
      */
     @NonNull
     @NextcloudServer(max = 12)
-    public static TextDrawable createAvatar(Account account, float radiusInDp) throws NoSuchAlgorithmException {
-        String username = UserAccountManager.getUsername(account);
+    public static TextDrawable createAvatar(Account account, float radiusInDp) throws
+            NoSuchAlgorithmException {
+        String username = UserAccountManager.getDisplayName(account);
         return createNamedAvatar(username, radiusInDp);
     }
 
@@ -132,11 +131,8 @@ public class TextDrawable extends Drawable {
      */
     @NonNull
     public static TextDrawable createNamedAvatar(String name, float radiusInDp) throws NoSuchAlgorithmException {
-        int[] hsl = BitmapUtils.calculateHSL(name);
-        int[] rgb = BitmapUtils.HSLtoRGB(hsl[0], hsl[1], hsl[2], 1);
-
-        return new TextDrawable(extractCharsFromDisplayName(name), rgb[0], rgb[1], rgb[2],
-                                radiusInDp);
+        BitmapUtils.Color color = BitmapUtils.usernameToColor(name);
+        return new TextDrawable(extractCharsFromDisplayName(name), color, radiusInDp);
     }
 
     @VisibleForTesting

+ 1 - 2
src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java

@@ -337,8 +337,7 @@ public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter<SectionedV
             ThumbnailsCacheManager.AsyncMediaThumbnailDrawable asyncDrawable =
                     new ThumbnailsCacheManager.AsyncMediaThumbnailDrawable(
                         context.getResources(),
-                        ThumbnailsCacheManager.mDefaultImg,
-                        task
+                        ThumbnailsCacheManager.mDefaultImg
                     );
             holder.image.setImageDrawable(asyncDrawable);
 

+ 103 - 162
src/main/java/com/owncloud/android/utils/BitmapUtils.java

@@ -39,9 +39,11 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Locale;
 
+import androidx.annotation.Nullable;
 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
 import androidx.exifinterface.media.ExifInterface;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
  * Utility class with methods for decoding Bitmaps.
@@ -49,24 +51,17 @@ import androidx.exifinterface.media.ExifInterface;
 public final class BitmapUtils {
     public static final String TAG = BitmapUtils.class.getSimpleName();
 
-    private static final int INDEX_RED = 0;
-    private static final int INDEX_GREEN = 1;
-    private static final int INDEX_BLUE = 2;
-    private static final int INDEX_HUE = 0;
-    private static final int INDEX_SATURATION = 1;
-    private static final int INDEX_LUMINATION = 2;
-
     private BitmapUtils() {
         // utility class -> private constructor
     }
 
     /**
-     * Decodes a bitmap from a file containing it minimizing the memory use, known that the bitmap
-     * will be drawn in a surface of reqWidth x reqHeight
+     * Decodes a bitmap from a file containing it minimizing the memory use, known that the bitmap will be drawn in a
+     * surface of reqWidth x reqHeight
      *
-     * @param srcPath       Absolute path to the file containing the image.
-     * @param reqWidth      Width of the surface where the Bitmap will be drawn on, in pixels.
-     * @param reqHeight     Height of the surface where the Bitmap will be drawn on, in pixels.
+     * @param srcPath   Absolute path to the file containing the image.
+     * @param reqWidth  Width of the surface where the Bitmap will be drawn on, in pixels.
+     * @param reqHeight Height of the surface where the Bitmap will be drawn on, in pixels.
      * @return decoded bitmap
      */
     public static Bitmap decodeSampledBitmapFromFile(String srcPath, int reqWidth, int reqHeight) {
@@ -93,16 +88,14 @@ public final class BitmapUtils {
 
 
     /**
-     * Calculates a proper value for options.inSampleSize in order to decode a Bitmap minimizing
-     * the memory overload and covering a target surface of reqWidth x reqHeight if the original
-     * image is big enough.
+     * Calculates a proper value for options.inSampleSize in order to decode a Bitmap minimizing the memory overload and
+     * covering a target surface of reqWidth x reqHeight if the original image is big enough.
      *
-     * @param options       Bitmap decoding options; options.outHeight and options.inHeight should
-     *                      be set.
-     * @param reqWidth      Width of the surface where the Bitmap will be drawn on, in pixels.
-     * @param reqHeight     Height of the surface where the Bitmap will be drawn on, in pixels.
-     * @return The largest inSampleSize value that is a power of 2 and keeps both
-     *                      height and width larger than reqWidth and reqHeight.
+     * @param options   Bitmap decoding options; options.outHeight and options.inHeight should be set.
+     * @param reqWidth  Width of the surface where the Bitmap will be drawn on, in pixels.
+     * @param reqHeight Height of the surface where the Bitmap will be drawn on, in pixels.
+     * @return The largest inSampleSize value that is a power of 2 and keeps both height and width larger than reqWidth
+     * and reqHeight.
      */
     public static int calculateSampleFactor(Options options, int reqWidth, int reqHeight) {
 
@@ -142,9 +135,9 @@ public final class BitmapUtils {
     }
 
     /**
-     * Rotate bitmap according to EXIF orientation.
-     * Cf. http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
-     * @param bitmap Bitmap to be rotated
+     * Rotate bitmap according to EXIF orientation. Cf. http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
+     *
+     * @param bitmap      Bitmap to be rotated
      * @param storagePath Path to source file of bitmap. Needed for EXIF information.
      * @return correctly EXIF-rotated bitmap
      */
@@ -201,167 +194,115 @@ public final class BitmapUtils {
         return resultBitmap;
     }
 
-    /**
-     *  Convert HSL values to a RGB Color.
-     *
-     *  @param h Hue is specified as degrees in the range 0 - 360.
-     *  @param s Saturation is specified as a percentage in the range 1 - 100.
-     *  @param l Luminance is specified as a percentage in the range 1 - 100.
-     *  @param alpha  the alpha value between 0 - 1
-     *  adapted from https://svn.codehaus.org/griffon/builders/gfxbuilder/tags/GFXBUILDER_0.2/
-     *  gfxbuilder-core/src/main/com/camick/awt/HSLColor.java
-     */
-    @SuppressWarnings("PMD.MethodNamingConventions")
-    public static int[] HSLtoRGB(float h, float s, float l, float alpha) {
-        if (s < 0.0f || s > 100.0f) {
-            String message = "Color parameter outside of expected range - Saturation";
-            throw new IllegalArgumentException(message);
-        }
-
-        if (l < 0.0f || l > 100.0f) {
-            String message = "Color parameter outside of expected range - Luminance";
-            throw new IllegalArgumentException(message);
-        }
-
-        if (alpha < 0.0f || alpha > 1.0f) {
-            String message = "Color parameter outside of expected range - Alpha";
-            throw new IllegalArgumentException(message);
-        }
-
-        //  Formula needs all values between 0 - 1.
-
-        h = h % 360.0f;
-        h /= 360f;
-        s /= 100f;
-        l /= 100f;
-
-        float q;
+    public static Color usernameToColor(String name) throws NoSuchAlgorithmException {
+        String hash = name.toLowerCase(Locale.ROOT);
 
-        if (l < 0.5) {
-            q = l * (1 + s);
-        } else {
-            q = (l + s) - (s * l);
+        // already a md5 hash?
+        if (!hash.matches("([0-9a-f]{4}-?){8}$")) {
+            hash = md5(hash);
         }
 
-        float p = 2 * l - q;
+        hash = hash.replaceAll("[^0-9a-f]", "");
+        int steps = 6;
 
-        int r = Math.round(Math.max(0, HueToRGB(p, q, h + (1.0f / 3.0f)) * 256));
-        int g = Math.round(Math.max(0, HueToRGB(p, q, h) * 256));
-        int b = Math.round(Math.max(0, HueToRGB(p, q, h - (1.0f / 3.0f)) * 256));
+        Color[] finalPalette = generateColors(steps);
 
-        return new int[]{r, g, b};
+        return finalPalette[hashToInt(hash, steps * 3)];
     }
 
-    @SuppressWarnings("PMD.MethodNamingConventions")
-    private static float HueToRGB(float p, float q, float h) {
-        if (h < 0) {
-            h += 1;
-        }
-
-        if (h > 1) {
-            h -= 1;
-        }
-
-        if (6 * h < 1) {
-            return p + ((q - p) * 6 * h);
-        }
+    private static int hashToInt(String hash, int maximum) {
+        int finalInt = 0;
+        int[] result = new int[hash.length()];
 
-        if (2 * h < 1) {
-            return q;
+        // splitting evenly the string
+        for (int i = 0; i < hash.length(); i++) {
+            // chars in md5 goes up to f, hex: 16
+            result[i] = Integer.parseInt(String.valueOf(hash.charAt(i)), 16) % 16;
         }
 
-        if (3 * h < 2) {
-            return p + ((q - p) * 6 * (2.0f / 3.0f - h));
+        // adds up all results
+        for (int value : result) {
+            finalInt += value;
         }
 
-        return p;
+        // chars in md5 goes up to f, hex:16
+        // make sure we're always using int in our operation
+        return Integer.parseInt(String.valueOf(Integer.parseInt(String.valueOf(finalInt), 10) % maximum), 10);
     }
 
-    /**
-     * calculates the RGB value based on a given account name.
-     *
-     * @param name The name
-     * @return corresponding RGB color
-     * @throws NoSuchAlgorithmException     if the specified algorithm is not available
-     */
-    public static int[] calculateHSL(String name) throws NoSuchAlgorithmException {
-        // using adapted algorithm from https://github.com/nextcloud/server/blob/master/core/js/placeholder.js#L126
-
-        String[] result = new String[]{"0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"};
-        double[] rgb = new double[]{0, 0, 0};
-        int sat = 70;
-        int lum = 68;
-        int modulo = 16;
+    private static Color[] generateColors(int steps) {
+        Color red = new Color(182, 70, 157);
+        Color yellow = new Color(221, 203, 85);
+        Color blue = new Color(0, 130, 201); // Nextcloud blue
+
+        Color[] palette1 = mixPalette(steps, red, yellow);
+        Color[] palette2 = mixPalette(steps, yellow, blue);
+        Color[] palette3 = mixPalette(steps, blue, red);
+
+        Color[] resultPalette = new Color[palette1.length + palette2.length + palette3.length];
+        System.arraycopy(palette1, 0, resultPalette, 0, palette1.length);
+        System.arraycopy(palette2, 0, resultPalette, palette1.length, palette2.length);
+        System.arraycopy(palette3,
+                         0,
+                         resultPalette,
+                         palette1.length + palette2.length,
+                         palette1.length);
+
+        return resultPalette;
+    }
 
-        String hash = name.toLowerCase(Locale.ROOT).replaceAll("[^0-9a-f]", "");
+    @SuppressFBWarnings("CLI_CONSTANT_LIST_INDEX")
+    private static Color[] mixPalette(int steps, Color color1, Color color2) {
+        Color[] palette = new Color[steps];
+        palette[0] = color1;
 
-        if (!hash.matches("^[0-9a-f]{32}")) {
-            hash = md5(hash);
-        }
+        float[] step = stepCalc(steps, color1, color2);
+        for (int i = 1; i < steps; i++) {
+            int r = (int) (color1.r + step[0] * i);
+            int g = (int) (color1.g + step[1] * i);
+            int b = (int) (color1.b + step[2] * i);
 
-        // Splitting evenly the string
-        for (int i = 0; i < hash.length(); i++) {
-            result[i % modulo] = result[i % modulo] + Integer.parseInt(hash.substring(i, i + 1), 16);
+            palette[i] = new Color(r, g, b);
         }
 
-        // Converting our data into a usable rgb format
-        // Start at 1 because 16%3=1 but 15%3=0 and makes the repartition even
-        for (int count = 1; count < modulo; count++) {
-            rgb[count % 3] += Integer.parseInt(result[count]);
-        }
-
-        // Reduce values bigger than rgb requirements
-        rgb[INDEX_RED] = rgb[INDEX_RED] % 255;
-        rgb[INDEX_GREEN] = rgb[INDEX_GREEN] % 255;
-        rgb[INDEX_BLUE] = rgb[INDEX_BLUE] % 255;
+        return palette;
+    }
 
-        double[] hsl = rgbToHsl(rgb[INDEX_RED], rgb[INDEX_GREEN], rgb[INDEX_BLUE]);
+    private static float[] stepCalc(int steps, Color color1, Color color2) {
+        float[] step = new float[3];
 
-        // Classic formula to check the brightness for our eye
-        // If too bright, lower the sat
-        double bright = Math.sqrt(0.299 * Math.pow(rgb[INDEX_RED], 2) + 0.587 * Math.pow(rgb[INDEX_GREEN], 2) + 0.114
-                * Math.pow(rgb[INDEX_BLUE], 2));
+        step[0] = (color2.r - color1.r) / (float) steps;
+        step[1] = (color2.g - color1.g) / (float) steps;
+        step[2] = (color2.b - color1.b) / (float) steps;
 
-        if (bright >= 200) {
-            sat = 60;
-        }
-
-        return new int[]{(int) (hsl[INDEX_HUE] * 360), sat, lum};
+        return step;
     }
 
-    private static double[] rgbToHsl(double rUntrimmed, double gUntrimmed, double bUntrimmed) {
-        double r = rUntrimmed / 255;
-        double g = gUntrimmed / 255;
-        double b = bUntrimmed / 255;
+    public static class Color {
+        public int r;
+        public int g;
+        public int b;
 
-        double max = Math.max(r, Math.max(g, b));
-        double min = Math.min(r, Math.min(g, b));
-        double h = (max + min) / 2;
-        double s;
-        double l = (max + min) / 2;
+        public Color(int r, int g, int b) {
+            this.r = r;
+            this.g = g;
+            this.b = b;
+        }
 
-        if (max == min) {
-            h = s = 0; // achromatic
-        } else {
-            double d = max - min;
-            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
-
-            if (max == r) {
-                h = (g - b) / d + (g < b ? 6 : 0);
-            } else if (max == g) {
-                h = (b - r) / d + 2;
-            } else if (max == b) {
-                h = (r - g) / d + 4;
+        @Override
+        public boolean equals(@Nullable Object obj) {
+            if (!(obj instanceof Color)) {
+                return false;
             }
-            h /= 6;
-        }
 
-        double[] hsl = new double[]{0.0, 0.0, 0.0};
-        hsl[INDEX_HUE] = h;
-        hsl[INDEX_SATURATION] = s;
-        hsl[INDEX_LUMINATION] = l;
+            Color other = (Color) obj;
+            return this.r == other.r && this.g == other.g && this.b == other.b;
+        }
 
-        return hsl;
+        @Override
+        public int hashCode() {
+            return r * 10000 + g * 1000 + b;
+        }
     }
 
     public static String md5(String string) throws NoSuchAlgorithmException {
@@ -372,11 +313,11 @@ public final class BitmapUtils {
     }
 
     /**
-     * Returns a new circular bitmap drawable by creating it from a bitmap, setting initial target density based on
-     * the display metrics of the resources.
+     * Returns a new circular bitmap drawable by creating it from a bitmap, setting initial target density based on the
+     * display metrics of the resources.
      *
      * @param resources the resources for initial target density
-     * @param bitmap the original bitmap
+     * @param bitmap    the original bitmap
      * @return the circular bitmap
      */
     public static RoundedBitmapDrawable bitmapToCircularBitmapDrawable(Resources resources,
@@ -420,7 +361,7 @@ public final class BitmapUtils {
             bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
         } else {
             bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
-                    Bitmap.Config.ARGB_8888);
+                                         Bitmap.Config.ARGB_8888);
         }
 
         Canvas canvas = new Canvas(bitmap);
@@ -436,14 +377,14 @@ public final class BitmapUtils {
                                      imageView);
     }
 
-    public static void setRoundedBitmapForGridMode(Bitmap thumbnail, ImageView imageView){
+    public static void setRoundedBitmapForGridMode(Bitmap thumbnail, ImageView imageView) {
         BitmapUtils.setRoundedBitmap(getResources(),
                                      thumbnail,
                                      getResources().getDimension(R.dimen.file_icon_rounded_corner_radius_for_grid_mode),
                                      imageView);
     }
 
-    private static Resources getResources(){
+    private static Resources getResources() {
         return MainApp.getAppContext().getResources();
     }
 }