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;
 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}