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