Merge pull request #6817 from nextcloud/avatar

Avatar: use display name & adapted algorithm to server one
This commit is contained in:
Tobias Kaminsky 2020-09-03 10:21:57 +02:00 committed by GitHub
commit 33ece4a557
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 223 additions and 201 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1 +1 @@
326
320

View file

@ -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())
}
}

View file

@ -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)}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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);

View file

@ -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,20 +51,13 @@ 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.
@ -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 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.
* @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,8 +135,8 @@ 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/
* 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);
}
public static Color usernameToColor(String name) throws NoSuchAlgorithmException {
String hash = name.toLowerCase(Locale.ROOT);
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;
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}")) {
// already a md5 hash?
if (!hash.matches("([0-9a-f]{4}-?){8}$")) {
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++) {
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
// 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]);
// adds up all results
for (int value : result) {
finalInt += value;
}
// 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;
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;
// 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);
}
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) {
double r = rUntrimmed / 255;
double g = gUntrimmed / 255;
double b = bUntrimmed / 255;
@SuppressFBWarnings("CLI_CONSTANT_LIST_INDEX")
private static Color[] mixPalette(int steps, Color color1, Color color2) {
Color[] palette = new Color[steps];
palette[0] = color1;
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;
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);
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;
}
h /= 6;
palette[i] = new Color(r, g, b);
}
double[] hsl = new double[]{0.0, 0.0, 0.0};
hsl[INDEX_HUE] = h;
hsl[INDEX_SATURATION] = s;
hsl[INDEX_LUMINATION] = l;
return palette;
}
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 {
@ -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
* 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