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}