diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutRankingUpdateJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutRankingUpdateJob.kt new file mode 100644 index 0000000000..52b10942f7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutRankingUpdateJob.kt @@ -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 { + 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) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java index d140b18235..a8b0ebb8d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 035e1d2cd9..9b20aa3746 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -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()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java index c20ca32d34..42e8752a40 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java @@ -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 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. *

- * 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 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 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); + } + } + } }