ShortcutService.java

  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}