Add avatar picker and defaults.

This commit is contained in:
Alex Hart
2021-07-20 13:08:52 -03:00
committed by Greyson Parrelli
parent 0093e1d3eb
commit ed23c3fe7c
133 changed files with 4935 additions and 859 deletions

View File

@@ -154,6 +154,6 @@ public final class AvatarUtil {
private static Drawable getFallback(@NonNull Context context, @NonNull Recipient recipient) {
String name = Optional.fromNullable(recipient.getDisplayName(context)).or("");
return new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40).asDrawable(context, recipient.getAvatarColor().colorInt());
return new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40).asDrawable(context, recipient.getAvatarColor());
}
}

View File

@@ -13,7 +13,6 @@ import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
@@ -23,9 +22,8 @@ import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.libsignal.util.ByteUtil;
@@ -186,8 +184,8 @@ public final class ConversationShortcutPhoto implements Key {
photoSource = R.drawable.ic_profile_80;
}
FallbackContactPhoto photo = recipient.isSelf() || recipient.isGroup() ? new FallbackPhoto80dp(photoSource, recipient.getAvatarColor().colorInt())
: new ShortcutGeneratedContactPhoto(recipient.getDisplayName(context), photoSource, ViewUtil.dpToPx(80), ViewUtil.dpToPx(28), recipient.getAvatarColor().colorInt());
FallbackContactPhoto photo = recipient.isSelf() || recipient.isGroup() ? new FallbackPhoto80dp(photoSource, recipient.getAvatarColor())
: new ShortcutGeneratedContactPhoto(recipient.getDisplayName(context), photoSource, ViewUtil.dpToPx(80), recipient.getAvatarColor());
Bitmap toWrap = DrawableUtil.toBitmap(photo.asCallCard(context), ViewUtil.dpToPx(80), ViewUtil.dpToPx(80));
Bitmap wrapped = DrawableUtil.wrapBitmapForShortcutInfo(toWrap);
@@ -199,20 +197,20 @@ public final class ConversationShortcutPhoto implements Key {
private static final class ShortcutGeneratedContactPhoto extends GeneratedContactPhoto {
private final int color;
private final AvatarColor color;
public ShortcutGeneratedContactPhoto(@NonNull String name, int fallbackResId, int targetSize, int fontSize, int color) {
super(name, fallbackResId, targetSize, fontSize);
public ShortcutGeneratedContactPhoto(@NonNull String name, int fallbackResId, int targetSize, @NonNull AvatarColor color) {
super(name, fallbackResId, targetSize);
this.color = color;
}
@Override
protected Drawable newFallbackDrawable(@NonNull Context context, int color, boolean inverted) {
return new FallbackPhoto80dp(getFallbackResId(), color).asDrawable(context, -1);
protected Drawable newFallbackDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) {
return new FallbackPhoto80dp(getFallbackResId(), color).asDrawable(context, AvatarColor.UNKNOWN);
}
@Override public Drawable asCallCard(Context context) {
@Override public Drawable asCallCard(@NonNull Context context) {
return new FallbackPhoto80dp(getFallbackResId(), color).asCallCard(context);
}
}

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
@@ -33,16 +34,16 @@ import kotlin.jvm.functions.Function1;
* override compiler typing recommendations when binding and diffing.
* <p></p>
* General pattern for implementation:
* <ol>
* <li>Create {@link MappingModel}s for the items in the list. These encapsulate data massaging methods for views to use and the diff logic.</li>
* <li>Create {@link MappingViewHolder}s for each item type in the list and their corresponding {@link Factory}.</li>
* <li>Create an instance or subclass of {@link MappingAdapter} and register the mapping of model type to view holder factory for that model type.</li>
* </ol>
* Event listeners, click or otherwise, are handled at the view holder level and should be passed into the appropriate view holder factories. This
* pattern mimics how we pass data into view models via factories.
* <p></p>
* NOTE: There can only be on factory registered per model type. Registering two for the same type will result in the last one being used. However, the
* same factory can be registered multiple times for multiple model types (if the model type class hierarchy supports it).
* <ol>
* <li>Create {@link MappingModel}s for the items in the list. These encapsulate data massaging methods for views to use and the diff logic.</li>
* <li>Create {@link MappingViewHolder}s for each item type in the list and their corresponding {@link Factory}.</li>
* <li>Create an instance or subclass of {@link MappingAdapter} and register the mapping of model type to view holder factory for that model type.</li>
* </ol>
* Event listeners, click or otherwise, are handled at the view holder level and should be passed into the appropriate view holder factories. This
* pattern mimics how we pass data into view models via factories.
* <p></p>
* NOTE: There can only be on factory registered per model type. Registering two for the same type will result in the last one being used. However, the
* same factory can be registered multiple times for multiple model types (if the model type class hierarchy supports it).
*/
public class MappingAdapter extends ListAdapter<MappingModel<?>, MappingViewHolder<?>> {
@@ -102,6 +103,12 @@ public class MappingAdapter extends ListAdapter<MappingModel<?>, MappingViewHold
return Objects.requireNonNull(factories.get(viewType)).createViewHolder(parent);
}
@Override
public void onBindViewHolder(@NonNull MappingViewHolder<?> holder, int position, @NonNull List<Object> payloads) {
holder.setPayload(payloads);
onBindViewHolder(holder, position);
}
@Override
public void onBindViewHolder(@NonNull MappingViewHolder holder, int position) {
//noinspection unchecked
@@ -142,6 +149,16 @@ public class MappingAdapter extends ListAdapter<MappingModel<?>, MappingViewHold
}
return false;
}
@Override
public @Nullable Object getChangePayload(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) {
if (oldItem.getClass() == newItem.getClass()) {
//noinspection unchecked
return oldItem.getChangePayload(newItem);
}
return null;
}
}
public interface Factory<T extends MappingModel<T>> {

View File

@@ -1,8 +1,13 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public interface MappingModel<T> {
boolean areItemsTheSame(@NonNull T newItem);
boolean areContentsTheSame(@NonNull T newItem);
default @Nullable Object getChangePayload(@NonNull T newItem) {
return null;
}
}

View File

@@ -7,13 +7,18 @@ import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import java.util.LinkedList;
import java.util.List;
public abstract class MappingViewHolder<Model extends MappingModel<Model>> extends LifecycleViewHolder implements LifecycleOwner {
protected final Context context;
protected final Context context;
protected final List<Object> payload;
public MappingViewHolder(@NonNull View itemView) {
super(itemView);
context = itemView.getContext();
payload = new LinkedList<>();
}
public <T extends View> T findViewById(@IdRes int id) {
@@ -26,6 +31,11 @@ public abstract class MappingViewHolder<Model extends MappingModel<Model>> exten
public abstract void bind(@NonNull Model model);
public void setPayload(@NonNull List<Object> payload) {
this.payload.clear();
this.payload.addAll(payload);
}
public static final class SimpleViewHolder<Model extends MappingModel<Model>> extends MappingViewHolder<Model> {
public SimpleViewHolder(@NonNull View itemView) {
super(itemView);

View File

@@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.util
import android.text.TextUtils
import java.util.regex.Pattern
object NameUtil {
private val PATTERN = Pattern.compile("[^\\p{L}\\p{Nd}\\p{S}]+")
/**
* Returns an abbreviation of the input, up to two characters long.
*/
@JvmStatic
fun getAbbreviation(name: String): String? {
val parts = name.split(" ").toTypedArray()
val builder = StringBuilder()
var count = 0
var i = 0
while (i < parts.size && count < 2) {
val cleaned = PATTERN.matcher(parts[i]).replaceFirst("")
if (!TextUtils.isEmpty(cleaned)) {
builder.appendCodePoint(cleaned.codePointAt(0))
count++
}
i++
}
return if (builder.isEmpty()) {
null
} else {
builder.toString()
}
}
}

View File

@@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.util.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.signal.core.util.StreamUtil;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* Manages the storage of custom files.
*/
public final class FileStorage {
/**
* Saves the provided input stream as a new file.
*/
@WorkerThread
public static @NonNull String save(@NonNull Context context,
@NonNull InputStream inputStream,
@NonNull String directoryName,
@NonNull String fileNameBase,
@NonNull String extension
) throws IOException
{
File directory = context.getDir(directoryName, Context.MODE_PRIVATE);
File file = File.createTempFile(fileNameBase, "." + extension, directory);
StreamUtil.copy(inputStream, getOutputStream(context, file));
return file.getName();
}
@WorkerThread
public static @NonNull InputStream read(@NonNull Context context,
@NonNull String directoryName,
@NonNull String filename) throws IOException
{
File directory = context.getDir(directoryName, Context.MODE_PRIVATE);
File file = new File(directory, filename);
return getInputStream(context, file);
}
@WorkerThread
public static @NonNull List<String> getAll(@NonNull Context context,
@NonNull String directoryName,
@NonNull String fileNameBase)
{
return getAllFiles(context, directoryName, fileNameBase).stream()
.map(File::getName)
.collect(Collectors.toList());
}
@WorkerThread
public static @NonNull List<File> getAllFiles(@NonNull Context context,
@NonNull String directoryName,
@NonNull String fileNameBase)
{
File directory = context.getDir(directoryName, Context.MODE_PRIVATE);
File[] allFiles = directory.listFiles(pathname -> pathname.getName().contains(fileNameBase));
if (allFiles != null) {
return Arrays.asList(allFiles);
} else {
return Collections.emptyList();
}
}
private static @NonNull OutputStream getOutputStream(@NonNull Context context, File outputFile) throws IOException {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second;
}
private static @NonNull InputStream getInputStream(@NonNull Context context, File inputFile) throws IOException {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
return ModernDecryptingPartInputStream.createFor(attachmentSecret, inputFile, 0);
}
}