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.Collections;
 18import java.util.Locale;
 19import java.util.Set;
 20import java.util.UUID;
 21import java.util.WeakHashMap;
 22import java.util.concurrent.atomic.AtomicBoolean;
 23
 24import javax.net.ssl.SSLHandshakeException;
 25
 26import eu.siacs.conversations.Config;
 27import eu.siacs.conversations.crypto.sasl.Plain;
 28import eu.siacs.conversations.entities.Account;
 29import eu.siacs.conversations.utils.AccountUtils;
 30import eu.siacs.conversations.utils.CryptoHelper;
 31import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 32import io.michaelrocks.libphonenumber.android.Phonenumber;
 33import rocks.xmpp.addr.Jid;
 34
 35public class QuickConversationsService {
 36
 37
 38    public static final int API_ERROR_OTHER = -1;
 39    public static final int API_ERROR_UNKNOWN_HOST = -2;
 40    public static final int API_ERROR_CONNECT = -3;
 41    public static final int API_ERROR_SSL_HANDSHAKE = -4;
 42    public static final int API_ERROR_AIRPLANE_MODE = -5;
 43
 44    private static final String BASE_URL = "http://venus.fritz.box:4567";
 45
 46    private static final String INSTALLATION_ID = "eu.siacs.conversations.installation-id";
 47
 48    private final XmppConnectionService service;
 49
 50    private final Set<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
 51    private final Set<OnVerification> mOnVerification = Collections.newSetFromMap(new WeakHashMap<>());
 52
 53    private final AtomicBoolean mVerificationInProgress = new AtomicBoolean(false);
 54    private final AtomicBoolean mVerificationRequestInProgress = new AtomicBoolean(false);
 55
 56    QuickConversationsService(XmppConnectionService xmppConnectionService) {
 57        this.service = xmppConnectionService;
 58    }
 59
 60    public void addOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
 61        synchronized (mOnVerificationRequested) {
 62            mOnVerificationRequested.add(onVerificationRequested);
 63        }
 64    }
 65
 66    public void removeOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
 67        synchronized (mOnVerificationRequested) {
 68            mOnVerificationRequested.remove(onVerificationRequested);
 69        }
 70    }
 71
 72    public void addOnVerificationListener(OnVerification onVerification) {
 73        synchronized (mOnVerification) {
 74            mOnVerification.add(onVerification);
 75        }
 76    }
 77
 78    public void removeOnVerificationListener(OnVerification onVerification) {
 79        synchronized (mOnVerification) {
 80            mOnVerification.remove(onVerification);
 81        }
 82    }
 83
 84    public void requestVerification(Phonenumber.PhoneNumber phoneNumber) {
 85        final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
 86        if (mVerificationRequestInProgress.compareAndSet(false, true)) {
 87            new Thread(() -> {
 88                try {
 89
 90                    Thread.sleep(5000);
 91
 92                    final URL url = new URL(BASE_URL + "/authentication/" + e164);
 93                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 94                    connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
 95                    connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
 96                    setHeader(connection);
 97                    final int code = connection.getResponseCode();
 98                    if (code == 200) {
 99                        createAccountAndWait(phoneNumber, 0L);
100                    } else if (code == 429) {
101                        createAccountAndWait(phoneNumber, retryAfter(connection));
102                    } else {
103                        synchronized (mOnVerificationRequested) {
104                            for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
105                                onVerificationRequested.onVerificationRequestFailed(code);
106                            }
107                        }
108                    }
109                } catch (Exception e) {
110                    final int code = getApiErrorCode(e);
111                    synchronized (mOnVerificationRequested) {
112                        for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
113                            onVerificationRequested.onVerificationRequestFailed(code);
114                        }
115                    }
116                } finally {
117                    mVerificationRequestInProgress.set(false);
118                }
119            }).start();
120        }
121
122
123    }
124
125    private void createAccountAndWait(Phonenumber.PhoneNumber phoneNumber, final long timestamp) {
126        String local = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
127        Log.d(Config.LOGTAG, "requesting verification for " + PhoneNumberUtilWrapper.normalize(service, phoneNumber));
128        Jid jid = Jid.of(local, Config.QUICKSY_DOMAIN, null);
129        Account account = AccountUtils.getFirst(service);
130        if (account == null || !account.getJid().asBareJid().equals(jid.asBareJid())) {
131            if (account != null) {
132                service.deleteAccount(account);
133            }
134            account = new Account(jid, CryptoHelper.createPassword(new SecureRandom()));
135            account.setOption(Account.OPTION_DISABLED, true);
136            account.setOption(Account.OPTION_UNVERIFIED, true);
137            service.createAccount(account);
138        }
139        synchronized (mOnVerificationRequested) {
140            for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
141                if (timestamp <= 0) {
142                    onVerificationRequested.onVerificationRequested();
143                } else {
144                    onVerificationRequested.onVerificationRequestedRetryAt(timestamp);
145                }
146            }
147        }
148    }
149
150    public void verify(final Account account, String pin) {
151        if (mVerificationInProgress.compareAndSet(false, true)) {
152            new Thread(() -> {
153                try {
154
155                    Thread.sleep(5000);
156
157                    final URL url = new URL(BASE_URL + "/password");
158                    final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
159                    connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
160                    connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
161                    connection.setRequestMethod("POST");
162                    connection.setRequestProperty("Authorization", Plain.getMessage(account.getUsername(), pin));
163                    setHeader(connection);
164                    final OutputStream os = connection.getOutputStream();
165                    final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
166                    writer.write(account.getPassword());
167                    writer.flush();
168                    writer.close();
169                    os.close();
170                    connection.connect();
171                    final int code = connection.getResponseCode();
172                    if (code == 200) {
173                        account.setOption(Account.OPTION_UNVERIFIED, false);
174                        account.setOption(Account.OPTION_DISABLED, false);
175                        service.updateAccount(account);
176                        synchronized (mOnVerification) {
177                            for (OnVerification onVerification : mOnVerification) {
178                                onVerification.onVerificationSucceeded();
179                            }
180                        }
181                    } else if (code == 429) {
182                        final long retryAfter = retryAfter(connection);
183                        synchronized (mOnVerification) {
184                            for (OnVerification onVerification : mOnVerification) {
185                                onVerification.onVerificationRetryAt(retryAfter);
186                            }
187                        }
188                    } else {
189                        synchronized (mOnVerification) {
190                            for (OnVerification onVerification : mOnVerification) {
191                                onVerification.onVerificationFailed(code);
192                            }
193                        }
194                    }
195                } catch (Exception e) {
196                    final int code = getApiErrorCode(e);
197                    synchronized (mOnVerification) {
198                        for (OnVerification onVerification : mOnVerification) {
199                            onVerification.onVerificationFailed(code);
200                        }
201                    }
202                } finally {
203                    mVerificationInProgress.set(false);
204                }
205            }).start();
206        }
207    }
208
209    private void setHeader(HttpURLConnection connection) {
210        connection.setRequestProperty("User-Agent", service.getIqGenerator().getUserAgent());
211        connection.setRequestProperty("Installation-Id", getInstallationId());
212        connection.setRequestProperty("Accept-Language", Locale.getDefault().getLanguage());
213    }
214
215    private String getInstallationId() {
216        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(service);
217        String id = preferences.getString(INSTALLATION_ID, null);
218        if (id != null) {
219            return id;
220        } else {
221            id = UUID.randomUUID().toString();
222            preferences.edit().putString(INSTALLATION_ID, id).apply();
223            return id;
224        }
225
226    }
227
228    private int getApiErrorCode(Exception e) {
229        if (!service.hasInternetConnection()) {
230            return API_ERROR_AIRPLANE_MODE;
231        } else if (e instanceof UnknownHostException) {
232            return API_ERROR_UNKNOWN_HOST;
233        } else if (e instanceof ConnectException) {
234            return API_ERROR_CONNECT;
235        } else if (e instanceof SSLHandshakeException) {
236            return API_ERROR_SSL_HANDSHAKE;
237        } else {
238            Log.d(Config.LOGTAG, e.getClass().getName());
239            return API_ERROR_OTHER;
240        }
241    }
242
243    private static long retryAfter(HttpURLConnection connection) {
244        try {
245            return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
246        } catch (Exception e) {
247            return 0;
248        }
249    }
250
251    public boolean isVerifying() {
252        return mVerificationInProgress.get();
253    }
254
255    public boolean isRequestingVerification() {
256        return mVerificationRequestInProgress.get();
257    }
258
259    public static boolean isQuicksy() {
260        return true;
261    }
262
263    public interface OnVerificationRequested {
264        void onVerificationRequestFailed(int code);
265
266        void onVerificationRequested();
267
268        void onVerificationRequestedRetryAt(long timestamp);
269    }
270
271    public interface OnVerification {
272        void onVerificationFailed(int code);
273
274        void onVerificationSucceeded();
275
276        void onVerificationRetryAt(long timestamp);
277    }
278}