QuickConversationsService.java

  1package eu.siacs.conversations.services;
  2
  3
  4import android.content.SharedPreferences;
  5import android.net.Uri;
  6import android.os.SystemClock;
  7import android.preference.PreferenceManager;
  8import android.util.Log;
  9
 10import java.io.BufferedWriter;
 11import java.io.OutputStream;
 12import java.io.OutputStreamWriter;
 13import java.net.ConnectException;
 14import java.net.HttpURLConnection;
 15import java.net.URL;
 16import java.net.UnknownHostException;
 17import java.security.SecureRandom;
 18import java.util.ArrayList;
 19import java.util.Collection;
 20import java.util.Collections;
 21import java.util.List;
 22import java.util.Locale;
 23import java.util.Map;
 24import java.util.Set;
 25import java.util.UUID;
 26import java.util.WeakHashMap;
 27import java.util.concurrent.atomic.AtomicBoolean;
 28
 29import javax.net.ssl.SSLHandshakeException;
 30
 31import eu.siacs.conversations.Config;
 32import eu.siacs.conversations.android.JabberIdContact;
 33import eu.siacs.conversations.android.PhoneNumberContact;
 34import eu.siacs.conversations.crypto.sasl.Plain;
 35import eu.siacs.conversations.entities.Account;
 36import eu.siacs.conversations.entities.Contact;
 37import eu.siacs.conversations.utils.AccountUtils;
 38import eu.siacs.conversations.utils.CryptoHelper;
 39import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 40import eu.siacs.conversations.xml.Element;
 41import eu.siacs.conversations.xml.Namespace;
 42import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 43import eu.siacs.conversations.xmpp.XmppConnection;
 44import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 45import io.michaelrocks.libphonenumber.android.Phonenumber;
 46import rocks.xmpp.addr.Jid;
 47
 48public class QuickConversationsService extends AbstractQuickConversationsService {
 49
 50
 51    public static final int API_ERROR_OTHER = -1;
 52    public static final int API_ERROR_UNKNOWN_HOST = -2;
 53    public static final int API_ERROR_CONNECT = -3;
 54    public static final int API_ERROR_SSL_HANDSHAKE = -4;
 55    public static final int API_ERROR_AIRPLANE_MODE = -5;
 56
 57    private static final String BASE_URL = "http://venus.fritz.box:4567";
 58
 59    private static final String INSTALLATION_ID = "eu.siacs.conversations.installation-id";
 60
 61    private final Set<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
 62    private final Set<OnVerification> mOnVerification = Collections.newSetFromMap(new WeakHashMap<>());
 63
 64    private final AtomicBoolean mVerificationInProgress = new AtomicBoolean(false);
 65    private final AtomicBoolean mVerificationRequestInProgress = new AtomicBoolean(false);
 66
 67    QuickConversationsService(XmppConnectionService xmppConnectionService) {
 68        super(xmppConnectionService);
 69    }
 70
 71    private static long retryAfter(HttpURLConnection connection) {
 72        try {
 73            return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
 74        } catch (Exception e) {
 75            return 0;
 76        }
 77    }
 78
 79    public void addOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
 80        synchronized (mOnVerificationRequested) {
 81            mOnVerificationRequested.add(onVerificationRequested);
 82        }
 83    }
 84
 85    public void removeOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
 86        synchronized (mOnVerificationRequested) {
 87            mOnVerificationRequested.remove(onVerificationRequested);
 88        }
 89    }
 90
 91    public void addOnVerificationListener(OnVerification onVerification) {
 92        synchronized (mOnVerification) {
 93            mOnVerification.add(onVerification);
 94        }
 95    }
 96
 97    public void removeOnVerificationListener(OnVerification onVerification) {
 98        synchronized (mOnVerification) {
 99            mOnVerification.remove(onVerification);
100        }
101    }
102
103    public void requestVerification(Phonenumber.PhoneNumber phoneNumber) {
104        final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
105        if (mVerificationRequestInProgress.compareAndSet(false, true)) {
106            new Thread(() -> {
107                try {
108
109                    Thread.sleep(5000);
110
111                    final URL url = new URL(BASE_URL + "/authentication/" + e164);
112                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
113                    connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
114                    connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
115                    setHeader(connection);
116                    final int code = connection.getResponseCode();
117                    if (code == 200) {
118                        createAccountAndWait(phoneNumber, 0L);
119                    } else if (code == 429) {
120                        createAccountAndWait(phoneNumber, retryAfter(connection));
121                    } else {
122                        synchronized (mOnVerificationRequested) {
123                            for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
124                                onVerificationRequested.onVerificationRequestFailed(code);
125                            }
126                        }
127                    }
128                } catch (Exception e) {
129                    final int code = getApiErrorCode(e);
130                    synchronized (mOnVerificationRequested) {
131                        for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
132                            onVerificationRequested.onVerificationRequestFailed(code);
133                        }
134                    }
135                } finally {
136                    mVerificationRequestInProgress.set(false);
137                }
138            }).start();
139        }
140
141
142    }
143
144    private void createAccountAndWait(Phonenumber.PhoneNumber phoneNumber, final long timestamp) {
145        String local = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
146        Log.d(Config.LOGTAG, "requesting verification for " + PhoneNumberUtilWrapper.normalize(service, phoneNumber));
147        Jid jid = Jid.of(local, Config.QUICKSY_DOMAIN, null);
148        Account account = AccountUtils.getFirst(service);
149        if (account == null || !account.getJid().asBareJid().equals(jid.asBareJid())) {
150            if (account != null) {
151                service.deleteAccount(account);
152            }
153            account = new Account(jid, CryptoHelper.createPassword(new SecureRandom()));
154            account.setOption(Account.OPTION_DISABLED, true);
155            account.setOption(Account.OPTION_UNVERIFIED, true);
156            service.createAccount(account);
157        }
158        synchronized (mOnVerificationRequested) {
159            for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
160                if (timestamp <= 0) {
161                    onVerificationRequested.onVerificationRequested();
162                } else {
163                    onVerificationRequested.onVerificationRequestedRetryAt(timestamp);
164                }
165            }
166        }
167    }
168
169    public void verify(final Account account, String pin) {
170        if (mVerificationInProgress.compareAndSet(false, true)) {
171            new Thread(() -> {
172                try {
173
174                    Thread.sleep(5000);
175
176                    final URL url = new URL(BASE_URL + "/password");
177                    final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
178                    connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
179                    connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
180                    connection.setRequestMethod("POST");
181                    connection.setRequestProperty("Authorization", Plain.getMessage(account.getUsername(), pin));
182                    setHeader(connection);
183                    final OutputStream os = connection.getOutputStream();
184                    final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
185                    writer.write(account.getPassword());
186                    writer.flush();
187                    writer.close();
188                    os.close();
189                    connection.connect();
190                    final int code = connection.getResponseCode();
191                    if (code == 200) {
192                        account.setOption(Account.OPTION_UNVERIFIED, false);
193                        account.setOption(Account.OPTION_DISABLED, false);
194                        service.updateAccount(account);
195                        synchronized (mOnVerification) {
196                            for (OnVerification onVerification : mOnVerification) {
197                                onVerification.onVerificationSucceeded();
198                            }
199                        }
200                    } else if (code == 429) {
201                        final long retryAfter = retryAfter(connection);
202                        synchronized (mOnVerification) {
203                            for (OnVerification onVerification : mOnVerification) {
204                                onVerification.onVerificationRetryAt(retryAfter);
205                            }
206                        }
207                    } else {
208                        synchronized (mOnVerification) {
209                            for (OnVerification onVerification : mOnVerification) {
210                                onVerification.onVerificationFailed(code);
211                            }
212                        }
213                    }
214                } catch (Exception e) {
215                    final int code = getApiErrorCode(e);
216                    synchronized (mOnVerification) {
217                        for (OnVerification onVerification : mOnVerification) {
218                            onVerification.onVerificationFailed(code);
219                        }
220                    }
221                } finally {
222                    mVerificationInProgress.set(false);
223                }
224            }).start();
225        }
226    }
227
228    private void setHeader(HttpURLConnection connection) {
229        connection.setRequestProperty("User-Agent", service.getIqGenerator().getUserAgent());
230        connection.setRequestProperty("Installation-Id", getInstallationId());
231        connection.setRequestProperty("Accept-Language", Locale.getDefault().getLanguage());
232    }
233
234    private String getInstallationId() {
235        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(service);
236        String id = preferences.getString(INSTALLATION_ID, null);
237        if (id != null) {
238            return id;
239        } else {
240            id = UUID.randomUUID().toString();
241            preferences.edit().putString(INSTALLATION_ID, id).apply();
242            return id;
243        }
244
245    }
246
247    private int getApiErrorCode(Exception e) {
248        if (!service.hasInternetConnection()) {
249            return API_ERROR_AIRPLANE_MODE;
250        } else if (e instanceof UnknownHostException) {
251            return API_ERROR_UNKNOWN_HOST;
252        } else if (e instanceof ConnectException) {
253            return API_ERROR_CONNECT;
254        } else if (e instanceof SSLHandshakeException) {
255            return API_ERROR_SSL_HANDSHAKE;
256        } else {
257            Log.d(Config.LOGTAG, e.getClass().getName());
258            return API_ERROR_OTHER;
259        }
260    }
261
262    public boolean isVerifying() {
263        return mVerificationInProgress.get();
264    }
265
266    public boolean isRequestingVerification() {
267        return mVerificationRequestInProgress.get();
268    }
269
270    @Override
271    public void considerSync() {
272        Map<String, PhoneNumberContact> contacts = PhoneNumberContact.load(service);
273        for (Account account : service.getAccounts()) {
274            refresh(account, contacts.values());
275            if (!considerSync(account, contacts)) {
276                service.syncRoster(account);
277            }
278        }
279    }
280
281    private void refresh(Account account, Collection<PhoneNumberContact> contacts) {
282        for(Contact contact : account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
283            final Uri uri = contact.getSystemAccount();
284            if (uri == null) {
285                continue;
286            }
287            PhoneNumberContact phoneNumberContact = findByUri(contacts, uri);
288            final boolean needsCacheClean;
289            if (phoneNumberContact != null) {
290                needsCacheClean = contact.setPhoneContact(phoneNumberContact);
291            } else {
292                needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
293                Log.d(Config.LOGTAG,uri.toString()+" vanished from address book");
294            }
295            if (needsCacheClean) {
296                service.getAvatarService().clear(contact);
297            }
298        }
299    }
300
301    private boolean considerSync(Account account, final Map<String, PhoneNumberContact> contacts) {
302        XmppConnection xmppConnection = account.getXmppConnection();
303        Jid syncServer = xmppConnection == null ? null : xmppConnection.findDiscoItemByFeature(Namespace.SYNCHRONIZATION);
304        if (syncServer == null) {
305            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping sync. no sync server found");
306            return false;
307        }
308        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending phone list to " + syncServer);
309        List<Element> entries = new ArrayList<>();
310        for (PhoneNumberContact c : contacts.values()) {
311            entries.add(new Element("entry").setAttribute("number", c.getPhoneNumber()));
312        }
313        IqPacket query = new IqPacket(IqPacket.TYPE.GET);
314        query.setTo(syncServer);
315        query.addChild(new Element("phone-book", Namespace.SYNCHRONIZATION).setChildren(entries));
316        service.sendIqPacket(account, query, (a, response) -> {
317            if (response.getType() == IqPacket.TYPE.RESULT) {
318                List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(PhoneNumberContact.class);
319                final Element phoneBook = response.findChild("phone-book", Namespace.SYNCHRONIZATION);
320                if (phoneBook != null) {
321                    for(Entry entry : Entry.ofPhoneBook(phoneBook)) {
322                        PhoneNumberContact phoneContact = contacts.get(entry.getNumber());
323                        if (phoneContact == null) {
324                            continue;
325                        }
326                        for(Jid jid : entry.getJids()) {
327                            Contact contact = account.getRoster().getContact(jid);
328                            final boolean needsCacheClean = contact.setPhoneContact(phoneContact);
329                            if (needsCacheClean) {
330                                service.getAvatarService().clear(contact);
331                            }
332                            withSystemAccounts.remove(contact);
333                        }
334                    }
335                }
336                for (Contact contact : withSystemAccounts) {
337                    boolean needsCacheClean = contact.unsetPhoneContact(JabberIdContact.class);
338                    if (needsCacheClean) {
339                        service.getAvatarService().clear(contact);
340                    }
341                }
342            }
343            service.syncRoster(account);
344        });
345        return true;
346    }
347
348    private static PhoneNumberContact findByUri(Collection<PhoneNumberContact> haystack, Uri needle) {
349        for(PhoneNumberContact contact : haystack) {
350            if (needle.equals(contact.getLookupUri())) {
351                return contact;
352            }
353        }
354        return null;
355    }
356
357    public static class Entry {
358        private final List<Jid> jids;
359        private final String number;
360
361        private Entry(String number, List<Jid> jids) {
362            this.number = number;
363            this.jids = jids;
364        }
365
366        public static Entry of(Element element) {
367            final String number = element.getAttribute("number");
368            final List<Jid> jids = new ArrayList<>();
369            for (Element jidElement : element.getChildren()) {
370                String content = jidElement.getContent();
371                if (content != null) {
372                    jids.add(Jid.of(content));
373                }
374            }
375            return new Entry(number, jids);
376        }
377
378        public static List<Entry> ofPhoneBook(Element phoneBook) {
379            List<Entry> entries = new ArrayList<>();
380            for (Element entry : phoneBook.getChildren()) {
381                if ("entry".equals(entry.getName())) {
382                    entries.add(of(entry));
383                }
384            }
385            return entries;
386        }
387
388        public List<Jid> getJids() {
389            return jids;
390        }
391
392        public String getNumber() {
393            return number;
394        }
395    }
396
397    public interface OnVerificationRequested {
398        void onVerificationRequestFailed(int code);
399
400        void onVerificationRequested();
401
402        void onVerificationRequestedRetryAt(long timestamp);
403    }
404
405    public interface OnVerification {
406        void onVerificationFailed(int code);
407
408        void onVerificationSucceeded();
409
410        void onVerificationRetryAt(long timestamp);
411    }
412}