That fuck shit the fascists are using
at master 321 lines 13 kB view raw
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}