diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 3f844207e2..48f333e377 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -533,7 +533,7 @@ public class ConversationActivity extends PassphraseRequiredActivity } setVisibleThread(threadId); - ConversationUtil.pushShortcutForRecipient(getApplicationContext(), recipientSnapshot); + ConversationUtil.refreshRecipientShortcuts(); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutUpdateJob.java new file mode 100644 index 0000000000..583c32edfb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutUpdateJob.java @@ -0,0 +1,87 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.database.model.ThreadRecord; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +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 java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * On some devices, interacting with the ShortcutManager can take a very long time (several seconds). + * So, we interact with it in a job instead, and keep it in one queue so it can't starve the other + * job runners. + */ +public class ConversationShortcutUpdateJob extends BaseJob { + + public static final String KEY = "ConversationShortcutUpdateJob"; + + public ConversationShortcutUpdateJob() { + this(new Parameters.Builder() + .setQueue("ConversationShortcutUpdateJob") + .setLifespan(TimeUnit.MINUTES.toMillis(15)) + .setMaxInstances(1) + .build()); + } + + private ConversationShortcutUpdateJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public @NonNull Data serialize() { + return Data.EMPTY; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + @RequiresApi(ConversationUtil.CONVERSATION_SUPPORT_VERSION) + protected void onRun() throws Exception { + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); + int maxShortcuts = ConversationUtil.getMaxShortcuts(context); + List ranked = new ArrayList<>(maxShortcuts); + + try (ThreadDatabase.Reader reader = threadDatabase.readerFor(threadDatabase.getRecentConversationList(maxShortcuts, false, false))) { + ThreadRecord record; + while ((record = reader.getNext()) != null) { + ranked.add(record.getRecipient().resolve()); + } + } + + boolean success = ConversationUtil.setActiveShortcuts(context, ranked); + + if (!success) { + throw new RetryLaterException(); + } + } + + @Override + protected boolean onShouldRetry(@NonNull Exception e) { + return e instanceof RetryLaterException; + } + + @Override + public void onFailure() { + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull ConversationShortcutUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new ConversationShortcutUpdateJob(parameters); + } + } +} 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 a5212312ad..d87275a4fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -66,6 +66,7 @@ public final class JobManagerFactories { put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory()); put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory()); put(ClearFallbackKbsEnclaveJob.KEY, new ClearFallbackKbsEnclaveJob.Factory()); + put(ConversationShortcutUpdateJob.KEY, new ConversationShortcutUpdateJob.Factory()); put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory()); put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory()); put(FcmRefreshJob.KEY, new FcmRefreshJob.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 7815676a59..1707478b98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java @@ -1,15 +1,11 @@ package org.thoughtcrime.securesms.util; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; 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 android.service.notification.StatusBarNotification; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; @@ -18,19 +14,19 @@ import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.conversation.ConversationActivity; import org.thoughtcrime.securesms.conversation.ConversationIntents; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.jobs.ConversationShortcutUpdateJob; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.thoughtcrime.securesms.notifications.NotificationIds; -import org.thoughtcrime.securesms.notifications.SingleRecipientNotificationBuilder; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -43,6 +39,8 @@ public final class ConversationUtil { public static final int CONVERSATION_SUPPORT_VERSION = 30; + private static final String TAG = Log.tag(ConversationUtil.class); + private ConversationUtil() {} @@ -57,14 +55,11 @@ public final class ConversationUtil { } /** - * Pushes a new dynamic shortcut for the given recipient and updates the ranks of all current - * shortcuts. + * Enqueues a job to update the list of shortcuts. */ - public static void pushShortcutForRecipient(@NonNull Context context, @NonNull Recipient recipient) { + public static void refreshRecipientShortcuts() { if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) { - SignalExecutors.BOUNDED.execute(() -> { - pushShortcutAndUpdateRanks(context, recipient); - }); + ApplicationDependencies.getJobManager().add(new ConversationShortcutUpdateJob()); } } @@ -139,27 +134,44 @@ public final class ConversationUtil { return getShortcutId(recipient.getId()); } + @RequiresApi(CONVERSATION_SUPPORT_VERSION) + public static int getMaxShortcuts(@NonNull Context context) { + ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context); + return shortcutManager.getMaxShortcutCountPerActivity(); + } + /** - * Updates the rank of each existing shortcut by 1 and then publishes a new shortcut of rank 0 - * for the given recipient. + * Sets the shortcuts to match the provided recipient list. This call may fail due to getting + * rate-limited. + * + * @param rankedRecipients The recipients in descending priority order. Meaning the most important + * recipient should be first in the list. + * @return True if the update was successful, false if we were rate-limited. */ @RequiresApi(CONVERSATION_SUPPORT_VERSION) @WorkerThread - private static void pushShortcutAndUpdateRanks(@NonNull Context context, @NonNull Recipient recipient) { - ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context); - List currentShortcuts = shortcutManager.getDynamicShortcuts(); + public static boolean setActiveShortcuts(@NonNull Context context, @NonNull List rankedRecipients) { + ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context); - if (Util.hasItems(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); - } + if (shortcutManager.isRateLimitingActive()) { + return false; } - pushShortcutForRecipientInternal(context, recipient, 0); + int maxShortcuts = shortcutManager.getMaxShortcutCountPerActivity(); + + if (rankedRecipients.size() > maxShortcuts) { + Log.w(TAG, "Too many recipients provided! Provided: " + rankedRecipients.size() + ", Max: " + maxShortcuts); + rankedRecipients = rankedRecipients.subList(0, maxShortcuts); + } + + List shortcuts = new ArrayList<>(rankedRecipients.size()); + + for (int i = 0; i < rankedRecipients.size(); i++) { + ShortcutInfo info = buildShortcutInfo(context, rankedRecipients.get(i), i); + shortcuts.add(info); + } + + return shortcutManager.setDynamicShortcuts(shortcuts); } /**