1package eu.siacs.conversations.services;
2
3import java.util.concurrent.atomic.AtomicInteger;
4import java.util.Collection;
5import java.util.Collections;
6import java.util.HashMap;
7import java.util.Set;
8import java.util.Objects;
9import java.util.ArrayList;
10import java.util.List;
11import java.util.Map;
12import java.util.stream.Collectors;
13import java.util.stream.Stream;
14
15import com.google.common.collect.ImmutableMap;
16import com.google.common.collect.ImmutableList;
17
18import android.content.Intent;
19import android.os.SystemClock;
20import android.net.Uri;
21import android.util.Log;
22
23import eu.siacs.conversations.Config;
24import eu.siacs.conversations.android.PhoneNumberContact;
25import eu.siacs.conversations.entities.Account;
26import eu.siacs.conversations.entities.Contact;
27import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
28import eu.siacs.conversations.xmpp.Jid;
29import eu.siacs.conversations.xmpp.manager.RosterManager;
30
31public class QuickConversationsService extends AbstractQuickConversationsService {
32
33 protected final AtomicInteger mRunningSyncJobs = new AtomicInteger(0);
34 protected final SerialSingleThreadExecutor mSerialSingleThreadExecutor = new SerialSingleThreadExecutor(QuickConversationsService.class.getSimpleName());
35 protected HashMap<String,Attempt> mLastSyncAttempt = new HashMap<>();
36
37 QuickConversationsService(XmppConnectionService xmppConnectionService) {
38 super(xmppConnectionService);
39 }
40
41 @Override
42 public void considerSync() {
43 considerSync(false);
44 }
45
46 @Override
47 public void signalAccountStateChange() {
48
49 }
50
51 @Override
52 public boolean isSynchronizing() {
53 return mRunningSyncJobs.get() > 0;
54 }
55
56 @Override
57 public void considerSyncBackground(boolean force) {
58 mRunningSyncJobs.incrementAndGet();
59 mSerialSingleThreadExecutor.execute(() -> {
60 considerSync(force);
61 if (mRunningSyncJobs.decrementAndGet() == 0) {
62 service.updateRosterUi(XmppConnectionService.UpdateRosterReason.INIT);
63 }
64 });
65 }
66
67 @Override
68 public void handleSmsReceived(Intent intent) {
69 Log.d(Config.LOGTAG,"ignoring received SMS");
70 }
71
72 protected static String getNumber(final Set<String> gateways, final Contact contact) {
73 final Jid jid = contact.getJid();
74 if (jid.getLocal() != null && ("quicksy.im".equals(jid.getDomain()) || gateways.contains(jid.getDomain()))) {
75 return jid.getLocal();
76 }
77 return null;
78 }
79
80 protected void refresh(Account account, final Set<String> gateways, Collection<PhoneNumberContact> phoneNumberContacts) {
81 for (Contact contact : account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
82 final Uri uri = contact.getSystemAccount();
83 if (uri == null) {
84 continue;
85 }
86 final String number = getNumber(gateways, contact);
87 final PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUriOrNumber(phoneNumberContacts, uri, number);
88 final boolean needsCacheClean;
89 if (phoneNumberContact != null) {
90 if (!uri.equals(phoneNumberContact.getLookupUri())) {
91 Log.d(Config.LOGTAG, "lookupUri has changed from " + uri + " to " + phoneNumberContact.getLookupUri());
92 }
93 needsCacheClean = contact.setPhoneContact(phoneNumberContact);
94 } else {
95 needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
96 Log.d(Config.LOGTAG, uri.toString() + " vanished from address book");
97 }
98 if (needsCacheClean) {
99 service.getAvatarService().clear(contact);
100 }
101 }
102 }
103
104 protected void considerSync(boolean forced) {
105 ImmutableMap<String, PhoneNumberContact> allContacts = null;
106 for (final Account account : ImmutableList.copyOf(service.getAccounts())) {
107 final var gateways = gateways(account);
108 if (allContacts == null) allContacts = PhoneNumberContact.load(service);
109 refresh(account, gateways, allContacts.values());
110 if (!considerSync(account, gateways, allContacts, forced)) {
111 account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
112 }
113 }
114 }
115
116 protected Set<String> gateways(final Account account) {
117 return Stream.concat(
118 account.getGateways("pstn").stream(),
119 account.getGateways("sms").stream()
120 ).map(a -> a.getJid().asBareJid().toString()).collect(Collectors.toSet());
121 }
122
123 protected boolean considerSync(final Account account, final Set<String> gateways, final Map<String, PhoneNumberContact> contacts, final boolean forced) {
124 final int hash = Objects.hash(contacts.keySet(), gateways);
125 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": consider sync of " + hash);
126 if (!mLastSyncAttempt.getOrDefault(account.getUuid(), Attempt.NULL).retry(hash) && !forced) {
127 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not attempt sync");
128 return false;
129 }
130 mRunningSyncJobs.incrementAndGet();
131
132 mLastSyncAttempt.put(account.getUuid(), Attempt.create(hash));
133 final List<Contact> withSystemAccounts = new ArrayList<>(account.getRoster().getWithSystemAccounts(PhoneNumberContact.class));
134 for (Map.Entry<String, PhoneNumberContact> item : contacts.entrySet()) {
135 PhoneNumberContact phoneContact = item.getValue();
136 for(String gateway : gateways) {
137 final Jid jid = Jid.ofLocalAndDomain(phoneContact.getPhoneNumber(), gateway);
138 final Contact contact = account.getRoster().getContact(jid);
139 boolean needsCacheClean = contact.setPhoneContact(phoneContact);
140 needsCacheClean |= contact.setSystemTags(phoneContact.getTags());
141 if (needsCacheClean) {
142 service.getAvatarService().clear(contact);
143 }
144 withSystemAccounts.remove(contact);
145 }
146 }
147 for (final Contact contact : withSystemAccounts) {
148 final boolean needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
149 if (needsCacheClean) {
150 service.getAvatarService().clear(contact);
151 }
152 }
153
154 mRunningSyncJobs.decrementAndGet();
155 account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
156 service.updateRosterUi(XmppConnectionService.UpdateRosterReason.INIT);
157 return true;
158 }
159
160 protected static class Attempt {
161 private final long timestamp;
162 private final int hash;
163
164 private static final Attempt NULL = new Attempt(0, 0);
165
166 private Attempt(long timestamp, int hash) {
167 this.timestamp = timestamp;
168 this.hash = hash;
169 }
170
171 public static Attempt create(int hash) {
172 return new Attempt(SystemClock.elapsedRealtime(), hash);
173 }
174
175 public boolean retry(int hash) {
176 return hash != this.hash || SystemClock.elapsedRealtime() - timestamp >= Config.CONTACT_SYNC_RETRY_INTERVAL;
177 }
178 }
179}