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