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