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