blob: f88328b3f4439ae2fb79ebc2f676d30fd28b0376 [file] [log] [blame]
/*
* Copyright (C) 2004-2020 Savoir-faire Linux Inc.
*
* Author: Adrien BĂ©raud <adrien.beraud@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package cx.ring.views;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import cx.ring.R;
import cx.ring.model.Account;
import cx.ring.model.CallContact;
import cx.ring.model.Conversation;
import cx.ring.services.VCardServiceImpl;
import cx.ring.utils.DeviceUtils;
import cx.ring.utils.HashUtils;
import io.reactivex.Single;
import android.graphics.drawable.VectorDrawable;
import android.text.TextUtils;
import android.util.TypedValue;
import java.util.List;
public class AvatarDrawable extends Drawable {
private static final int SIZE_AB = 36;
private static final float DEFAULT_TEXT_SIZE_PERCENTAGE = 0.5f;
private static final int PLACEHOLDER_ICON = R.drawable.baseline_account_crop_24;
private static final int CHECKED_ICON = R.drawable.baseline_check_circle_24;
private static final int PRESENCE_COLOR = R.color.green_A700;
private static final int[] contactColors = {
R.color.red_500, R.color.pink_500,
R.color.purple_500, R.color.deep_purple_500,
R.color.indigo_500, R.color.blue_500,
R.color.cyan_500, R.color.teal_500,
R.color.green_500, R.color.light_green_500,
R.color.grey_500, R.color.lime_500,
R.color.amber_500, R.color.deep_orange_500,
R.color.brown_500, R.color.blue_grey_500
};
private static class PresenceIndicatorInfo { int cx, cy, radius; };
private final PresenceIndicatorInfo presence = new PresenceIndicatorInfo();
private boolean update = true;
private int inSize = -1;
private final int minSize;
private Bitmap workspace;
private Bitmap bitmap;
private VectorDrawable placeholder;
private VectorDrawable checkedIcon;
private final RectF backgroundBounds = new RectF();
private String avatarText;
private float textStartXPoint;
private float textStartYPoint;
private int color;
private final Paint clipPaint = new Paint();
private final Paint textPaint = new Paint();
private static final Paint drawPaint = new Paint();
private final Paint presenceFillPaint;
private final Paint presenceStrokePaint;
private final Paint checkedPaint;
static {
drawPaint.setAntiAlias(true);
drawPaint.setFilterBitmap(true);
}
private final boolean cropCircle;
private boolean isOnline;
private boolean isChecked;
private boolean showPresence;
public static class Builder {
private Bitmap photo = null;
private String name = null;
private String id = null;
private boolean circleCrop = false;
private boolean isOnline = false;
private boolean showPresence = true;
private boolean isChecked = false;
public Builder() {}
public Builder withId(String id) {
this.id = id;
return this;
}
public Builder withPhoto(Bitmap photo) {
this.photo = photo;
return this;
}
public Builder withName(String name) {
this.name = name;
return this;
}
public Builder withCircleCrop(boolean crop) {
this.circleCrop = crop;
return this;
}
public Builder withOnlineState(boolean isOnline) {
this.isOnline = isOnline;
return this;
}
public Builder withPresence(boolean showPresence) {
this.showPresence = showPresence;
return this;
}
public Builder withCheck(boolean checked) {
this.isChecked = checked;
return this;
}
public Builder withNameData(String profileName, String username) {
return withName(TextUtils.isEmpty(profileName) ? username : profileName);
}
public Builder withContact(CallContact contact){
return withPhoto((Bitmap)contact.getPhoto())
.withId(contact.getPrimaryNumber())
.withOnlineState(contact.isOnline())
.withNameData(contact.getProfileName(), contact.getUsername());
}
public Builder withContacts(List<CallContact> contacts) {
if (contacts.size() == 1) {
return withContact(contacts.get(0));
} else {
return withName("G");
}
}
public Builder withConversation(Conversation conversation) {
return withContacts(conversation.getContacts());
}
public AvatarDrawable build(Context context) {
AvatarDrawable avatarDrawable = new AvatarDrawable(
context, photo, name, id, circleCrop);
avatarDrawable.setOnline(isOnline);
avatarDrawable.setChecked(isChecked);
avatarDrawable.showPresence = this.showPresence;
return avatarDrawable;
}
public Single<AvatarDrawable> buildAsync(Context context) {
return Single.fromCallable(() -> build(context));
}
}
public static Single<AvatarDrawable> load(Context context, Account account, boolean crop) {
return VCardServiceImpl.loadProfile(account)
.map(data -> new Builder()
.withPhoto((Bitmap)data.second)
.withNameData(data.first, account.getRegisteredName())
.withId(account.getUri())
.withCircleCrop(crop)
.build(context));
}
public static Single<AvatarDrawable> load(Context context, Account account) {
return load(context, account, true);
}
public void update(CallContact contact) {
String profileName = contact.getProfileName();
String username = contact.getUsername();
avatarText = convertNameToAvatarText(
TextUtils.isEmpty(profileName) ? username : profileName);
bitmap = (Bitmap)contact.getPhoto();
isOnline = contact.isOnline();
update = true;
}
public void setName(String name) {
avatarText = convertNameToAvatarText(name);
update = true;
}
public void setPhoto(Bitmap photo) {
bitmap = photo;
update = true;
}
public void setOnline(boolean online) {
isOnline = online;
}
public void setChecked(boolean checked) {
isChecked = checked;
}
private AvatarDrawable(Context context, Bitmap photo, String name, String id, boolean crop) {
cropCircle = crop;
minSize = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, SIZE_AB, context.getResources().getDisplayMetrics());
if (photo != null) {
avatarText = null;
bitmap = photo;
} else {
bitmap = null;
avatarText = convertNameToAvatarText(name);
color = ContextCompat.getColor(context, getAvatarColor(id));
if (avatarText == null) {
placeholder = (VectorDrawable) context.getDrawable(PLACEHOLDER_ICON);
} else {
textPaint.setColor(Color.WHITE);
textPaint.setTypeface(Typeface.SANS_SERIF);
}
}
presenceFillPaint = new Paint();
presenceFillPaint.setColor(ContextCompat.getColor(context, PRESENCE_COLOR));
presenceFillPaint.setStyle(Paint.Style.FILL);
presenceFillPaint.setAntiAlias(true);
presenceFillPaint.setColor(ContextCompat.getColor(context, PRESENCE_COLOR));
presenceFillPaint.setStyle(Paint.Style.FILL);
presenceFillPaint.setAntiAlias(true);
presenceStrokePaint = new Paint();
presenceStrokePaint.setColor(ContextCompat.getColor(context, DeviceUtils.isTv(context) ? R.color.grey_900 : R.color.background));
presenceStrokePaint.setStyle(Paint.Style.STROKE);
presenceStrokePaint.setAntiAlias(true);
checkedIcon = (VectorDrawable) context.getDrawable(CHECKED_ICON);
checkedIcon.setTint(ContextCompat.getColor(context, R.color.colorPrimary));
checkedPaint = new Paint();
checkedPaint.setColor(ContextCompat.getColor(context, R.color.background));
checkedPaint.setStyle(Paint.Style.FILL_AND_STROKE);
checkedPaint.setAntiAlias(true);
clipPaint.setAntiAlias(true);
textPaint.setAntiAlias(true);
textPaint.setColor(Color.WHITE);
textPaint.setTypeface(Typeface.SANS_SERIF);
}
public AvatarDrawable(AvatarDrawable other) {
cropCircle = other.cropCircle;
minSize = other.minSize;
bitmap = other.bitmap;
color = other.color;
placeholder = other.placeholder;
avatarText = other.avatarText;
isOnline = other.isOnline;
isChecked = other.isChecked;
showPresence = other.showPresence;
presenceFillPaint = other.presenceFillPaint;
presenceStrokePaint = other.presenceStrokePaint;
checkedPaint = other.checkedPaint;
clipPaint.setAntiAlias(true);
textPaint.setAntiAlias(true);
textPaint.setColor(Color.WHITE);
textPaint.setTypeface(Typeface.SANS_SERIF);
}
@Override
public void draw(@NonNull Canvas finalCanvas) {
if (workspace == null)
return;
if (update) {
drawActual(new Canvas(workspace));
update = false;
}
if (cropCircle) {
int d = Math.min(getBounds().width(), getBounds().height());
int r = d / 2;
finalCanvas.drawCircle(getBounds().centerX(), getBounds().centerY(), r, clipPaint);
} else {
finalCanvas.drawBitmap(workspace, null, getBounds(), drawPaint);
}
if (showPresence && isOnline) {
drawPresence(finalCanvas);
}
if (isChecked) {
drawChecked(finalCanvas);
}
}
private void drawActual(@NonNull Canvas canvas) {
if (bitmap != null) {
canvas.drawBitmap(bitmap, null, backgroundBounds, drawPaint);
} else {
canvas.drawColor(color);
if (avatarText != null) {
canvas.drawText(avatarText, textStartXPoint, textStartYPoint, textPaint);
} else {
placeholder.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
placeholder.draw(canvas);
}
}
}
private void setupPresenceIndicator(Rect bounds) {
presence.radius = (int) (0.29289321881 * (double) (bounds.width()) * 0.5);
presence.cx = bounds.right - presence.radius;
presence.cy = bounds.bottom - presence.radius;
int presenceStrokeWidth = presence.radius / 3;
presenceStrokePaint.setStrokeWidth(presenceStrokeWidth);
checkedPaint.setStrokeWidth(presenceStrokeWidth);
presence.radius -= presenceStrokeWidth * 0.5;
if (checkedIcon != null)
checkedIcon.setBounds(presence.cx - presence.radius, presence.cy - presence.radius, presence.cx + presence.radius, presence.cy + presence.radius);
}
private void drawPresence(@NonNull Canvas canvas) {
canvas.drawCircle(presence.cx, presence.cy, presence.radius - 1, presenceFillPaint);
canvas.drawCircle(presence.cx, presence.cy, presence.radius, presenceStrokePaint);
}
private void drawChecked(@NonNull Canvas canvas) {
if (checkedIcon != null) {
canvas.drawCircle(presence.cx, presence.cy, presence.radius, checkedPaint);
checkedIcon.draw(canvas);
}
}
@Override
protected void onBoundsChange(Rect bounds) {
//if (showPresence)
setupPresenceIndicator(bounds);
int d = Math.min(bounds.width(), bounds.height());
if (placeholder != null) {
int cx = (bounds.width()-d)/2;
int cy = (bounds.height()-d)/2;
placeholder.setBounds(cx, cy, cx + d, cy + d);
}
if (bitmap != null) {
int iw = cropCircle ? d : bounds.width();
int ih = cropCircle ? d : bounds.height();
int a = bitmap.getWidth() * ih;
int b = bitmap.getHeight() * iw;
int w;
int h;
if (a < b) {
w = iw;
h = (iw * bitmap.getHeight())/bitmap.getWidth();
} else {
w = (ih * bitmap.getWidth())/bitmap.getHeight();
h = ih;
}
int cx = (iw - w)/2;
int cy = (ih - h)/2;
backgroundBounds.set(cx, cy, cx + w, h + cy);
} else {
setAvatarTextValues(bounds);
}
if (cropCircle) {
if (d > 0) {
workspace = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888);
clipPaint.setShader(new BitmapShader(workspace, BitmapShader.TileMode.CLAMP,
BitmapShader.TileMode.CLAMP));
} else {
clipPaint.setShader(null);
if (workspace != null) {
workspace.recycle();
workspace = null;
}
}
} else {
workspace = Bitmap.createBitmap(bounds.width(), bounds.height(),
Bitmap.Config.ARGB_8888);
}
update = true;
}
@Override
public void setAlpha(int alpha) {
if (placeholder != null) {
placeholder.setAlpha(alpha);
} else {
textPaint.setAlpha(alpha);
}
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
if (placeholder != null) {
placeholder.setColorFilter(colorFilter);
} else {
textPaint.setColorFilter(colorFilter);
}
}
@Override
public int getMinimumWidth() {
return minSize;
}
@Override
public int getMinimumHeight() {
return minSize;
}
public void setInSize(int s) {
inSize = s;
}
@Override
public int getIntrinsicWidth() {
return inSize;
}
@Override
public int getIntrinsicHeight() {
return inSize;
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
private void setAvatarTextValues(Rect bounds) {
if (avatarText != null) {
textPaint.setTextSize(bounds.height() * DEFAULT_TEXT_SIZE_PERCENTAGE);
float stringWidth = textPaint.measureText(avatarText);
textStartXPoint = (bounds.width() / 2f) - (stringWidth / 2f);
textStartYPoint = (bounds.height() / 2f) - ((textPaint.ascent() + textPaint.descent()) / 2f);
}
}
private String convertNameToAvatarText(String name) {
if (TextUtils.isEmpty(name)) {
return null;
} else {
return new String(Character.toChars(name.codePointAt(0))).toUpperCase();
}
}
private static int getAvatarColor(String id) {
if (id == null) {
return R.color.grey_500;
}
String md5 = HashUtils.md5(id);
if (md5 == null) {
return R.color.grey_500;
}
int colorIndex = Integer.parseInt(md5.charAt(0) + "", 16);
return contactColors[colorIndex % contactColors.length];
}
}