Add Android 14 improvements for dynamic shortcuts.

Closes #12923
Co-authored-by: Yuichi Araki <yaraki@google.com>
This commit is contained in:
Greyson Parrelli
2023-05-01 12:34:39 -04:00
committed by Alex Hart
parent 35e96fecdb
commit f081591354
4 changed files with 173 additions and 26 deletions

View File

@@ -0,0 +1,101 @@
package org.thoughtcrime.securesms.jobs
import android.os.Build
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JsonJobData
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.transport.RetryLaterException
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.ConversationUtil.Direction
import org.thoughtcrime.securesms.util.TextSecurePreferences
import kotlin.time.Duration.Companion.seconds
/**
* Updates the ranking of a shortcut by providing hints for when we send/receive messages to different recipients.
*/
class ConversationShortcutRankingUpdateJob private constructor(
parameters: Parameters,
private val recipient: Recipient,
private val direction: Direction
) : BaseJob(parameters) {
companion object {
private val TAG = Log.tag(ConversationShortcutRankingUpdateJob::class.java)
const val KEY = "ConversationShortcutRankingUpdateJob"
private const val KEY_RECIPIENT = "recipient"
private const val KEY_REPORTED_SIGNAL = "reported_signal"
@JvmStatic
fun enqueueForOutgoingIfNecessary(recipient: Recipient) {
if (Build.VERSION.SDK_INT >= 34) {
ApplicationDependencies.getJobManager().add(ConversationShortcutRankingUpdateJob(recipient, Direction.OUTGOING))
}
}
@JvmStatic
fun enqueueForIncomingIfNecessary(recipient: Recipient) {
if (Build.VERSION.SDK_INT >= 34) {
ApplicationDependencies.getJobManager().add(ConversationShortcutRankingUpdateJob(recipient, Direction.INCOMING))
}
}
}
private constructor(recipient: Recipient, direction: Direction) : this(
Parameters.Builder()
.setQueue("ConversationShortcutRankingUpdateJob::${recipient.id.serialize()}")
.setMaxInstancesForQueue(1)
.setMaxAttempts(3)
.build(),
recipient,
direction
)
override fun serialize(): ByteArray? {
return JsonJobData.Builder()
.putString(KEY_RECIPIENT, recipient.id.serialize())
.putInt(KEY_REPORTED_SIGNAL, direction.serialize())
.serialize()
}
override fun getFactoryKey() = KEY
override fun onRun() {
if (TextSecurePreferences.isScreenLockEnabled(context)) {
Log.i(TAG, "Screen lock enabled. Clearing shortcuts.")
ConversationUtil.clearAllShortcuts(context)
return
}
val success: Boolean = ConversationUtil.pushShortcutForRecipientSync(context, recipient, direction)
if (!success) {
Log.w(TAG, "Failed to update shortcut for ${recipient.id}. Possibly retrying.")
throw RetryLaterException()
}
}
override fun onShouldRetry(e: Exception): Boolean {
return e is RetryLaterException
}
override fun getNextRunAttemptBackoff(pastAttemptCount: Int, exception: Exception): Long {
return 30.seconds.inWholeMilliseconds
}
override fun onFailure() = Unit
class Factory : Job.Factory<ConversationShortcutRankingUpdateJob?> {
override fun create(parameters: Parameters, serializedData: ByteArray?): ConversationShortcutRankingUpdateJob {
val data = JsonJobData.deserialize(serializedData)
val recipient: Recipient = Recipient.resolved(RecipientId.from(data.getString(KEY_RECIPIENT)))
val direction: Direction = Direction.deserialize(data.getInt(KEY_REPORTED_SIGNAL))
return ConversationShortcutRankingUpdateJob(parameters, recipient, direction)
}
}
}

View File

@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalServiceMessageSender.IndividualSendEvents;
@@ -195,6 +196,8 @@ public class IndividualSendJob extends PushSendJob {
SignalDatabase.attachments().deleteAttachmentFilesForViewOnceMessage(messageId);
}
ConversationShortcutRankingUpdateJob.enqueueForOutgoingIfNecessary(recipient);
log(TAG, String.valueOf(message.getSentTimeMillis()), "Sent message: " + messageId);
} catch (InsecureFallbackApprovalException ifae) {

View File

@@ -99,6 +99,7 @@ public final class JobManagerFactories {
put(CheckServiceReachabilityJob.KEY, new CheckServiceReachabilityJob.Factory());
put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory());
put(ClearFallbackKbsEnclaveJob.KEY, new ClearFallbackKbsEnclaveJob.Factory());
put(ConversationShortcutRankingUpdateJob.KEY, new ConversationShortcutRankingUpdateJob.Factory());
put(ConversationShortcutUpdateJob.KEY, new ConversationShortcutUpdateJob.Factory());
put(CreateReleaseChannelJob.KEY, new CreateReleaseChannelJob.Factory());
put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory());

View File

