1package eu.siacs.conversations.services;
2
3import android.annotation.TargetApi;
4import android.content.Intent;
5import android.content.pm.ShortcutManager;
6import android.graphics.Bitmap;
7import android.net.Uri;
8import android.os.Build;
9import android.os.PersistableBundle;
10import android.util.Log;
11import androidx.annotation.NonNull;
12import androidx.core.content.pm.ShortcutInfoCompat;
13import androidx.core.content.pm.ShortcutManagerCompat;
14import androidx.core.graphics.drawable.IconCompat;
15
16import java.util.ArrayList;
17import java.util.Collection;
18import java.util.HashMap;
19import java.util.List;
20import java.util.Set;
21
22import com.google.common.base.Joiner;
23import com.google.common.collect.ImmutableList;
24import com.google.common.collect.ImmutableMap;
25import com.google.common.collect.ImmutableSet;
26import com.google.common.collect.Maps;
27
28import eu.siacs.conversations.Config;
29import eu.siacs.conversations.entities.Account;
30import eu.siacs.conversations.entities.Contact;
31import eu.siacs.conversations.entities.MucOptions;
32import eu.siacs.conversations.ui.ConversationsActivity;
33import eu.siacs.conversations.ui.StartConversationActivity;
34import eu.siacs.conversations.ui.ConversationsActivity;
35import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
36import eu.siacs.conversations.xmpp.Jid;
37
38public class ShortcutService {
39
40 public static final char ID_SEPARATOR = '#';
41
42 private final XmppConnectionService xmppConnectionService;
43 private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor =
44 new ReplacingSerialSingleThreadExecutor(ShortcutService.class.getSimpleName());
45
46 public ShortcutService(final XmppConnectionService xmppConnectionService) {
47 this.xmppConnectionService = xmppConnectionService;
48 }
49
50 public void refresh() {
51 refresh(false);
52 }
53
54 public void refresh(final boolean forceUpdate) {
55 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
56 final Runnable r = () -> refreshImpl(forceUpdate);
57 replacingSerialSingleThreadExecutor.execute(r);
58 }
59 }
60
61 @TargetApi(25)
62 public void report(Contact contact) {
63 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
64 ShortcutManager shortcutManager =
65 xmppConnectionService.getSystemService(ShortcutManager.class);
66 shortcutManager.reportShortcutUsed(getShortcutId(contact));
67 }
68 }
69
70 @TargetApi(25)
71 private void refreshImpl(final boolean forceUpdate) {
72 final var frequentContacts = xmppConnectionService.databaseBackend.getFrequentContacts(30);
73 final var accounts =
74 ImmutableMap.copyOf(
75 Maps.uniqueIndex(xmppConnectionService.getAccounts(), Account::getUuid));
76 final var contactBuilder = new ImmutableMap.Builder<FrequentContact, Contact>();
77 for (final var frequentContact : frequentContacts) {
78 final Account account = accounts.get(frequentContact.account);
79 if (account != null) {
80 final var contact = account.getRoster().getContact(frequentContact.contact);
81 contactBuilder.put(frequentContact, contact);
82 }
83 }
84 final var contacts = contactBuilder.build();
85 final var current = ShortcutManagerCompat.getDynamicShortcuts(xmppConnectionService);
86 boolean needsUpdate = forceUpdate || contactsChanged(contacts.values(), current);
87 if (!needsUpdate) {
88 Log.d(Config.LOGTAG, "skipping shortcut update");
89 return;
90 }
91 final var newDynamicShortcuts = new ImmutableList.Builder<ShortcutInfoCompat>();
92 for (final var entry : contacts.entrySet()) {
93 final var contact = entry.getValue();
94 final var conversation = entry.getKey().conversation;
95 final var shortcut = getShortcutInfo(contact, conversation);
96 newDynamicShortcuts.add(shortcut);
97 }
98 if (ShortcutManagerCompat.setDynamicShortcuts(
99 xmppConnectionService, newDynamicShortcuts.build())) {
100 Log.d(Config.LOGTAG, "updated dynamic shortcuts");
101 } else {
102 Log.d(Config.LOGTAG, "unable to update dynamic shortcuts");
103 }
104 }
105
106 public ShortcutInfoCompat getShortcutInfo(final Contact contact) {
107 final var conversation = xmppConnectionService.find(contact);
108 final var uuid = conversation == null ? null : conversation.getUuid();
109 return getShortcutInfo(contact, uuid);
110 }
111
112 public ShortcutInfoCompat getShortcutInfo(final Contact contact, final String conversation) {
113 final ShortcutInfoCompat.Builder builder =
114 new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(contact))
115 .setShortLabel(contact.getDisplayName())
116 .setIntent(getShortcutIntent(contact))
117 .setIsConversation();
118 builder.setIcon(
119 IconCompat.createWithBitmap(
120 xmppConnectionService.getAvatarService().getRoundedShortcut(contact)));
121 if (conversation != null) {
122 setConversation(builder, conversation);
123 }
124 return builder.build();
125 }
126
127 public ShortcutInfoCompat getShortcutInfo(final MucOptions mucOptions) {
128 final ShortcutInfoCompat.Builder builder =
129 new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(mucOptions))
130 .setShortLabel(mucOptions.getConversation().getName())
131 .setIntent(getShortcutIntent(mucOptions))
132 .setIsConversation();
133 builder.setIcon(
134 IconCompat.createWithBitmap(
135 xmppConnectionService.getAvatarService().getRoundedShortcut(mucOptions)));
136 setConversation(builder, mucOptions.getConversation().getUuid());
137 return builder.build();
138 }
139
140 private static void setConversation(
141 final ShortcutInfoCompat.Builder builder, @NonNull final String conversation) {
142 builder.setCategories(ImmutableSet.of("eu.siacs.conversations.category.SHARE_TARGET"));
143 final var extras = new PersistableBundle();
144 extras.putString(ConversationsActivity.EXTRA_CONVERSATION, conversation);
145 builder.setExtras(extras);
146 }
147
148 private static boolean contactsChanged(
149 final Collection<Contact> needles, final List<ShortcutInfoCompat> haystack) {
150 for (final Contact needle : needles) {
151 if (!contactExists(needle, haystack)) {
152 return true;
153 }
154 }
155 return needles.size() != haystack.size();
156 }
157
158 @TargetApi(25)
159 private static boolean contactExists(
160 final Contact needle, final List<ShortcutInfoCompat> haystack) {
161 for (final ShortcutInfoCompat shortcutInfo : haystack) {
162 final var label = shortcutInfo.getShortLabel();
163 if (getShortcutId(needle).equals(shortcutInfo.getId())
164 && needle.getDisplayName().equals(label.toString())) {
165 return true;
166 }
167 }
168 return false;
169 }
170
171 private static String getShortcutId(final Contact contact) {
172 return Joiner.on(ID_SEPARATOR)
173 .join(
174 contact.getAccount().getJid().asBareJid().toString(),
175 contact.getJid().asBareJid().toString());
176 }
177
178 private static String getShortcutId(final MucOptions mucOptions) {
179 final Account account = mucOptions.getAccount();
180 final Jid jid = mucOptions.getConversation().getJid();
181 return Joiner.on(ID_SEPARATOR)
182 .join(account.getJid().asBareJid().toString(), jid.asBareJid().toString());
183 }
184
185 private Intent getShortcutIntent(final MucOptions mucOptions) {
186 final Account account = mucOptions.getAccount();
187 return getShortcutIntent(
188 account,
189 Uri.parse(
190 String.format(
191 "xmpp:%s?join",
192 mucOptions.getConversation().getJid().asBareJid().toString())));
193 }
194
195 private Intent getShortcutIntent(final Contact contact) {
196 return getShortcutIntent(
197 contact.getAccount(), Uri.parse("xmpp:" + contact.getJid().asBareJid().toString()));
198 }
199
200 private Intent getShortcutIntent(final Account account, final Uri uri) {
201 Intent intent = new Intent(xmppConnectionService, StartConversationActivity.class);
202 intent.setAction(Intent.ACTION_VIEW);
203 intent.setData(uri);
204 intent.putExtra("account", account.getJid().asBareJid().toString());
205 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
206 return intent;
207 }
208
209 @NonNull
210 public Intent createShortcut(final Contact contact, final boolean legacy) {
211 Intent intent;
212 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !legacy) {
213 final var shortcut = getShortcutInfo(contact);
214 intent =
215 ShortcutManagerCompat.createShortcutResultIntent(
216 xmppConnectionService, shortcut);
217 } else {
218 intent = createShortcutResultIntent(contact);
219 }
220 return intent;
221 }
222
223 @NonNull
224 private Intent createShortcutResultIntent(final Contact contact) {
225 AvatarService avatarService = xmppConnectionService.getAvatarService();
226 Bitmap icon = avatarService.getRoundedShortcutWithIcon(contact);
227 Intent intent = new Intent();
228 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, contact.getDisplayName());
229 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
230 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, getShortcutIntent(contact));
231 return intent;
232 }
233
234 public static class FrequentContact {
235 private final String conversation;
236 private final String account;
237 private final Jid contact;
238
239 public FrequentContact(final String conversation, final String account, final Jid contact) {
240 this.conversation = conversation;
241 this.account = account;
242 this.contact = contact;
243 }
244 }
245}