Implement ShortcutInfo for API 30.

This commit is contained in:
Alex Hart
2020-11-17 09:58:28 -04:00
parent 75062ada8a
commit 930370783e
18 changed files with 464 additions and 50 deletions

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
@@ -10,6 +11,7 @@ import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.IconCompat;
@@ -22,8 +24,11 @@ import com.bumptech.glide.request.transition.Transition;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -33,6 +38,8 @@ import java.util.concurrent.ExecutionException;
public final class AvatarUtil {
private static final String TAG = Log.tag(AvatarUtil.class);
private AvatarUtil() {
}
@@ -93,6 +100,16 @@ public final class AvatarUtil {
}
}
@RequiresApi(ConversationUtil.CONVERSATION_SUPPORT_VERSION)
@WorkerThread
public static Icon getIconForShortcut(@NonNull Context context, @NonNull Recipient recipient) {
try {
return Icon.createWithAdaptiveBitmap(getShortcutInfoBitmap(context, recipient));
} catch (ExecutionException | InterruptedException e) {
return Icon.createWithAdaptiveBitmap(getFallbackForShortcut(context, recipient));
}
}
@WorkerThread
public static Bitmap getBitmapForNotification(@NonNull Context context, @NonNull Recipient recipient) {
try {
@@ -102,13 +119,8 @@ public final class AvatarUtil {
}
}
public static GlideRequest<Drawable> getSelfAvatarOrFallbackIcon(@NonNull Context context, @DrawableRes int fallbackIcon) {
return GlideApp.with(context)
.asDrawable()
.load(new ProfileContactPhoto(Recipient.self(), Recipient.self().getProfileAvatar()))
.error(fallbackIcon)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL);
private static @NonNull Bitmap getShortcutInfoBitmap(@NonNull Context context, @NonNull Recipient recipient) throws ExecutionException, InterruptedException {
return DrawableUtil.wrapBitmapForShortcutInfo(request(GlideApp.with(context).asBitmap(), context, recipient, false).circleCrop().submit().get());
}
private static <T> GlideRequest<T> requestCircle(@NonNull GlideRequest<T> glideRequest, @NonNull Context context, @NonNull Recipient recipient) {
@@ -120,11 +132,40 @@ public final class AvatarUtil {
}
private static <T> GlideRequest<T> request(@NonNull GlideRequest<T> glideRequest, @NonNull Context context, @NonNull Recipient recipient) {
return glideRequest.load(new ProfileContactPhoto(recipient, recipient.getProfileAvatar()))
return request(glideRequest, context, recipient, true);
}
private static <T> GlideRequest<T> request(@NonNull GlideRequest<T> glideRequest, @NonNull Context context, @NonNull Recipient recipient, boolean loadSelf) {
final ContactPhoto photo;
if (Recipient.self().equals(recipient) && loadSelf) {
photo = new ProfileContactPhoto(recipient, recipient.getProfileAvatar());
} else {
photo = recipient.getContactPhoto();
}
return glideRequest.load(photo)
.error(getFallback(context, recipient))
.diskCacheStrategy(DiskCacheStrategy.ALL);
}
private static @NonNull Bitmap getFallbackForShortcut(@NonNull Context context, @NonNull Recipient recipient) {
@DrawableRes final int photoSource;
if (recipient.isSelf()) {
photoSource = R.drawable.ic_note_80;
} else if (recipient.isGroup()) {
photoSource = R.drawable.ic_group_80;
} else {
photoSource = R.drawable.ic_profile_80;
}
Bitmap toWrap = DrawableUtil.toBitmap(new FallbackPhoto80dp(photoSource, recipient.getColor()).asDrawable(context, -1), ViewUtil.dpToPx(80), ViewUtil.dpToPx(80));
Bitmap wrapped = DrawableUtil.wrapBitmapForShortcutInfo(toWrap);
toWrap.recycle();
return wrapped;
}
private static Drawable getFallback(@NonNull Context context, @NonNull Recipient recipient) {
String name = Optional.fromNullable(recipient.getDisplayName(context)).or("");
MaterialColor fallbackColor = recipient.getColor();

View File

@@ -0,0 +1,239 @@
package org.thoughtcrime.securesms.util;
import android.app.Person;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* ConversationUtil encapsulates support for Android 11+'s new Conversations system
*/
public final class ConversationUtil {
public static final int CONVERSATION_SUPPORT_VERSION = 30;
private ConversationUtil() {}
/**
* Pushes a new dynamic shortcut for the given recipient and updates the ranks of all current
* shortcuts.
*/
public static void pushShortcutForRecipient(@NonNull Context context, @NonNull Recipient recipient) {
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
SignalExecutors.BOUNDED.execute(() -> {
pushShortcutAndUpdateRanks(context, recipient);
});
}
}
/**
* Synchronously pushes a new dynamic shortcut for the given recipient if one does not already exist.
*
* If added, this recipient is given a high ranking with the intention of not appearing immediately in results.
*/
@WorkerThread
public static void pushShortcutForRecipientIfNeededSync(@NonNull Context context, @NonNull Recipient recipient) {
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
String shortcutId = getShortcutId(recipient);
List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
boolean hasPushedRecipientShortcut = Stream.of(shortcuts)
.filter(info -> Objects.equals(shortcutId, info.getId()))
.findFirst()
.isPresent();
if (!hasPushedRecipientShortcut) {
pushShortcutForRecipientInternal(context, recipient, shortcuts.size());
}
}
}
/**
* Clears all currently set dynamic shortcuts
*/
public static void clearAllShortcuts(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
List<ShortcutInfo> shortcutInfos = shortcutManager.getDynamicShortcuts();
shortcutManager.removeLongLivedShortcuts(Stream.of(shortcutInfos).map(ShortcutInfo::getId).toList());
}
}
/**
* Clears the shortcuts tied to a given thread.
*/
public static void clearShortcuts(@NonNull Context context, @NonNull Set<Long> threadIds) {
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
SignalExecutors.BOUNDED.execute(() -> {
List<RecipientId> recipientIds = DatabaseFactory.getThreadDatabase(context).getRecipientIdsForThreadIds(threadIds);
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
shortcutManager.removeLongLivedShortcuts(Stream.of(recipientIds).map(ConversationUtil::getShortcutId).toList());
});
}
}
/**
* Returns an ID that is unique between all recipients.
*
* @param recipientId The recipient ID to get a shortcut ID for
*
* @return A unique identifier that is stable for a given recipient id
*/
public static @NonNull String getShortcutId(@NonNull RecipientId recipientId) {
return recipientId.serialize();
}
/**
* Returns an ID that is unique between all recipients.
*
* @param recipient The recipient to get a shortcut for.
*
* @return A unique identifier that is stable for a given recipient id
*/
public static @NonNull String getShortcutId(@NonNull Recipient recipient) {
return getShortcutId(recipient.getId());
}
/**
* Updates the rank of each existing shortcut by 1 and then publishes a new shortcut of rank 0
* for the given recipient.
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static void pushShortcutAndUpdateRanks(@NonNull Context context, @NonNull Recipient recipient) {
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
List<ShortcutInfo> currentShortcuts = shortcutManager.getDynamicShortcuts();
if (Util.isEmpty(currentShortcuts)) {
for (ShortcutInfo shortcutInfo : currentShortcuts) {
RecipientId recipientId = RecipientId.from(shortcutInfo.getId());
Recipient resolved = Recipient.resolved(recipientId);
ShortcutInfo updated = buildShortcutInfo(context, resolved, shortcutInfo.getRank() + 1);
shortcutManager.pushDynamicShortcut(updated);
}
}
pushShortcutForRecipientInternal(context, recipient, 0);
}
/**
* Pushes a dynamic shortcut for a given recipient to the shortcut manager
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static void pushShortcutForRecipientInternal(@NonNull Context context, @NonNull Recipient recipient, int rank) {
ShortcutInfo shortcutInfo = buildShortcutInfo(context, recipient, rank);
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
shortcutManager.pushDynamicShortcut(shortcutInfo);
}
/**
* Builds the shortcut info object for a given Recipient.
*
* @param context The Context under which we are operating
* @param recipient The Recipient to generate a ShortcutInfo for
* @param rank The rank that should be assigned to this recipient
* @return The new ShortcutInfo
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static @NonNull ShortcutInfo buildShortcutInfo(@NonNull Context context,
@NonNull Recipient recipient,
int rank)
{
Recipient resolved = recipient.resolve();
Person[] persons = buildPersons(context, resolved);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(resolved);
String shortName = resolved.isSelf() ? context.getString(R.string.note_to_self) : resolved.getShortDisplayName(context);
String longName = resolved.isSelf() ? context.getString(R.string.note_to_self) : resolved.getDisplayName(context);
return new ShortcutInfo.Builder(context, getShortcutId(resolved))
.setLongLived(true)
.setIntent(ConversationActivity.buildIntent(context, resolved.getId(), threadId))
.setShortLabel(shortName)
.setLongLabel(longName)
.setIcon(AvatarUtil.getIconForShortcut(context, resolved))
.setPersons(persons)
.setCategories(Collections.singleton(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION))
.setActivity(new ComponentName(context, "org.thoughtcrime.securesms.RoutingActivity"))
.setRank(rank)
.build();
}
/**
* @return an array of Person objects correlating to members of a conversation (other than self)
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static @NonNull Person[] buildPersons(@NonNull Context context, @NonNull Recipient recipient) {
if (recipient.isGroup()) {
return buildPersonsForGroup(context, recipient.getGroupId().get());
} else {
return new Person[]{buildPerson(context, recipient)};
}
}
/**
* @return an array of Person objects correlating to members of a group (other than self)
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static @NonNull Person[] buildPersonsForGroup(@NonNull Context context, @NonNull GroupId groupId) {
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
return Stream.of(members).map(member -> buildPerson(context, member.resolve())).toArray(Person[]::new);
}
/**
* @return A Person object representing the given Recipient
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static @NonNull Person buildPerson(@NonNull Context context, @NonNull Recipient recipient) {
return new Person.Builder()
.setKey(getShortcutId(recipient.getId()))
.setName(recipient.getDisplayName(context))
.setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null)
.build();
}
/**
* @return A Compat Library Person object representing the given Recipient
*/
@WorkerThread
public static @NonNull androidx.core.app.Person buildPersonCompat(@NonNull Context context, @NonNull Recipient recipient) {
return new androidx.core.app.Person.Builder()
.setKey(getShortcutId(recipient.getId()))
.setName(recipient.getDisplayName(context))
.setIcon(AvatarUtil.getIconForNotification(context, recipient))
.setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null)
.build();
}
}

