- 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>
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
@ -1 +1 @@
|
||||||
326
|
320
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,12 +20,15 @@
|
||||||
package com.nextcloud.client.account;
|
package com.nextcloud.client.account;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
|
import android.accounts.AccountManager;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
import com.nextcloud.java.util.Optional;
|
import com.nextcloud.java.util.Optional;
|
||||||
|
import com.owncloud.android.MainApp;
|
||||||
import com.owncloud.android.datamodel.OCFile;
|
import com.owncloud.android.datamodel.OCFile;
|
||||||
import com.owncloud.android.lib.common.OwnCloudAccount;
|
import com.owncloud.android.lib.common.OwnCloudAccount;
|
||||||
|
import com.owncloud.android.lib.common.accounts.AccountUtils;
|
||||||
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
|
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -154,13 +157,14 @@ public interface UserAccountManager extends CurrentAccountProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract username from account.
|
* Extract username from account.
|
||||||
*
|
* <p>
|
||||||
* Full account name is in form of "username@nextcloud.domain".
|
* Full account name is in form of "username@nextcloud.domain".
|
||||||
*
|
*
|
||||||
* @param account Account instance
|
* @param account Account instance
|
||||||
* @return User name (without domain) or null, if name cannot be extracted.
|
* @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) {
|
if (account != null && account.name != null) {
|
||||||
return account.name.substring(0, account.name.lastIndexOf('@'));
|
return account.name.substring(0, account.name.lastIndexOf('@'));
|
||||||
} else {
|
} 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.
|
* Launch account registration activity.
|
||||||
*
|
* <p>
|
||||||
* This method returns immediately. Authenticator activity will be launched asynchronously.
|
* This method returns immediately. Authenticator activity will be launched asynchronously.
|
||||||
*
|
*
|
||||||
* @param activity Activity used to launch authenticator flow via {@link Activity#startActivity(Intent)}
|
* @param activity Activity used to launch authenticator flow via {@link Activity#startActivity(Intent)}
|
||||||
|
|
|
@ -1136,13 +1136,10 @@ public final class ThumbnailsCacheManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AsyncMediaThumbnailDrawable extends BitmapDrawable {
|
public static class AsyncMediaThumbnailDrawable extends BitmapDrawable {
|
||||||
private final WeakReference<MediaThumbnailGenerationTask> bitmapWorkerTaskReference;
|
|
||||||
|
|
||||||
public AsyncMediaThumbnailDrawable(Resources res, Bitmap bitmap,
|
public AsyncMediaThumbnailDrawable(Resources res, Bitmap bitmap) {
|
||||||
MediaThumbnailGenerationTask bitmapWorkerTask) {
|
|
||||||
|
|
||||||
super(res, bitmap);
|
super(res, bitmap);
|
||||||
bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,19 +69,17 @@ public class TextDrawable extends Drawable {
|
||||||
* Create a TextDrawable with the given radius.
|
* Create a TextDrawable with the given radius.
|
||||||
*
|
*
|
||||||
* @param text the text to be rendered
|
* @param text the text to be rendered
|
||||||
* @param r rgb red value
|
* @param color color
|
||||||
* @param g rgb green value
|
|
||||||
* @param b rgb blue value
|
|
||||||
* @param radius circle radius
|
* @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;
|
mRadius = radius;
|
||||||
mText = text;
|
mText = text;
|
||||||
|
|
||||||
mBackground = new Paint();
|
mBackground = new Paint();
|
||||||
mBackground.setStyle(Paint.Style.FILL);
|
mBackground.setStyle(Paint.Style.FILL);
|
||||||
mBackground.setAntiAlias(true);
|
mBackground.setAntiAlias(true);
|
||||||
mBackground.setColor(Color.rgb(r, g, b));
|
mBackground.setColor(Color.rgb(color.r, color.g, color.b));
|
||||||
|
|
||||||
mTextPaint = new Paint();
|
mTextPaint = new Paint();
|
||||||
mTextPaint.setColor(Color.WHITE);
|
mTextPaint.setColor(Color.WHITE);
|
||||||
|
@ -101,8 +99,9 @@ public class TextDrawable extends Drawable {
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
@NextcloudServer(max = 12)
|
@NextcloudServer(max = 12)
|
||||||
public static TextDrawable createAvatar(Account account, float radiusInDp) throws NoSuchAlgorithmException {
|
public static TextDrawable createAvatar(Account account, float radiusInDp) throws
|
||||||
String username = UserAccountManager.getUsername(account);
|
NoSuchAlgorithmException {
|
||||||
|
String username = UserAccountManager.getDisplayName(account);
|
||||||
return createNamedAvatar(username, radiusInDp);
|
return createNamedAvatar(username, radiusInDp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,11 +131,8 @@ public class TextDrawable extends Drawable {
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static TextDrawable createNamedAvatar(String name, float radiusInDp) throws NoSuchAlgorithmException {
|
public static TextDrawable createNamedAvatar(String name, float radiusInDp) throws NoSuchAlgorithmException {
|
||||||
int[] hsl = BitmapUtils.calculateHSL(name);
|
BitmapUtils.Color color = BitmapUtils.usernameToColor(name);
|
||||||
int[] rgb = BitmapUtils.HSLtoRGB(hsl[0], hsl[1], hsl[2], 1);
|
return new TextDrawable(extractCharsFromDisplayName(name), color, radiusInDp);
|
||||||
|
|
||||||
return new TextDrawable(extractCharsFromDisplayName(name), rgb[0], rgb[1], rgb[2],
|
|
||||||
radiusInDp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
|
@ -337,8 +337,7 @@ public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter<SectionedV
|
||||||
ThumbnailsCacheManager.AsyncMediaThumbnailDrawable asyncDrawable =
|
ThumbnailsCacheManager.AsyncMediaThumbnailDrawable asyncDrawable =
|
||||||
new ThumbnailsCacheManager.AsyncMediaThumbnailDrawable(
|
new ThumbnailsCacheManager.AsyncMediaThumbnailDrawable(
|
||||||
context.getResources(),
|
context.getResources(),
|
||||||
ThumbnailsCacheManager.mDefaultImg,
|
ThumbnailsCacheManager.mDefaultImg
|
||||||
task
|
|
||||||
);
|
);
|
||||||
holder.image.setImageDrawable(asyncDrawable);
|
holder.image.setImageDrawable(asyncDrawable);
|
||||||
|
|
||||||
|
|
|
@ -39,9 +39,11 @@ import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
||||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||||
import androidx.exifinterface.media.ExifInterface;
|
import androidx.exifinterface.media.ExifInterface;
|
||||||
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class with methods for decoding Bitmaps.
|
* Utility class with methods for decoding Bitmaps.
|
||||||
|
@ -49,20 +51,13 @@ import androidx.exifinterface.media.ExifInterface;
|
||||||
public final class BitmapUtils {
|
public final class BitmapUtils {
|
||||||
public static final String TAG = BitmapUtils.class.getSimpleName();
|
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() {
|
private BitmapUtils() {
|
||||||
// utility class -> private constructor
|
// utility class -> private constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes a bitmap from a file containing it minimizing the memory use, known that the bitmap
|
* Decodes a bitmap from a file containing it minimizing the memory use, known that the bitmap will be drawn in a
|
||||||
* will be drawn in a surface of reqWidth x reqHeight
|
* surface of reqWidth x reqHeight
|
||||||
*
|
*
|
||||||
* @param srcPath Absolute path to the file containing the image.
|
* @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 reqWidth Width of the surface where the Bitmap will be drawn on, in pixels.
|
||||||
|
@ -93,16 +88,14 @@ public final class BitmapUtils {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates a proper value for options.inSampleSize in order to decode a Bitmap minimizing
|
* Calculates a proper value for options.inSampleSize in order to decode a Bitmap minimizing the memory overload and
|
||||||
* the memory overload and covering a target surface of reqWidth x reqHeight if the original
|
* covering a target surface of reqWidth x reqHeight if the original image is big enough.
|
||||||
* image is big enough.
|
|
||||||
*
|
*
|
||||||
* @param options Bitmap decoding options; options.outHeight and options.inHeight should
|
* @param options Bitmap decoding options; options.outHeight and options.inHeight should be set.
|
||||||
* be set.
|
|
||||||
* @param reqWidth Width of the surface where the Bitmap will be drawn on, in pixels.
|
* @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 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
|
* @return The largest inSampleSize value that is a power of 2 and keeps both height and width larger than reqWidth
|
||||||
* height and width larger than reqWidth and reqHeight.
|
* and reqHeight.
|
||||||
*/
|
*/
|
||||||
public static int calculateSampleFactor(Options options, int reqWidth, int reqHeight) {
|
public static int calculateSampleFactor(Options options, int reqWidth, int reqHeight) {
|
||||||
|
|
||||||
|
@ -142,8 +135,8 @@ public final class BitmapUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rotate bitmap according to EXIF orientation.
|
* Rotate bitmap according to EXIF orientation. Cf. http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
|
||||||
* Cf. http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
|
*
|
||||||
* @param bitmap Bitmap to be rotated
|
* @param bitmap Bitmap to be rotated
|
||||||
* @param storagePath Path to source file of bitmap. Needed for EXIF information.
|
* @param storagePath Path to source file of bitmap. Needed for EXIF information.
|
||||||
* @return correctly EXIF-rotated bitmap
|
* @return correctly EXIF-rotated bitmap
|
||||||
|
@ -201,167 +194,115 @@ public final class BitmapUtils {
|
||||||
return resultBitmap;
|
return resultBitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static Color usernameToColor(String name) throws NoSuchAlgorithmException {
|
||||||
* Convert HSL values to a RGB Color.
|
String hash = name.toLowerCase(Locale.ROOT);
|
||||||
*
|
|
||||||
* @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) {
|
// already a md5 hash?
|
||||||
String message = "Color parameter outside of expected range - Luminance";
|
if (!hash.matches("([0-9a-f]{4}-?){8}$")) {
|
||||||
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;
|
|
||||||
|
|
||||||
if (l < 0.5) {
|
|
||||||
q = l * (1 + s);
|
|
||||||
} else {
|
|
||||||
q = (l + s) - (s * l);
|
|
||||||
}
|
|
||||||
|
|
||||||
float p = 2 * l - q;
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
return new int[]{r, g, b};
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (2 * h < 1) {
|
|
||||||
return q;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (3 * h < 2) {
|
|
||||||
return p + ((q - p) * 6 * (2.0f / 3.0f - h));
|
|
||||||
}
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
String hash = name.toLowerCase(Locale.ROOT).replaceAll("[^0-9a-f]", "");
|
|
||||||
|
|
||||||
if (!hash.matches("^[0-9a-f]{32}")) {
|
|
||||||
hash = md5(hash);
|
hash = md5(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splitting evenly the string
|
hash = hash.replaceAll("[^0-9a-f]", "");
|
||||||
|
int steps = 6;
|
||||||
|
|
||||||
|
Color[] finalPalette = generateColors(steps);
|
||||||
|
|
||||||
|
return finalPalette[hashToInt(hash, steps * 3)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int hashToInt(String hash, int maximum) {
|
||||||
|
int finalInt = 0;
|
||||||
|
int[] result = new int[hash.length()];
|
||||||
|
|
||||||
|
// splitting evenly the string
|
||||||
for (int i = 0; i < hash.length(); i++) {
|
for (int i = 0; i < hash.length(); i++) {
|
||||||
result[i % modulo] = result[i % modulo] + Integer.parseInt(hash.substring(i, i + 1), 16);
|
// chars in md5 goes up to f, hex: 16
|
||||||
|
result[i] = Integer.parseInt(String.valueOf(hash.charAt(i)), 16) % 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converting our data into a usable rgb format
|
// adds up all results
|
||||||
// Start at 1 because 16%3=1 but 15%3=0 and makes the repartition even
|
for (int value : result) {
|
||||||
for (int count = 1; count < modulo; count++) {
|
finalInt += value;
|
||||||
rgb[count % 3] += Integer.parseInt(result[count]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduce values bigger than rgb requirements
|
// chars in md5 goes up to f, hex:16
|
||||||
rgb[INDEX_RED] = rgb[INDEX_RED] % 255;
|
// make sure we're always using int in our operation
|
||||||
rgb[INDEX_GREEN] = rgb[INDEX_GREEN] % 255;
|
return Integer.parseInt(String.valueOf(Integer.parseInt(String.valueOf(finalInt), 10) % maximum), 10);
|
||||||
rgb[INDEX_BLUE] = rgb[INDEX_BLUE] % 255;
|
|
||||||
|
|
||||||
double[] hsl = rgbToHsl(rgb[INDEX_RED], rgb[INDEX_GREEN], rgb[INDEX_BLUE]);
|
|
||||||
|
|
||||||
// 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));
|
|
||||||
|
|
||||||
if (bright >= 200) {
|
|
||||||
sat = 60;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new int[]{(int) (hsl[INDEX_HUE] * 360), sat, lum};
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double[] rgbToHsl(double rUntrimmed, double gUntrimmed, double bUntrimmed) {
|
@SuppressFBWarnings("CLI_CONSTANT_LIST_INDEX")
|
||||||
double r = rUntrimmed / 255;
|
private static Color[] mixPalette(int steps, Color color1, Color color2) {
|
||||||
double g = gUntrimmed / 255;
|
Color[] palette = new Color[steps];
|
||||||
double b = bUntrimmed / 255;
|
palette[0] = color1;
|
||||||
|
|
||||||
double max = Math.max(r, Math.max(g, b));
|
float[] step = stepCalc(steps, color1, color2);
|
||||||
double min = Math.min(r, Math.min(g, b));
|
for (int i = 1; i < steps; i++) {
|
||||||
double h = (max + min) / 2;
|
int r = (int) (color1.r + step[0] * i);
|
||||||
double s;
|
int g = (int) (color1.g + step[1] * i);
|
||||||
double l = (max + min) / 2;
|
int b = (int) (color1.b + step[2] * i);
|
||||||
|
|
||||||
if (max == min) {
|
palette[i] = new Color(r, g, b);
|
||||||
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;
|
|
||||||
}
|
|
||||||
h /= 6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double[] hsl = new double[]{0.0, 0.0, 0.0};
|
return palette;
|
||||||
hsl[INDEX_HUE] = h;
|
}
|
||||||
hsl[INDEX_SATURATION] = s;
|
|
||||||
hsl[INDEX_LUMINATION] = l;
|
|
||||||
|
|
||||||
return hsl;
|
private static float[] stepCalc(int steps, Color color1, Color color2) {
|
||||||
|
float[] step = new float[3];
|
||||||
|
|
||||||
|
step[0] = (color2.r - color1.r) / (float) steps;
|
||||||
|
step[1] = (color2.g - color1.g) / (float) steps;
|
||||||
|
step[2] = (color2.b - color1.b) / (float) steps;
|
||||||
|
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Color {
|
||||||
|
public int r;
|
||||||
|
public int g;
|
||||||
|
public int b;
|
||||||
|
|
||||||
|
public Color(int r, int g, int b) {
|
||||||
|
this.r = r;
|
||||||
|
this.g = g;
|
||||||
|
this.b = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (!(obj instanceof Color)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color other = (Color) obj;
|
||||||
|
return this.r == other.r && this.g == other.g && this.b == other.b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return r * 10000 + g * 1000 + b;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String md5(String string) throws NoSuchAlgorithmException {
|
public static String md5(String string) throws NoSuchAlgorithmException {
|
||||||
|
@ -372,8 +313,8 @@ public final class BitmapUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new circular bitmap drawable by creating it from a bitmap, setting initial target density based on
|
* Returns a new circular bitmap drawable by creating it from a bitmap, setting initial target density based on the
|
||||||
* the display metrics of the resources.
|
* display metrics of the resources.
|
||||||
*
|
*
|
||||||
* @param resources the resources for initial target density
|
* @param resources the resources for initial target density
|
||||||
* @param bitmap the original bitmap
|
* @param bitmap the original bitmap
|
||||||
|
|