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