QuickConversationsService.java

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