That fuck shit the fascists are using
1package org.tm.archive.util;
2
3import android.content.ComponentName;
4import android.content.Context;
5
6import androidx.annotation.NonNull;
7import androidx.annotation.Nullable;
8import androidx.annotation.WorkerThread;
9import androidx.core.app.Person;
10import androidx.core.content.LocusIdCompat;
11import androidx.core.content.pm.ShortcutInfoCompat;
12import androidx.core.content.pm.ShortcutManagerCompat;
13
14import com.annimon.stream.Stream;
15import com.google.common.collect.Sets;
16
17import org.signal.core.util.concurrent.SignalExecutors;
18import org.signal.core.util.logging.Log;
19import org.tm.archive.R;
20import org.tm.archive.components.settings.app.appearance.appicon.util.AppIconUtility;
21import org.tm.archive.conversation.ConversationIntents;
22import org.tm.archive.database.GroupTable;
23import org.tm.archive.database.SignalDatabase;
24import org.tm.archive.groups.GroupId;
25import org.tm.archive.jobs.ConversationShortcutUpdateJob;
26import org.tm.archive.notifications.NotificationChannels;
27import org.tm.archive.recipients.Recipient;
28import org.tm.archive.recipients.RecipientId;
29
30import java.util.ArrayList;
31import java.util.Collection;
32import java.util.Collections;
33import java.util.List;
34import java.util.stream.Collectors;
35
36/**
37 * ConversationUtil encapsulates support for Android 11+'s new Conversations system
38 */
39public final class ConversationUtil {
40
41 private static final String TAG = Log.tag(ConversationUtil.class);
42
43 public static final int CONVERSATION_SUPPORT_VERSION = 30;
44
45 private static final String CATEGORY_SHARE_TARGET = "org.tm.archive.sharing.CATEGORY_SHARE_TARGET";
46
47 private static final String CAPABILITY_SEND_MESSAGE = "actions.intent.SEND_MESSAGE";
48 private static final String CAPABILITY_RECEIVE_MESSAGE = "actions.intent.RECEIVE_MESSAGE";
49
50 private static final String PARAMETER_RECIPIENT_TYPE = "message.recipient.@type";
51 private static final String PARAMETER_SENDER_TYPE = "message.sender.@type";
52
53 private static final List<String> PARAMETERS_AUDIENCE = Collections.singletonList("Audience");
54
55 private ConversationUtil() {}
56
57
58 /**
59 * @return The stringified channel id for a given Recipient
60 */
61 @WorkerThread
62 public static @NonNull String getChannelId(@NonNull Context context, @NonNull Recipient recipient) {
63 Recipient resolved = recipient.resolve();
64
65 return resolved.getNotificationChannel() != null ? resolved.getNotificationChannel() : NotificationChannels.getInstance().getMessagesChannel();
66 }
67
68 /**
69 * Enqueues a job to update the list of shortcuts.
70 */
71 public static void refreshRecipientShortcuts() {
72 ConversationShortcutUpdateJob.enqueue();
73 }
74
75 /**
76 * Synchronously pushes a dynamic shortcut for the given recipient.
77 * <p>
78 * The recipient is given a high ranking with the intention of not appearing immediately in results.
79 *
80 * @return True if it succeeded, or false if it was rate-limited.
81 */
82 @WorkerThread
83 public static boolean pushShortcutForRecipientSync(@NonNull Context context, @NonNull Recipient recipient, @NonNull Direction direction ) {
84 List<ShortcutInfoCompat> shortcuts = ShortcutManagerCompat.getDynamicShortcuts(context);
85 return pushShortcutForRecipientInternal(context, recipient, shortcuts.size(), direction);
86 }
87
88 /**
89 * Clears all currently set dynamic shortcuts
90 */
91 public static void clearAllShortcuts(@NonNull Context context) {
92 List<ShortcutInfoCompat> shortcutInfos = ShortcutManagerCompat.getDynamicShortcuts(context);
93
94 ShortcutManagerCompat.removeLongLivedShortcuts(context, Stream.of(shortcutInfos).map(ShortcutInfoCompat::getId).toList());
95 }
96
97 /**
98 * Clears the shortcuts tied to a given thread.
99 */
100 public static void clearShortcuts(@NonNull Context context, @NonNull Collection<RecipientId> recipientIds) {
101 SignalExecutors.BOUNDED.execute(() -> {
102 ShortcutManagerCompat.removeLongLivedShortcuts(context, Stream.of(recipientIds).withoutNulls().map(ConversationUtil::getShortcutId).toList());
103 });
104 }
105
106 /**
107 * Returns an ID that is unique between all recipients.
108 *
109 * @param recipientId The recipient ID to get a shortcut ID for
110 * @return A unique identifier that is stable for a given recipient id
111 */
112 public static @NonNull String getShortcutId(@NonNull RecipientId recipientId) {
113 return recipientId.serialize();
114 }
115
116 /**
117 * Returns an ID that is unique between all recipients.
118 *
119 * @param recipient The recipient to get a shortcut for.
120 * @return A unique identifier that is stable for a given recipient id
121 */
122 public static @NonNull String getShortcutId(@NonNull Recipient recipient) {
123 return getShortcutId(recipient.getId());
124 }
125
126 /**
127 * Extract the recipient id from the provided shortcutId.
128 */
129 public static @Nullable RecipientId getRecipientId(@Nullable String shortcutId) {
130 if (shortcutId == null) {
131 return null;
132 }
133
134 try {
135 return RecipientId.from(shortcutId);
136 } catch (Throwable t) {
137 Log.d(TAG, "Unable to parse recipientId from shortcutId", t);
138 return null;
139 }
140 }
141
142 public static int getMaxShortcuts(@NonNull Context context) {
143 return Math.min(ShortcutManagerCompat.getMaxShortcutCountPerActivity(context), 150);
144 }
145
146 /**
147 * Removes the long-lived shortcuts for the given set of recipients.
148 */
149 @WorkerThread
150 public static void removeLongLivedShortcuts(@NonNull Context context, @NonNull Collection<RecipientId> recipients) {
151 ShortcutManagerCompat.removeLongLivedShortcuts(context, recipients.stream().map(ConversationUtil::getShortcutId).collect(Collectors.toList()));
152 }
153
154 /**
155 * Sets the shortcuts to match the provided recipient list. This call may fail due to getting
156 * rate-limited.
157 *
158 * @param rankedRecipients The recipients in descending priority order. Meaning the most important
159 * recipient should be first in the list.
160 * @return True if the update was successful, false if we were rate-limited.
161 */
162 @WorkerThread
163 public static boolean setActiveShortcuts(@NonNull Context context, @NonNull List<Recipient> rankedRecipients) {
164 if (ShortcutManagerCompat.isRateLimitingActive(context)) {
165 return false;
166 }
167
168 int maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context);
169
170 if (rankedRecipients.size() > maxShortcuts) {
171 Log.w(TAG, "Too many recipients provided! Provided: " + rankedRecipients.size() + ", Max: " + maxShortcuts);
172 rankedRecipients = rankedRecipients.subList(0, maxShortcuts);
173 }
174
175 List<ShortcutInfoCompat> shortcuts = new ArrayList<>(rankedRecipients.size());
176
177 ComponentName activityName = new AppIconUtility(context).currentAppIconComponentName();
178
179 for (int i = 0; i < rankedRecipients.size(); i++) {
180 ShortcutInfoCompat info = buildShortcutInfo(context, activityName, rankedRecipients.get(i), i, Direction.NONE);
181 shortcuts.add(info);
182 }
183
184 return ShortcutManagerCompat.setDynamicShortcuts(context, shortcuts);
185 }
186
187 /**
188 * Pushes a dynamic shortcut for a given recipient to the shortcut manager
189 *
190 * @return True if it succeeded, or false if it was rate-limited.
191 */
192 @WorkerThread
193 private static boolean pushShortcutForRecipientInternal(@NonNull Context context, @NonNull Recipient recipient, int rank, @NonNull Direction direction) {
194
195 ComponentName activityName = new AppIconUtility(context).currentAppIconComponentName();
196
197 ShortcutInfoCompat shortcutInfo = buildShortcutInfo(context, activityName, recipient, rank, direction);
198
199 return ShortcutManagerCompat.pushDynamicShortcut(context, shortcutInfo);
200 }
201
202 /**
203 * Builds the shortcut info object for a given Recipient.
204 *
205 * @param context The Context under which we are operating
206 * @param recipient The Recipient to generate a ShortcutInfo for
207 * @param rank The rank that should be assigned to this recipient
208 * @return The new ShortcutInfo
209 */
210 @WorkerThread
211 private static @NonNull ShortcutInfoCompat buildShortcutInfo(@NonNull Context context,
212 @NonNull ComponentName activity,
213 @NonNull Recipient recipient,
214 int rank,
215 @NonNull Direction direction)
216 {
217 Recipient resolved = recipient.resolve();
218 Person[] persons = buildPersons(context, resolved);
219 long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(resolved);
220 String shortName = resolved.isSelf() ? context.getString(R.string.note_to_self) : resolved.getShortDisplayName(context);
221 String longName = resolved.isSelf() ? context.getString(R.string.note_to_self) : resolved.getDisplayName(context);
222 String shortcutId = getShortcutId(resolved);
223
224 ShortcutInfoCompat.Builder builder = new ShortcutInfoCompat.Builder(context, shortcutId)
225 .setLongLived(true)
226 .setIntent(ConversationIntents.createBuilderSync(context, resolved.getId(), threadId).build())
227 .setShortLabel(shortName)
228 .setLongLabel(longName)
229 .setIcon(AvatarUtil.getIconCompat(context, resolved))
230 .setPersons(persons)
231 .setCategories(Sets.newHashSet(CATEGORY_SHARE_TARGET))
232 .setActivity(activity)
233 .setRank(rank)
234 .setLocusId(new LocusIdCompat(shortcutId));
235
236 if (direction == Direction.OUTGOING) {
237 if (recipient.isGroup()) {
238 builder.addCapabilityBinding(CAPABILITY_SEND_MESSAGE, PARAMETER_RECIPIENT_TYPE, PARAMETERS_AUDIENCE);
239 } else {
240 builder.addCapabilityBinding(CAPABILITY_SEND_MESSAGE);
241 }
242 } else if (direction == Direction.INCOMING) {
243 if (recipient.isGroup()) {
244 builder.addCapabilityBinding(CAPABILITY_RECEIVE_MESSAGE, PARAMETER_SENDER_TYPE, PARAMETERS_AUDIENCE);
245 } else {
246 builder.addCapabilityBinding(CAPABILITY_RECEIVE_MESSAGE);
247 }
248 }
249
250 return builder.build();
251 }
252
253 /**
254 * @return an array of Person objects correlating to members of a conversation (other than self)
255 */
256 @WorkerThread
257 private static @NonNull Person[] buildPersons(@NonNull Context context, @NonNull Recipient recipient) {
258 if (recipient.isGroup()) {
259 return buildPersonsForGroup(context, recipient.getGroupId().get());
260 } else {
261 return new Person[] { buildPerson(context, recipient) };
262 }
263 }
264
265 /**
266 * @return an array of Person objects correlating to members of a group (other than self)
267 */
268 @WorkerThread
269 private static @NonNull Person[] buildPersonsForGroup(@NonNull Context context, @NonNull GroupId groupId) {
270 List<Recipient> members = SignalDatabase.groups().getGroupMembers(groupId, GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
271
272 return Stream.of(members).map(member -> buildPersonWithoutIcon(context, member.resolve())).toArray(Person[]::new);
273 }
274
275 /**
276 * @return A Person object representing the given Recipient
277 */
278 public static @NonNull Person buildPersonWithoutIcon(@NonNull Context context, @NonNull Recipient recipient) {
279 return new Person.Builder()
280 .setKey(getShortcutId(recipient.getId()))
281 .setName(recipient.getDisplayName(context))
282 .setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null)
283 .build();
284 }
285
286 /**
287 * @return A Compat Library Person object representing the given Recipient
288 */
289 @WorkerThread
290 public static @NonNull Person buildPerson(@NonNull Context context, @NonNull Recipient recipient) {
291 return new Person.Builder()
292 .setKey(getShortcutId(recipient.getId()))
293 .setName(recipient.getDisplayName(context))
294 .setIcon(AvatarUtil.getIconCompat(context, recipient))
295 .setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null)
296 .build();
297 }
298
299 public enum Direction {
300 NONE(0), INCOMING(1), OUTGOING(2);
301
302 private final int value;
303
304 Direction(int value) {
305 this.value = value;
306 }
307
308 public int serialize() {
309 return value;
310 }
311
312 public static Direction deserialize(int value) {
313 switch (value) {
314 case 0: return NONE;
315 case 1: return INCOMING;
316 case 2: return OUTGOING;
317 default: throw new IllegalArgumentException("Unrecognized value: " + value);
318 }
319 }
320 }
321}