@@ -12,6 +12,7 @@ import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import com.annimon.stream.Stream;
import com.google.common.collect.Sets;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
@@ -29,7 +30,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -37,12 +37,20 @@ import java.util.stream.Collectors;
*/
public final class ConversationUtil {
public static final int CONVERSATION_SUPPORT_VERSION = 30;
private static final String TAG = Log.tag(ConversationUtil.class);
public static final int CONVERSATION_SUPPORT_VERSION = 30;
private static final String CATEGORY_SHARE_TARGET = "org.thoughtcrime.securesms.sharing.CATEGORY_SHARE_TARGET";
private static final String CAPABILITY_SEND_MESSAGE = "actions.intent.SEND_MESSAGE";
private static final String CAPABILITY_RECEIVE_MESSAGE = "actions.intent.RECEIVE_MESSAGE";
private static final String PARAMETER_RECIPIENT_TYPE = "message.recipient.@type";
private static final String PARAMETER_SENDER_TYPE = "message.sender.@type";
private static final List<String> PARAMETERS_AUDIENCE = Collections.singletonList("Audience");
private ConversationUtil() {}
@@ -64,23 +72,16 @@ public final class ConversationUtil {
}
/**
* Synchronously pushes a new dynamic shortcut for the given recipient if one does not already exist.
* Synchronously pushes a dynamic shortcut for the given recipient.
* <p>
* If added, this recipient is given a high ranking with the intention of not appearing immediately in results.
* The recipient is given a high ranking with the intention of not appearing immediately in results.
*
* @return True if it succeeded, or false if it was rate-limited.
*/
@WorkerThread
public static void pushShortcutForRecipientIfNeededSync(@NonNull Context context, @NonNull Recipient recipient) {
String shortcutId = getShortcutId(recipient);
public static boolean pushShortcutForRecipientSync(@NonNull Context context, @NonNull Recipient recipient, @NonNull Direction direction ) {
List<ShortcutInfoCompat> shortcuts = ShortcutManagerCompat.getDynamicShortcuts(context);
boolean hasPushedRecipientShortcut = Stream.of(shortcuts)
.filter(info -> Objects.equals(shortcutId, info.getId()))
.findFirst()
.isPresent();
if (!hasPushedRecipientShortcut) {
pushShortcutForRecipientInternal(context, recipient, shortcuts.size());
}
return pushShortcutForRecipientInternal(context, recipient, shortcuts.size(), direction);
}
/**
@@ -173,7 +174,7 @@ public final class ConversationUtil {
List<ShortcutInfoCompat> shortcuts = new ArrayList<>(rankedRecipients.size());
for (int i = 0; i < rankedRecipients.size(); i++) {
ShortcutInfoCompat info = buildShortcutInfo(context, rankedRecipients.get(i), i);
ShortcutInfoCompat info = buildShortcutInfo(context, rankedRecipients.get(i), i, Direction.NONE);
shortcuts.add(info);
}
@@ -182,12 +183,14 @@ public final class ConversationUtil {
/**
* Pushes a dynamic shortcut for a given recipient to the shortcut manager
*
* @return True if it succeeded, or false if it was rate-limited.
*/
@WorkerThread
private static void pushShortcutForRecipientInternal(@NonNull Context context, @NonNull Recipient recipient, int rank) {
ShortcutInfoCompat shortcutInfo = buildShortcutInfo(context, recipient, rank);
private static boolean pushShortcutForRecipientInternal(@NonNull Context context, @NonNull Recipient recipient, int rank, @NonNull Direction direction) {
ShortcutInfoCompat shortcutInfo = buildShortcutInfo(context, recipient, rank, direction);
ShortcutManagerCompat.pushDynamicShortcut(context, shortcutInfo);
return ShortcutManagerCompat.pushDynamicShortcut(context, shortcutInfo);
}
/**
@@ -201,7 +204,8 @@ public final class ConversationUtil {
@WorkerThread
private static @NonNull ShortcutInfoCompat buildShortcutInfo(@NonNull Context context,
@NonNull Recipient recipient,
int rank)
int rank,
@NonNull Direction direction)
{
Recipient resolved = recipient.resolve();
Person[] persons = buildPersons(context, resolved);
@@ -209,19 +213,34 @@ public final class ConversationUtil {
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);
String shortcutId = getShortcutId(resolved);
return new ShortcutInfoCompat.Builder(context, shortcutId)
ShortcutInfoCompat.Builder builder = new ShortcutInfoCompat.Builder(context, shortcutId)
.setLongLived(true)
.setIntent(ConversationIntents.createBuilder(context, resolved.getId(), threadId != null ? threadId : -1).build())
.setShortLabel(shortName)
.setLongLabel(longName)
.setIcon(AvatarUtil.getIconCompatForShortcut(context, resolved))
.setPersons(persons)
.setCategories(Collections.singleton(CATEGORY_SHARE_TARGET))
.setCategories(Sets.newHashSet(CATEGORY_SHARE_TARGET))
.setActivity(new ComponentName(context, "org.thoughtcrime.securesms.RoutingActivity"))
.setRank(rank)
.setLocusId(new LocusIdCompat(shortcutId))
.build();
.setLocusId(new LocusIdCompat(shortcutId));
if (direction == Direction.OUTGOING) {
if (recipient.isGroup()) {
builder.addCapabilityBinding(CAPABILITY_SEND_MESSAGE, PARAMETER_RECIPIENT_TYPE, PARAMETERS_AUDIENCE);
} else {
builder.addCapabilityBinding(CAPABILITY_SEND_MESSAGE);
}
} else if (direction == Direction.INCOMING) {
if (recipient.isGroup()) {
builder.addCapabilityBinding(CAPABILITY_RECEIVE_MESSAGE, PARAMETER_SENDER_TYPE, PARAMETERS_AUDIENCE);
} else {
builder.addCapabilityBinding(CAPABILITY_RECEIVE_MESSAGE);
}
}
return builder.build();
}
/**
@@ -269,4 +288,27 @@ public final class ConversationUtil {
.setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null)
.build();
}
public enum Direction {
NONE(0), INCOMING(1), OUTGOING(2);
private final int value;
Direction(int value) {
this.value = value;
}
public int serialize() {
return value;
}
public static Direction deserialize(int value) {
switch (value) {
case 0: return NONE;
case 1: return INCOMING;
case 2: return OUTGOING;
default: throw new IllegalArgumentException("Unrecognized value: " + value);
}
}
}
}