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