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