View File

@@ -6,10 +6,15 @@ import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.camera.core.impl.SingleImageProxyBundle;
import androidx.core.graphics.drawable.DrawableCompat;
public final class DrawableUtil {
private static final int SHORTCUT_INFO_BITMAP_SIZE = ViewUtil.dpToPx(108);
private static final int SHORTCUT_INFO_WRAPPED_SIZE = ViewUtil.dpToPx(72);
private static final int SHORTCUT_INFO_PADDING = (SHORTCUT_INFO_BITMAP_SIZE - SHORTCUT_INFO_WRAPPED_SIZE) / 2;
private DrawableUtil() {}
public static @NonNull Bitmap toBitmap(@NonNull Drawable drawable, int width, int height) {
@@ -22,6 +27,16 @@ public final class DrawableUtil {
return bitmap;
}
public static @NonNull Bitmap wrapBitmapForShortcutInfo(@NonNull Bitmap toWrap) {
Bitmap bitmap = Bitmap.createBitmap(SHORTCUT_INFO_BITMAP_SIZE, SHORTCUT_INFO_BITMAP_SIZE, Bitmap.Config.ARGB_8888);
Bitmap scaled = Bitmap.createScaledBitmap(toWrap, SHORTCUT_INFO_WRAPPED_SIZE, SHORTCUT_INFO_WRAPPED_SIZE, true);
Canvas canvas = new Canvas(bitmap);
canvas.drawBitmap(scaled, SHORTCUT_INFO_PADDING, SHORTCUT_INFO_PADDING, null);
return bitmap;
}
/**
* Returns a new {@link Drawable} that safely wraps and tints the provided drawable.
*/

View File

@@ -7,6 +7,7 @@ import android.app.NotificationManager;
import android.app.job.JobScheduler;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.pm.ShortcutManager;
import android.hardware.SensorManager;
import android.hardware.display.DisplayManager;
import android.location.LocationManager;
@@ -31,6 +32,11 @@ public class ServiceUtil {
return (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
}
@RequiresApi(25)
public static @Nullable ShortcutManager getShortcutManager(@NonNull Context context) {
return ContextCompat.getSystemService(context, ShortcutManager.class);
}
public static WindowManager getWindowManager(Context context) {
return (WindowManager) context.getSystemService(Activity.WINDOW_SERVICE);
}